├── .devcontainer ├── devcontainer.json └── post-install.sh ├── .dockerignore ├── .github ├── CODEOWNERS ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── auto-close.yml │ ├── auto-update.yml │ ├── co-integration-test.yaml │ ├── dev-artifacts-push.yml │ ├── post-merge.yml │ └── pre-merge.yml ├── .gitignore ├── .golangci.yml ├── .yamllint ├── CODE_OF_CONDUCT.md ├── LICENSES └── Apache-2.0.txt ├── Makefile ├── PROJECT ├── README.md ├── REUSE.toml ├── SECURITY.md ├── VERSION ├── api └── v1alpha1 │ ├── clusterconnect_types.go │ ├── groupversion_info.go │ └── zz_generated.deepcopy.go ├── build ├── Dockerfile.connect-agent ├── Dockerfile.connect-controller └── Dockerfile.connect-gateway ├── cmd ├── connect-agent │ └── main.go ├── connect-controller │ └── main.go └── connect-gateway │ └── main.go ├── config ├── crd │ ├── bases │ │ └── cluster.edge-orchestrator.intel.com_clusterconnects.yaml │ ├── deps │ │ └── clusters.cluster.x-k8s.io.yaml │ ├── kustomization.yaml │ └── kustomizeconfig.yaml ├── default │ ├── cert_metrics_manager_patch.yaml │ ├── kustomization.yaml │ ├── manager_metrics_patch.yaml │ └── metrics_service.yaml ├── manager │ ├── kustomization.yaml │ └── manager.yaml ├── network-policy │ ├── allow-metrics-traffic.yaml │ └── kustomization.yaml ├── prometheus │ ├── kustomization.yaml │ ├── monitor.yaml │ └── monitor_tls_patch.yaml ├── rbac │ ├── clusterconnect_admin_role.yaml │ ├── clusterconnect_editor_role.yaml │ ├── clusterconnect_viewer_role.yaml │ ├── kustomization.yaml │ ├── leader_election_role.yaml │ ├── leader_election_role_binding.yaml │ ├── metrics_auth_role.yaml │ ├── metrics_auth_role_binding.yaml │ ├── metrics_reader_role.yaml │ ├── role.yaml │ ├── role_binding.yaml │ └── service_account.yaml └── samples │ ├── cluster_v1alpha1_clusterconnect.yaml │ ├── cluster_v1alpha1_clusterconnect_with_controlplane.yaml │ └── rke2_v1beta1_control_plane.yaml ├── deployment └── charts │ ├── cluster-connect-gateway-crd │ ├── .helmignore │ ├── Chart.yaml │ ├── templates │ │ └── cluster.edge-orchestrator.intel.com_clusterconnects.yaml │ └── values.yaml │ └── cluster-connect-gateway │ ├── .helmignore │ ├── Chart.lock │ ├── Chart.yaml │ ├── charts │ └── connect-agent │ │ ├── .helmignore │ │ ├── Chart.yaml │ │ ├── templates │ │ ├── _helpers.tpl │ │ └── configmap.yaml │ │ └── values.yaml │ ├── files │ ├── dashboards │ │ ├── dashboard.json │ │ └── dashboard.json.license │ └── openpolicyagent │ │ └── policy.rego │ ├── templates │ ├── _helpers.tpl │ ├── configmap.yaml │ ├── deployment-controller.yaml │ ├── deployment-gateway.yaml │ ├── rbac.yaml │ ├── service.yaml │ ├── service_metrics.yaml │ ├── service_monitor.yaml │ ├── serviceaccount.yaml │ └── traefik-ingress.yaml │ └── values.yaml ├── go.mod ├── go.sum ├── hack └── boilerplate.go.txt ├── internal ├── agent │ └── agent.go ├── agentconfig │ ├── agentconfig.go │ └── agentconfig_test.go ├── auth │ ├── agent_token_authorizer.go │ ├── agent_token_authorizer_test.go │ ├── mocks │ │ └── token_manager_mock.go │ ├── secret_token_manager.go │ ├── secret_token_manager_test.go │ ├── token_manager.go │ └── token_manager_test.go ├── controller │ ├── clusterconnect_controller.go │ ├── clusterconnect_controller_test.go │ ├── conditions.go │ └── suite_test.go ├── metrics │ └── metrics.go ├── middleware │ ├── middleware.go │ └── middleware_test.go ├── opa │ └── opa.go ├── provider │ └── provider_manager.go ├── server │ ├── error_responder.go │ ├── kubeapi_handler.go │ ├── server.go │ └── server_test.go └── utils │ ├── certutil │ ├── cert.go │ └── cert_test.go │ └── kubeutil │ ├── k8sclient.go │ ├── k8sclient_test.go │ └── kubeconfig.go ├── logging.yaml ├── requirements.txt ├── test ├── e2e │ ├── controller_test.go │ ├── deployment_test.go │ └── e2e_suite_test.go ├── kind-config.yaml ├── local-e2e.sh ├── resources │ ├── capiproviders │ │ ├── core-provider.yaml │ │ ├── docker-infra-provider.yaml │ │ └── rke2-provider.yaml │ └── testdata │ │ ├── namespace.yaml │ │ ├── test-cluster-connect.yaml │ │ ├── test-cluster-controlplane-rke2.yaml │ │ └── test-cluster-infra-docker.yaml ├── setup.sh └── utils │ └── utils.go └── trivy.yaml /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Kubebuilder DevContainer", 3 | "image": "golang:1.23", 4 | "features": { 5 | "ghcr.io/devcontainers/features/docker-in-docker:2": {}, 6 | "ghcr.io/devcontainers/features/git:1": {} 7 | }, 8 | 9 | "runArgs": ["--network=host"], 10 | 11 | "customizations": { 12 | "vscode": { 13 | "settings": { 14 | "terminal.integrated.shell.linux": "/bin/bash" 15 | }, 16 | "extensions": [ 17 | "ms-kubernetes-tools.vscode-kubernetes-tools", 18 | "ms-azuretools.vscode-docker" 19 | ] 20 | } 21 | }, 22 | 23 | "onCreateCommand": "bash .devcontainer/post-install.sh" 24 | } 25 | 26 | -------------------------------------------------------------------------------- /.devcontainer/post-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | 4 | curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-amd64 5 | chmod +x ./kind 6 | mv ./kind /usr/local/bin/kind 7 | 8 | curl -L -o kubebuilder https://go.kubebuilder.io/dl/latest/linux/amd64 9 | chmod +x kubebuilder 10 | mv kubebuilder /usr/local/bin/ 11 | 12 | KUBECTL_VERSION=$(curl -L -s https://dl.k8s.io/release/stable.txt) 13 | curl -LO "https://dl.k8s.io/release/$KUBECTL_VERSION/bin/linux/amd64/kubectl" 14 | chmod +x kubectl 15 | mv kubectl /usr/local/bin/kubectl 16 | 17 | docker network create -d=bridge --subnet=172.19.0.0/24 kind 18 | 19 | kind version 20 | kubebuilder version 21 | docker --version 22 | go version 23 | kubectl version --client 24 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | # Ignore build and test binaries. 3 | bin/ 4 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | * @andybavier @hyunsun @gcgirish @madalazar @jdanieck -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | ### Description 3 | 4 | Please include a summary of the changes and the related issue. List any dependencies that are required for this change. 5 | 6 | Fixes # (issue) 7 | 8 | ### Any Newly Introduced Dependencies 9 | 10 | Please describe any newly introduced 3rd party dependencies in this change. List their name, license information and how they are used in the project. 11 | 12 | ### How Has This Been Tested? 13 | 14 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration 15 | 16 | ### Checklist: 17 | 18 | - [ ] I agree to use the APACHE-2.0 license for my code changes 19 | - [ ] I have not introduced any 3rd party dependency changes 20 | - [ ] I have performed a self-review of my code -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | --- 5 | version: 2 6 | updates: 7 | - package-ecosystem: "gomod" 8 | directories: 9 | - "/" 10 | schedule: 11 | interval: daily 12 | open-pull-requests-limit: 10 13 | commit-message: 14 | prefix: "[gomod] " 15 | - package-ecosystem: "github-actions" 16 | directory: "/" 17 | schedule: 18 | interval: daily 19 | open-pull-requests-limit: 10 20 | commit-message: 21 | prefix: "[gha] " 22 | - package-ecosystem: "docker" 23 | directories: 24 | - "/" 25 | schedule: 26 | interval: daily 27 | open-pull-requests-limit: 10 28 | commit-message: 29 | prefix: "[docker] " 30 | -------------------------------------------------------------------------------- /.github/workflows/auto-close.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | --- 5 | 6 | name: Stale Pull Requests 7 | 8 | # After 30 days of no activity on a PR, the PR should be marked as stale, 9 | # a comment made on the PR informing the author of the new status, 10 | # and closed after 15 days if there is no further activity from the change to stale state. 11 | on: 12 | schedule: 13 | - cron: '30 1 * * *' # run every day 14 | workflow_dispatch: {} 15 | 16 | permissions: {} 17 | 18 | jobs: 19 | stale-auto-close: 20 | permissions: 21 | contents: read 22 | pull-requests: write 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0 26 | with: 27 | repo-token: ${{ secrets.GITHUB_TOKEN }} 28 | stale-pr-message: 'This pull request is stale because it has been open 30 days with no activity. Make a comment or update the PR to avoid closing PR after 15 days.' 29 | days-before-pr-stale: 30 30 | days-before-pr-close: 15 31 | remove-pr-stale-when-updated: 'true' 32 | close-pr-message: 'This pull request was automatically closed due to inactivity' -------------------------------------------------------------------------------- /.github/workflows/auto-update.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | --- 5 | 6 | name: Auto Update PR 7 | 8 | # On push to the main branch and support branches, update any branches that are out of date 9 | # and have auto-merge enabled. If the branch is currently out of date with the base branch, 10 | # it must be first manually updated and then will be kept up to date on future runs. 11 | on: 12 | push: 13 | branches: 14 | - main 15 | - release-* 16 | 17 | permissions: {} 18 | 19 | concurrency: 20 | group: ${{ github.workflow }}-${{ github.ref }} 21 | cancel-in-progress: true 22 | 23 | jobs: 24 | update-pull-requests: 25 | permissions: 26 | contents: read 27 | pull-requests: write 28 | runs-on: ubuntu-latest 29 | 30 | steps: 31 | - name: Checkout repository 32 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 33 | with: 34 | persist-credentials: false 35 | 36 | - name: Update pull requests 37 | uses: open-edge-platform/orch-ci/.github/actions/pr_updater@3bdd409ccf738472c6e1547d14628b51c70dbe99 # 0.1.21 38 | with: 39 | github_token: ${{ secrets.SYS_ORCH_GITHUB }} 40 | -------------------------------------------------------------------------------- /.github/workflows/co-integration-test.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | --- 5 | 6 | name: CO Integration test CI Pipeline 7 | 8 | on: 9 | pull_request: 10 | branches: 11 | - main 12 | - release-* 13 | workflow_dispatch: 14 | 15 | permissions: {} 16 | 17 | jobs: 18 | integration-smoke-test: 19 | permissions: 20 | contents: read 21 | runs-on: ubuntu-24.04-16core-64GB 22 | if: true 23 | env: 24 | VERSION: ${{ github.head_ref }} # Use the component branch that triggered the action for the test 25 | steps: 26 | - name: Checkout orch ci 27 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 28 | with: 29 | repository: open-edge-platform/orch-ci 30 | path: ci 31 | ref: "main" 32 | token: ${{ secrets.SYS_ORCH_GITHUB }} 33 | persist-credentials: false 34 | 35 | - name: Checkout cluster-tests for integration tests 36 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 37 | with: 38 | repository: open-edge-platform/cluster-tests 39 | path: cluster-tests 40 | ref: "main" 41 | token: ${{ secrets.SYS_ORCH_GITHUB }} 42 | persist-credentials: false 43 | 44 | - name: Bootstrap CI environment 45 | uses: ./ci/.github/actions/bootstrap 46 | with: 47 | gh_token: ${{ secrets.SYS_ORCH_GITHUB }} 48 | 49 | - name: Run make test with additional config 50 | env: 51 | VERSION: ${{ env.VERSION }} 52 | run: | 53 | cd cluster-tests 54 | ADDITIONAL_CONFIG="{\"components\":[{\"name\":\"cluster-connect-gateway\", \"skip-local-build\": false, \"git-repo\": {\"version\":\"${VERSION}\"}}]}" make test 55 | -------------------------------------------------------------------------------- /.github/workflows/dev-artifacts-push.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | --- 5 | 6 | name: Push development artifacts to the Release Service 7 | 8 | on: 9 | # manual trigger from the Actions tab 10 | workflow_dispatch: 11 | 12 | env: 13 | VERSION_SUFFIX: -test 14 | 15 | permissions: {} 16 | 17 | jobs: 18 | dev-artifacts-push: 19 | runs-on: ubuntu-latest 20 | permissions: 21 | contents: read 22 | steps: 23 | - name: Checkout code 24 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 25 | with: 26 | persist-credentials: false 27 | 28 | - name: Build Docker image 29 | run: | 30 | make docker-build 31 | 32 | - name: Build Helm chart 33 | run: | 34 | make helm-build 35 | 36 | - name: Configure AWS credentials 37 | uses: aws-actions/configure-aws-credentials@ececac1a45f3b08a01d2dd070d28d111c5fe6722 # v4.0.1 38 | with: 39 | aws-access-key-id: ${{ secrets.NO_AUTH_ECR_PUSH_USERNAME }} 40 | aws-secret-access-key: ${{ secrets.NO_AUTH_ECR_PUSH_PASSWD }} 41 | aws-region: us-west-2 42 | 43 | - name: Login to Amazon ECR 44 | uses: aws-actions/amazon-ecr-login@062b18b96a7aff071d4dc91bc00c4c1a7945b076 # v2.0.1 45 | with: 46 | registries: "080137407410" 47 | 48 | - name: Push Docker image 49 | run: | 50 | make docker-push 51 | 52 | - name: Push Helm chart 53 | run: | 54 | make helm-push 55 | -------------------------------------------------------------------------------- /.github/workflows/post-merge.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | --- 5 | 6 | name: Post-Merge CI Pipeline 7 | 8 | permissions: {} 9 | 10 | on: 11 | push: 12 | branches: 13 | - main 14 | - release-* 15 | workflow_dispatch: 16 | 17 | jobs: 18 | post-merge: 19 | permissions: 20 | contents: read 21 | security-events: write 22 | id-token: write 23 | uses: open-edge-platform/orch-ci/.github/workflows/post-merge.yml@main 24 | with: 25 | cache_go: true 26 | remove_cache_go: true 27 | run_build: true 28 | run_version_check: true 29 | run_dep_version_check: true 30 | run_version_tag: true 31 | run_docker_build: true 32 | run_docker_push: true 33 | run_helm_build: true 34 | run_helm_push: true 35 | secrets: 36 | SYS_ORCH_GITHUB: ${{ secrets.SYS_ORCH_GITHUB }} 37 | COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }} 38 | COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }} 39 | NO_AUTH_ECR_PUSH_USERNAME: ${{ secrets.NO_AUTH_ECR_PUSH_USERNAME }} 40 | NO_AUTH_ECR_PUSH_PASSWD: ${{ secrets.NO_AUTH_ECR_PUSH_PASSWD }} 41 | MSTEAMS_WEBHOOK: ${{ secrets.TEAMS_WEBHOOK }} 42 | -------------------------------------------------------------------------------- /.github/workflows/pre-merge.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | --- 5 | 6 | name: Pre-Merge CI Pipeline 7 | 8 | on: 9 | pull_request: 10 | branches: 11 | - main 12 | - release-* 13 | workflow_dispatch: 14 | 15 | permissions: {} 16 | 17 | jobs: 18 | pre-merge: 19 | permissions: 20 | contents: read 21 | uses: open-edge-platform/orch-ci/.github/workflows/pre-merge.yml@0.1.21 22 | with: 23 | bootstrap_tools: "base,go" 24 | cache_go: true 25 | remove_cache_go: true 26 | run_security_scans: true 27 | run_version_check: true 28 | run_dep_version_check: true 29 | run_build: true 30 | run_lint: true 31 | run_test: true 32 | run_validate_clean_folder: false 33 | run_docker_build: true 34 | run_docker_push: false 35 | run_helm_build: true 36 | run_helm_push: false 37 | run_artifact: false 38 | version_suffix: "-pr-${{ github.event.number }}" 39 | secrets: # zizmor: ignore[secrets-inherit] 40 | inherit 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | bin/* 8 | Dockerfile.cross 9 | .cache 10 | # Test binary, built with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | coverage.html 16 | coverage.xml 17 | 18 | # Go workspace file 19 | go.work 20 | 21 | # Kubernetes Generated files - skip generated files, except for vendored files 22 | !vendor/**/zz_generated.* 23 | *.tgz 24 | 25 | # editor and IDE paraphernalia 26 | .idea 27 | .vscode 28 | *.swp 29 | *.swo 30 | *~ 31 | 32 | # Test generated files 33 | venv-env 34 | vendor 35 | test/kubeconfig 36 | 37 | # CI 38 | ci 39 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | run: 5 | timeout: 5m 6 | tests: false # Skip linter checks on unit test files 7 | 8 | linters-settings: 9 | dupl: 10 | threshold: 200 # TODO: reduce to 100 11 | misspell: 12 | locale: US 13 | cyclop: 14 | max-complexity: 24 # TODO: gradually lower these values to 10/5.0 15 | package-average: 9 16 | 17 | unparam: 18 | check-exported: false 19 | 20 | gosec: 21 | includes: 22 | - G401 23 | - G306 24 | - G101 25 | - G102 26 | - G103 27 | - G104 28 | - G106 29 | - G107 30 | - G108 31 | - G109 32 | - G110 33 | 34 | linters: 35 | fast: false 36 | disable-all: false 37 | enable: 38 | # - bodyclose # false positives, even with nolint directive applied 39 | - cyclop 40 | - dupl 41 | - errcheck 42 | - gofmt 43 | - gosec 44 | - gosimple 45 | - govet 46 | - ineffassign 47 | - misspell 48 | - nilerr 49 | - staticcheck 50 | - typecheck 51 | - unconvert 52 | - unparam 53 | - unused 54 | - goimports 55 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | extends: default 5 | 6 | rules: 7 | document-start: disable 8 | indentation: disable 9 | line-length: disable 10 | # max: 99 11 | # level: warning 12 | 13 | # Kubebuilder comments don't have leading space 14 | comments: 15 | require-starting-space: false 16 | min-spaces-from-content: 1 17 | 18 | ignore: | 19 | .github/ 20 | .cache 21 | config/ 22 | vendor/ 23 | deployment/ 24 | ci/ 25 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | CommunityCodeOfConduct AT intel DOT com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series of 86 | actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or permanent 93 | ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within the 113 | community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.1, available at 119 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 126 | [https://www.contributor-covenant.org/translations][translations]. 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 130 | [Mozilla CoC]: https://github.com/mozilla/diversity 131 | [FAQ]: https://www.contributor-covenant.org/faq 132 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | # Code generated by tool. DO NOT EDIT. 2 | # This file is used to track the info used to scaffold your project 3 | # and allow the plugins properly work. 4 | # More info: https://book.kubebuilder.io/reference/project-config.html 5 | domain: edge-orchestrator.intel.com 6 | layout: 7 | - go.kubebuilder.io/v4 8 | projectName: cluster-connect-gateway 9 | repo: github.com/open-edge-platform/cluster-connect-gateway 10 | resources: 11 | - api: 12 | crdVersion: v1 13 | namespaced: true 14 | controller: true 15 | domain: edge-orchestrator.intel.com 16 | group: cluster 17 | kind: ClusterConnection 18 | path: github.com/open-edge-platform/cluster-connect-gateway/api/v1alpha1 19 | version: v1alpha1 20 | version: "3" 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cluster Connect Gateway 2 | 3 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 4 | [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/open-edge-platform/cluster-connect-gateway/badge)](https://scorecard.dev/viewer/?uri=github.com/open-edge-platform/cluster-connect-gateway) 5 | 6 | ## Table of Contents 7 | 8 | - [Overview](#overview) 9 | - [Get Started](#get-started) 10 | - [Develop](#develop) 11 | - [Contribute](#contribute) 12 | - [Community and Support](#community-and-support) 13 | - [License](#license) 14 | 15 | ## Overview 16 | 17 | Cluster Connect Gateway helps solve the challenge of accessing Kubernetes API and Services on edge clusters that are behind NAT or firewalls. By establishing a secure tunnel between a gateway in the management cluster and agents on edge clusters, users can seamlessly access these services without exposing the edge clusters to the external network. 18 | 19 | Key features include: 20 | 21 | - **Secure Tunnel**: Establishes a websocket connection between the management cluster and edge clusters. 22 | - **Agent-Initiated Connection**: The connection is initiated by the Connect Agent on the edge, working seamlessly with edge clusters behind NAT or firewalls. 23 | - **Service Access**: Allows users to access Kubernetes API and Services running on multiple edge clusters through a centralized gateway. 24 | - **OIDC integration**: Supports OIDC integration, ensuring only authenticated users can access Kubernetes APIs and Services on the edges. 25 | - **Cluster API integration**: Seamlessly work with Cluster API. 26 | - **Intel Open Edge Platform integration**: Seamlessly work with Intel® Open Edge Platform. 27 | 28 | Read more about Cluster Connect Gateway in the [Edge Cluster Orchestrator Developer Guide][cluster-orch-dev-guide-url] for internals and software architecture. 29 | 30 | ## Get Started 31 | 32 | The recommended way to try out the Cluster Connect Gateway is by using the Edge Orchestrator. 33 | Refer to the [Getting Started Guide](https://docs.openedgeplatform.intel.com/edge-manage-docs/main/user_guide/get_started_guide/index.html) to get started with the Edge Orchestrator. 34 | 35 | ## Develop 36 | 37 | If you are interested in contributing to the development of Cluster Connect Gateway, you will need an environment where you can use it to create and delete clusters. 38 | 39 | The [cluster-tests](https://github.com/open-edge-platform/cluster-tests) repo provides a lightweight environment for integration testing of Cluster Connect Gateway as well as other Edge Orchestrator components related to cluster management. Clone that repo, change into the cluster-tests directory, and run: 40 | 41 | ``` 42 | make test 43 | ``` 44 | 45 | This command creates a KinD cluster and deploys cert-manager, Cluster API operator, CAPI Provider for Intel, Cluster Manager, and Cluster Connect Gateway. It then creates and deletes a cluster inside a Kubernetes pod. Consult the cluster-tests [README](https://github.com/open-edge-platform/cluster-tests/blob/main/README.md) for details on how to test your code in this environment. 46 | 47 | ## Contribute 48 | 49 | We welcome contributions from the community! To contribute, please open a pull request to have your changes reviewed and merged into the main. To learn how to contribute to the project, see the [contributor's guide](https://docs.openedgeplatform.intel.com/edge-manage-docs/main/developer_guide/contributor_guide/index.html). We encourage you to add appropriate unit tests and e2e tests if your contribution introduces a new feature. 50 | 51 | Additionally, ensure the following commands are successful: 52 | 53 | ``` 54 | make test 55 | make lint 56 | make license 57 | ``` 58 | 59 | ## Community and Support 60 | 61 | To learn more about the project, its community, and governance, visit the [Edge Orchestrator Community](https://docs.openedgeplatform.intel.com/edge-manage-docs/main/index.html). 62 | For support, start with [Troubleshooting](https://docs.openedgeplatform.intel.com/edge-manage-docs/main/developer_guide/troubleshooting/index.html) or contact us. 63 | 64 | ## License 65 | 66 | Cluster Connect Gateway is licensed under [Apache 2.0 License](LICENSES/Apache-2.0.txt) 67 | 68 | Last Updated Date: April 16, 2025 69 | 70 | [cluster-orch-dev-guide-url]: https://docs.openedgeplatform.intel.com/edge-manage-docs/main/developer_guide/cluster_orch/index.html 71 | -------------------------------------------------------------------------------- /REUSE.toml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 Intel Corporation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | version = 1 5 | 6 | [[annotations]] 7 | path = [ 8 | "*.md", 9 | ".github/*", 10 | "VERSION", 11 | "go.mod", 12 | "go.sum", 13 | ".gitignore", 14 | "api/*", 15 | ".dockerignore", 16 | ".devcontainer/*", 17 | "bin", 18 | "test", 19 | "config/**", 20 | "PROJECT", 21 | "mocks/*", 22 | "internal/auth/mocks/*", 23 | "hack/*" 24 | ] 25 | SPDX-FileCopyrightText = "2025 Intel Corporation" 26 | SPDX-License-Identifier = "Apache-2.0" 27 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | Intel is committed to rapidly addressing security vulnerabilities affecting our customers and providing clear guidance on the solution, impact, severity and mitigation. 3 | 4 | ## Reporting a Vulnerability 5 | Please report any security vulnerabilities in this project utilizing the guidelines [here](https://www.intel.com/content/www/us/en/security-center/vulnerability-handling-guidelines.html). 6 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.2.0 2 | -------------------------------------------------------------------------------- /api/v1alpha1/clusterconnect_types.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package v1alpha1 5 | 6 | import ( 7 | corev1 "k8s.io/api/core/v1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 10 | ) 11 | 12 | const ( 13 | ClusterConnectKind = "ClusterConnect" 14 | 15 | // AuthTokenReadyCondition reports if an authentication token Secret for an object is ready. 16 | AuthTokenReadyCondition = "AuthTokenReady" 17 | 18 | // ConnectAgentManifestReadyCondition reports if the agent pod manifest is ready in status. 19 | AgentManifestGeneratedCondition = "ConnectAgentManifestGenerated" 20 | 21 | // ControlPlaneEndpointSetCondition reports if the ControlPlane endpoint URL is generated. 22 | ControlPlaneEndpointSetCondition = "ControlPlaneEndpointSet" 23 | 24 | // ClusterSpecUpdated reports if the Cluster spec is updated with connect agent configuration. 25 | // Note: This condition is valid only when CAPI ClusterRef is set. 26 | ClusterSpecUpdatedCondition = "ClusterSpecUpdated" 27 | 28 | // TopologyReconciled reports if the ControlPlane spec is updated with agent pod manifest. 29 | // Note: This condition is valid only when CAPI ClusterRef is set. 30 | TopologyReconciledCondition = "TopologyReconciled" 31 | 32 | // KubeconfigReadyCondition reports if the kubeconfig Secret is ready. 33 | // Note: This condition is valid only when CAPI ClusterRef is set. 34 | KubeconfigReadyCondition = "KubeconfigReady" 35 | 36 | // ReadyReason applies to a condition surfacing object readiness. 37 | ReadyReason = "Ready" 38 | 39 | // NotReadyReason applies to a condition surfacing object not satisfying readiness criteria. 40 | NotReadyReason = "NotReady" 41 | 42 | // ReadyUnknownReason applies to a condition surfacing object readiness unknown. 43 | ReadyUnknownReason = "ReadyUnknown" 44 | ) 45 | 46 | // ConnectionProbe condition and corresponding reasons. 47 | const ( 48 | ConnectionProbeCondition = "ConnectionProbe" 49 | 50 | // ConnectionProbeFailedReason surfaces issues with the connection to the workload's cluster connect-agent. 51 | ConnectionProbeFailedReason = "ProbeFailed" 52 | 53 | // ConnectionProbeSucceededReason is used to report a working connection with the workload's cluster connect-agent. 54 | ConnectionProbeSucceededReason = "ProbeSucceeded" 55 | ) 56 | 57 | // ClusterConnectSpec defines the desired state of ClusterConnect. 58 | type ClusterConnectSpec struct { 59 | // ClusterRef is an optional reference to a CAPI provider-specific resource that holds 60 | // the details for the Cluster to connect. 61 | // +optional 62 | ClusterRef *corev1.ObjectReference `json:"clusterRef,omitempty"` 63 | 64 | // ServerCertRef is an optional reference to a PEM-encoded server certificate authority data for the kubeapi-server to proxy. 65 | // The secret format is intended to match the format of the -ca secret used in CAPI. 66 | // +optional 67 | ServerCertRef *corev1.ObjectReference `json:"serverCertRef,omitempty"` 68 | 69 | // ClientCertRef is an optional reference to a PEM-encoded client certificates for the cluster administrator 70 | // to authenticate with the kubeapi-server. 71 | // The secret format is intended to match the format of the -cca secret used in cluster-api. 72 | // +optional 73 | ClientCertRef *corev1.ObjectReference `json:"clientCertRef,omitempty"` 74 | } 75 | 76 | // ClusterConnectStatus defines the observed state of ClusterConnect. 77 | type ClusterConnectStatus struct { 78 | // Ready indicates connect-agent pod manifest is ready to be consumed. 79 | // +optional 80 | Ready bool `json:"ready,omitempty"` 81 | 82 | // ControlPlaneEndpoint provides the URL for accessing the kubeapi-server through the connection gateway. 83 | // +optional 84 | ControlPlaneEndpoint clusterv1.APIEndpoint `json:"controlPlaneEndpoint,omitempty"` 85 | 86 | // AgentManifest is the connect-agent Pod manifest. 87 | // +optional 88 | AgentManifest string `json:"agentManifest,omitempty"` 89 | 90 | // ConnectionProbe defines the state of the connection with connect-agent. 91 | ConnectionProbe ConnectionProbeState `json:"connectionProbe,omitempty"` 92 | 93 | // Conditions defines current connection state of the cluster. 94 | // Known condition types are TBD. 95 | // +optional 96 | Conditions []metav1.Condition `json:"conditions,omitempty"` 97 | } 98 | 99 | type ConnectionProbeState struct { 100 | // LastProbeTimestamp is the time when the health probe was executed last. 101 | LastProbeTimestamp metav1.Time `json:"lastProbeTimestamp,omitempty"` 102 | 103 | // LastProbeSuccessTimestamp is the time when the health probe was successfully executed last. 104 | LastProbeSuccessTimestamp metav1.Time `json:"lastProbeSuccessTimestamp,omitempty"` 105 | } 106 | 107 | // +kubebuilder:object:root=true 108 | // +kubebuilder:subresource:status 109 | // +kubebuilder:resource:path=clusterconnects,shortName=ccon,scope=Cluster 110 | // +kubebuilder:printcolumn:name="Ready",type="boolean",JSONPath=".status.ready" 111 | // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Age of this resource" 112 | 113 | // ClusterConnect is the Schema for the clusterconnects API. 114 | type ClusterConnect struct { 115 | metav1.TypeMeta `json:",inline"` 116 | metav1.ObjectMeta `json:"metadata,omitempty"` 117 | 118 | Spec ClusterConnectSpec `json:"spec,omitempty"` 119 | Status ClusterConnectStatus `json:"status,omitempty"` 120 | } 121 | 122 | // GetTunnelID returns the tunnel ID. 123 | // ClusterConnect object ID is used as globally unique tunnel ID. 124 | func (c *ClusterConnect) GetTunnelID() string { 125 | return c.Name 126 | } 127 | 128 | // GetConditions returns the set of conditions for this object. 129 | func (c *ClusterConnect) GetConditions() []metav1.Condition { 130 | return c.Status.Conditions 131 | } 132 | 133 | // GetV1Beta2Conditions returns the set of conditions for this object. 134 | // Implements Cluster API condition setter interface. 135 | func (c *ClusterConnect) GetV1Beta2Conditions() []metav1.Condition { 136 | return c.Status.Conditions 137 | } 138 | 139 | // SetConditions sets the conditions on this object. 140 | func (c *ClusterConnect) SetConditions(conditions []metav1.Condition) { 141 | c.Status.Conditions = conditions 142 | } 143 | 144 | // SetV1Beta2Conditions sets the conditions on this object. 145 | // Implements Cluster API condition setter interface. 146 | func (c *ClusterConnect) SetV1Beta2Conditions(conditions []metav1.Condition) { 147 | c.Status.Conditions = conditions 148 | } 149 | 150 | // +kubebuilder:object:root=true 151 | 152 | // ClusterConnectList contains a list of ClusterConnect. 153 | type ClusterConnectList struct { 154 | metav1.TypeMeta `json:",inline"` 155 | metav1.ListMeta `json:"metadata,omitempty"` 156 | Items []ClusterConnect `json:"items"` 157 | } 158 | 159 | func init() { 160 | objectTypes = append(objectTypes, &ClusterConnect{}, &ClusterConnectList{}) 161 | } 162 | -------------------------------------------------------------------------------- /api/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Package v1alpha1 contains API Schema definitions for the cluster v1alpha1 API group. 5 | // +kubebuilder:object:generate=true 6 | // +groupName=cluster.edge-orchestrator.intel.com 7 | package v1alpha1 8 | 9 | import ( 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | runtime "k8s.io/apimachinery/pkg/runtime" 12 | "k8s.io/apimachinery/pkg/runtime/schema" 13 | ) 14 | 15 | var ( 16 | // GroupVersion is group version used to register these objects. 17 | GroupVersion = schema.GroupVersion{Group: "cluster.edge-orchestrator.intel.com", Version: "v1alpha1"} 18 | 19 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme. 20 | SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) 21 | 22 | // AddToScheme adds the types in this group-version to the given scheme. 23 | AddToScheme = SchemeBuilder.AddToScheme 24 | 25 | ClusterConnectResource = "clusterconnects" 26 | 27 | objectTypes = []runtime.Object{} 28 | ) 29 | 30 | func addKnownTypes(scheme *runtime.Scheme) error { 31 | scheme.AddKnownTypes(GroupVersion, objectTypes...) 32 | metav1.AddToGroupVersion(scheme, GroupVersion) 33 | 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /api/v1alpha1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | 3 | // SPDX-FileCopyrightText: (C) 2025 Intel Corporation 4 | // SPDX-License-Identifier: Apache-2.0 5 | 6 | // Code generated by controller-gen. DO NOT EDIT. 7 | 8 | package v1alpha1 9 | 10 | import ( 11 | "k8s.io/api/core/v1" 12 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | "k8s.io/apimachinery/pkg/runtime" 14 | ) 15 | 16 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 17 | func (in *ClusterConnect) DeepCopyInto(out *ClusterConnect) { 18 | *out = *in 19 | out.TypeMeta = in.TypeMeta 20 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 21 | in.Spec.DeepCopyInto(&out.Spec) 22 | in.Status.DeepCopyInto(&out.Status) 23 | } 24 | 25 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterConnect. 26 | func (in *ClusterConnect) DeepCopy() *ClusterConnect { 27 | if in == nil { 28 | return nil 29 | } 30 | out := new(ClusterConnect) 31 | in.DeepCopyInto(out) 32 | return out 33 | } 34 | 35 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 36 | func (in *ClusterConnect) DeepCopyObject() runtime.Object { 37 | if c := in.DeepCopy(); c != nil { 38 | return c 39 | } 40 | return nil 41 | } 42 | 43 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 44 | func (in *ClusterConnectList) DeepCopyInto(out *ClusterConnectList) { 45 | *out = *in 46 | out.TypeMeta = in.TypeMeta 47 | in.ListMeta.DeepCopyInto(&out.ListMeta) 48 | if in.Items != nil { 49 | in, out := &in.Items, &out.Items 50 | *out = make([]ClusterConnect, len(*in)) 51 | for i := range *in { 52 | (*in)[i].DeepCopyInto(&(*out)[i]) 53 | } 54 | } 55 | } 56 | 57 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterConnectList. 58 | func (in *ClusterConnectList) DeepCopy() *ClusterConnectList { 59 | if in == nil { 60 | return nil 61 | } 62 | out := new(ClusterConnectList) 63 | in.DeepCopyInto(out) 64 | return out 65 | } 66 | 67 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 68 | func (in *ClusterConnectList) DeepCopyObject() runtime.Object { 69 | if c := in.DeepCopy(); c != nil { 70 | return c 71 | } 72 | return nil 73 | } 74 | 75 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 76 | func (in *ClusterConnectSpec) DeepCopyInto(out *ClusterConnectSpec) { 77 | *out = *in 78 | if in.ClusterRef != nil { 79 | in, out := &in.ClusterRef, &out.ClusterRef 80 | *out = new(v1.ObjectReference) 81 | **out = **in 82 | } 83 | if in.ServerCertRef != nil { 84 | in, out := &in.ServerCertRef, &out.ServerCertRef 85 | *out = new(v1.ObjectReference) 86 | **out = **in 87 | } 88 | if in.ClientCertRef != nil { 89 | in, out := &in.ClientCertRef, &out.ClientCertRef 90 | *out = new(v1.ObjectReference) 91 | **out = **in 92 | } 93 | } 94 | 95 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterConnectSpec. 96 | func (in *ClusterConnectSpec) DeepCopy() *ClusterConnectSpec { 97 | if in == nil { 98 | return nil 99 | } 100 | out := new(ClusterConnectSpec) 101 | in.DeepCopyInto(out) 102 | return out 103 | } 104 | 105 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 106 | func (in *ClusterConnectStatus) DeepCopyInto(out *ClusterConnectStatus) { 107 | *out = *in 108 | out.ControlPlaneEndpoint = in.ControlPlaneEndpoint 109 | in.ConnectionProbe.DeepCopyInto(&out.ConnectionProbe) 110 | if in.Conditions != nil { 111 | in, out := &in.Conditions, &out.Conditions 112 | *out = make([]metav1.Condition, len(*in)) 113 | for i := range *in { 114 | (*in)[i].DeepCopyInto(&(*out)[i]) 115 | } 116 | } 117 | } 118 | 119 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterConnectStatus. 120 | func (in *ClusterConnectStatus) DeepCopy() *ClusterConnectStatus { 121 | if in == nil { 122 | return nil 123 | } 124 | out := new(ClusterConnectStatus) 125 | in.DeepCopyInto(out) 126 | return out 127 | } 128 | 129 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 130 | func (in *ConnectionProbeState) DeepCopyInto(out *ConnectionProbeState) { 131 | *out = *in 132 | in.LastProbeTimestamp.DeepCopyInto(&out.LastProbeTimestamp) 133 | in.LastProbeSuccessTimestamp.DeepCopyInto(&out.LastProbeSuccessTimestamp) 134 | } 135 | 136 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConnectionProbeState. 137 | func (in *ConnectionProbeState) DeepCopy() *ConnectionProbeState { 138 | if in == nil { 139 | return nil 140 | } 141 | out := new(ConnectionProbeState) 142 | in.DeepCopyInto(out) 143 | return out 144 | } 145 | -------------------------------------------------------------------------------- /build/Dockerfile.connect-agent: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # Build the connect-agent 5 | FROM golang:1.23.6 AS builder 6 | ARG TARGETOS 7 | ARG TARGETARCH 8 | ENV CGO_ENABLED=0 9 | ENV GO111MODULE=on 10 | 11 | WORKDIR /workspace 12 | # Copy the Go Modules manifests 13 | COPY go.mod go.mod 14 | COPY go.sum go.sum 15 | 16 | # Copy the go source 17 | COPY cmd/ cmd 18 | COPY api/ api/ 19 | COPY internal/ internal/ 20 | COPY vendor/ vendor/ 21 | COPY Makefile Makefile 22 | COPY VERSION VERSION 23 | 24 | # Build the connect-agent binary 25 | RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} make build-agent 26 | 27 | # Use distroless as minimal base image to package the connect-agent binary 28 | FROM gcr.io/distroless/static:nonroot 29 | USER nonroot 30 | 31 | ARG org_oci_version=unknown 32 | ARG org_oci_source=unknown 33 | ARG org_oci_revision=unknown 34 | ARG org_oci_created=unknown 35 | 36 | LABEL org.opencontainers.image.version=$org_oci_version \ 37 | org.opencontainers.image.source=$org_oci_source \ 38 | org.opencontainers.image.revision=$org_oci_revision \ 39 | org.opencontainers.image.created=$org_oci_created 40 | 41 | WORKDIR / 42 | COPY --from=builder /workspace/bin/connect-agent . 43 | USER 65532:65532 -------------------------------------------------------------------------------- /build/Dockerfile.connect-controller: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # Build the connect-controller 5 | FROM golang:1.23.6 AS builder 6 | ARG TARGETOS 7 | ARG TARGETARCH 8 | ENV CGO_ENABLED=0 9 | ENV GO111MODULE=on 10 | 11 | WORKDIR /workspace 12 | # Copy the Go Modules manifests 13 | COPY go.mod go.mod 14 | COPY go.sum go.sum 15 | 16 | # Copy the go source 17 | COPY cmd/ cmd 18 | COPY api/ api/ 19 | COPY internal/ internal/ 20 | COPY vendor/ vendor/ 21 | COPY Makefile Makefile 22 | COPY VERSION VERSION 23 | 24 | # Build the connect-controller binary 25 | RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} make build-controller 26 | 27 | # Use distroless as minimal base image to package the connect-controller binary 28 | FROM gcr.io/distroless/static:nonroot 29 | USER nonroot 30 | 31 | ARG org_oci_version=unknown 32 | ARG org_oci_source=unknown 33 | ARG org_oci_revision=unknown 34 | ARG org_oci_created=unknown 35 | 36 | LABEL org.opencontainers.image.version=$org_oci_version \ 37 | org.opencontainers.image.source=$org_oci_source \ 38 | org.opencontainers.image.revision=$org_oci_revision \ 39 | org.opencontainers.image.created=$org_oci_created 40 | 41 | WORKDIR / 42 | COPY --from=builder /workspace/bin/connect-controller . 43 | USER 65532:65532 -------------------------------------------------------------------------------- /build/Dockerfile.connect-gateway: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # Build the connect-gateway 5 | FROM golang:1.23.6 AS builder 6 | ARG TARGETOS 7 | ARG TARGETARCH 8 | ENV CGO_ENABLED=0 9 | ENV GO111MODULE=on 10 | 11 | WORKDIR /workspace 12 | # Copy the Go Modules manifests 13 | COPY go.mod go.mod 14 | COPY go.sum go.sum 15 | 16 | # Copy the go source 17 | COPY cmd/ cmd 18 | COPY api/ api/ 19 | COPY internal/ internal/ 20 | COPY vendor/ vendor/ 21 | COPY Makefile Makefile 22 | COPY VERSION VERSION 23 | 24 | # Build the connect-gateway binary 25 | RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} make build-gateway 26 | 27 | # Use distroless as minimal base image to package the connect-gateway binary 28 | FROM gcr.io/distroless/static:nonroot 29 | USER nonroot 30 | 31 | ARG org_oci_version=unknown 32 | ARG org_oci_source=unknown 33 | ARG org_oci_revision=unknown 34 | ARG org_oci_created=unknown 35 | 36 | LABEL org.opencontainers.image.version=$org_oci_version \ 37 | org.opencontainers.image.source=$org_oci_source \ 38 | org.opencontainers.image.revision=$org_oci_revision \ 39 | org.opencontainers.image.created=$org_oci_created 40 | 41 | WORKDIR / 42 | COPY --from=builder /workspace/bin/connect-gateway . 43 | COPY logging.yaml . 44 | USER 65532:65532 45 | -------------------------------------------------------------------------------- /cmd/connect-agent/main.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "context" 8 | "flag" 9 | "log" 10 | "os" 11 | "os/signal" 12 | 13 | "go.uber.org/zap" 14 | 15 | "github.com/open-edge-platform/cluster-connect-gateway/internal/agent" 16 | "github.com/sirupsen/logrus" 17 | ) 18 | 19 | func main() { 20 | var gatewayUrl, tunnelId, logLevel, tokenPath, authToken, tunnelAuthMode string 21 | var insecureSkipVerify bool 22 | flag.StringVar(&gatewayUrl, "gateway-url", "", "The URL of the gateway") 23 | // TODO: set this to false by default once CA mount is implemented 24 | flag.BoolVar(&insecureSkipVerify, "insecure-skip-verify", true, "Skip TLS verification") 25 | flag.StringVar(&tunnelAuthMode, "tunnel-auth-mode", "token", "Specify the authentication mode for tunnel connections: 'token' or 'jwt'") 26 | flag.StringVar(&tunnelId, "tunnel-id", "", "The tunnel ID") 27 | flag.StringVar(&authToken, "auth-token", "", "The authentication token") 28 | flag.StringVar(&logLevel, "log-level", "info", "Log levels: info, debug, trace") 29 | flag.StringVar(&tokenPath, "token-path", "./access_token", "path to jwt token") 30 | flag.Parse() 31 | 32 | // Set log level for the tunnel data 33 | if level, err := logrus.ParseLevel(logLevel); err == nil { 34 | logrus.SetLevel(level) 35 | } else { 36 | log.Fatalf("can't initialize logrus logger: %v", err) 37 | } 38 | 39 | logger, err := zap.NewProduction(zap.Fields(zap.String("tunnel-id", tunnelId))) 40 | if logLevel == "debug" || logLevel == "trace" { 41 | logger, err = zap.NewDevelopment(zap.Fields(zap.String("tunnel-id", tunnelId))) 42 | } 43 | if err != nil { 44 | log.Fatalf("can't initialize zap logger: %v", err) 45 | } 46 | zap.ReplaceGlobals(logger) 47 | 48 | // Required parameters 49 | if gatewayUrl == "" || tunnelId == "" { 50 | logger.Error("gateway-url and tunnel-id are required") 51 | os.Exit(1) 52 | } 53 | 54 | ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) 55 | defer func() { 56 | logger.Info("Received interrupt signal, shutting down") 57 | stop() 58 | }() 59 | 60 | agent := &agent.ConnectAgent{ 61 | GatewayUrl: gatewayUrl, 62 | InsecureSkipVerify: insecureSkipVerify, 63 | TunnelId: tunnelId, 64 | TokenPath: tokenPath, 65 | TunnelAuthMode: tunnelAuthMode, 66 | AuthToken: authToken, 67 | } 68 | 69 | agent.Run(ctx) 70 | } 71 | -------------------------------------------------------------------------------- /cmd/connect-gateway/main.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "context" 8 | "flag" 9 | "fmt" 10 | "net/http" 11 | "os" 12 | "os/signal" 13 | "time" 14 | 15 | "github.com/atomix/dazl" 16 | _ "github.com/atomix/dazl/zap" 17 | "github.com/sirupsen/logrus" 18 | 19 | "github.com/open-edge-platform/cluster-connect-gateway/internal/auth" 20 | "github.com/open-edge-platform/cluster-connect-gateway/internal/server" 21 | orchlibraryauth "github.com/open-edge-platform/orch-library/go/pkg/auth" 22 | ) 23 | 24 | var ( 25 | log = dazl.GetPackageLogger() 26 | ) 27 | 28 | func main() { 29 | var gatewayAddress, logLevel, opaAddress, oidcIssuerURL, externalHost, tunnelAuthMode string 30 | var gatewayPort, opaPort int 31 | var enableAuth, enableMetrics, oidcInsecureSkipVerify bool 32 | var connectionProbeInterval time.Duration 33 | flag.StringVar(&gatewayAddress, "address", "0.0.0.0", "Address to listen on for edge connection gateway") 34 | flag.IntVar(&gatewayPort, "port", 8080, "Port to listen on for edge connection gateway") 35 | flag.BoolVar(&enableAuth, "enable-auth", false, "Enable OIDC authentication") 36 | flag.BoolVar(&enableMetrics, "enable-metrics", false, "Enable metrics") 37 | flag.StringVar(&logLevel, "log-level", "info", "Log levels: info, debug, trace, warn") 38 | flag.StringVar(&oidcIssuerURL, "oidc-issuer-url", "", "OIDC Issuer URL") 39 | flag.BoolVar(&oidcInsecureSkipVerify, "oidc-insecure-skip-verify", false, "OIDC Insecure Skip Verify") 40 | flag.StringVar(&externalHost, "external-host", "", "External host for the gateway") 41 | 42 | flag.StringVar(&opaAddress, "opa-address", "http://localhost", "Address to opa") 43 | flag.IntVar(&opaPort, "opa-port", 8181, "Port to opa") 44 | flag.StringVar(&tunnelAuthMode, "tunnel-auth-mode", "token", "Specify the authentication mode for tunnel connections: 'token' or 'jwt'") 45 | flag.DurationVar(&connectionProbeInterval, "connection-probe-interval", 1*time.Minute, "Interval for connection probe checks") 46 | flag.Parse() 47 | 48 | setLogLevel(logLevel) 49 | log.Infof("Agent authentication mode for tunnel connections %s", tunnelAuthMode) 50 | var tunnelAuth func(req *http.Request) (clientKey string, authed bool, err error) 51 | switch tunnelAuthMode { 52 | case "token": 53 | tokenManager, err := auth.NewTokenManager() 54 | if err != nil { 55 | log.Fatal(err) 56 | } 57 | secretTokenAuth := auth.SecretTokenAuthorizer{TokenManager: tokenManager} 58 | tunnelAuth = secretTokenAuth.Authorizer 59 | case "jwt": 60 | jwtAuth := auth.JwtTokenAuthorizer{JwtAuth: &orchlibraryauth.JwtAuthenticator{}} 61 | tunnelAuth = jwtAuth.Authorizer 62 | } 63 | 64 | listenAddr := fmt.Sprintf("%s:%d", gatewayAddress, gatewayPort) 65 | // TODO: make the # of hours configurable via helm chart. Will use minutes so we can trigger it quick if needed 66 | // hours seems too much 67 | clientCleanupTicker := time.NewTicker(480 * time.Minute) 68 | defer clientCleanupTicker.Stop() 69 | 70 | connectionProbeTicker := time.NewTicker(connectionProbeInterval) 71 | defer connectionProbeTicker.Stop() 72 | 73 | server, err := server.NewServer( 74 | server.WithListenAddr(listenAddr), 75 | server.WithAuth(enableAuth, opaAddress, opaPort), 76 | server.WithAuthorizer(tunnelAuth, enableMetrics), 77 | server.WithExternalHost(externalHost), 78 | server.WithOIDCIssuerURL(oidcIssuerURL), 79 | server.WithOIDCInsecureSkipVerify(oidcInsecureSkipVerify), 80 | server.WithCleanupTicker(clientCleanupTicker), 81 | server.WithConnectionProbeTicker(connectionProbeTicker), 82 | ) 83 | if err != nil { 84 | log.Fatalf("Failed to create gateway server: %v", err) 85 | os.Exit(1) 86 | } 87 | 88 | c := make(chan os.Signal, 1) 89 | signal.Notify(c, os.Interrupt) 90 | 91 | // Create an error channel 92 | errChan := make(chan error, 1) 93 | ctx, cancel := context.WithCancel(context.Background()) 94 | defer cancel() 95 | 96 | log.Infof("Starting edge connection gateway server on %s", listenAddr) 97 | log.Infof("Connection probe interval set to %s", connectionProbeInterval) 98 | go runServer(ctx, server, errChan) 99 | 100 | // Wait for either an error or an OS signal 101 | select { 102 | case err := <-errChan: 103 | // Handle the error 104 | log.Errorf("Error encountered: %s", err) 105 | case sig := <-c: 106 | // Handle the signal 107 | log.Infof("Got %s signal. Aborting...", sig) 108 | } 109 | } 110 | 111 | func setLogLevel(logLevel string) { 112 | var level dazl.Level 113 | switch logLevel { 114 | case "debug": 115 | level = dazl.DebugLevel 116 | logrus.SetLevel(logrus.DebugLevel) 117 | case "info": 118 | level = dazl.InfoLevel 119 | case "warn": 120 | level = dazl.WarnLevel 121 | default: 122 | level = dazl.InfoLevel 123 | log.Warnf("Unknown log level '%s', defaulting to 'info'", logLevel) 124 | } 125 | dazl.GetRootLogger().SetLevel(level) 126 | } 127 | 128 | func runServer(ctx context.Context, server *server.Server, errChan chan error) { 129 | select { 130 | case <-ctx.Done(): 131 | // Handle the context being canceled 132 | log.Info("Context canceled") 133 | default: 134 | // Catch any error from Run and send it to the error channel 135 | if err := server.Run(); err != nil { 136 | errChan <- err 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # This kustomization.yaml is not intended to be run by itself, 2 | # since it depends on service name and namespace that are out of this kustomize package. 3 | # It should be run by config/default 4 | resources: 5 | - bases/cluster.edge-orchestrator.intel.com_clusterconnects.yaml 6 | # +kubebuilder:scaffold:crdkustomizeresource 7 | 8 | patches: 9 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 10 | # patches here are for enabling the conversion webhook for each CRD 11 | # +kubebuilder:scaffold:crdkustomizewebhookpatch 12 | 13 | # [WEBHOOK] To enable webhook, uncomment the following section 14 | # the following config is for teaching kustomize how to do kustomization for CRDs. 15 | #configurations: 16 | #- kustomizeconfig.yaml 17 | -------------------------------------------------------------------------------- /config/crd/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This file is for teaching kustomize how to substitute name and namespace reference in CRD 2 | nameReference: 3 | - kind: Service 4 | version: v1 5 | fieldSpecs: 6 | - kind: CustomResourceDefinition 7 | version: v1 8 | group: apiextensions.k8s.io 9 | path: spec/conversion/webhook/clientConfig/service/name 10 | 11 | namespace: 12 | - kind: CustomResourceDefinition 13 | version: v1 14 | group: apiextensions.k8s.io 15 | path: spec/conversion/webhook/clientConfig/service/namespace 16 | create: false 17 | 18 | varReference: 19 | - path: metadata/annotations 20 | -------------------------------------------------------------------------------- /config/default/cert_metrics_manager_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch adds the args, volumes, and ports to allow the manager to use the metrics-server certs. 2 | 3 | # Add the volumeMount for the metrics-server certs 4 | - op: add 5 | path: /spec/template/spec/containers/0/volumeMounts/- 6 | value: 7 | mountPath: /tmp/k8s-metrics-server/metrics-certs 8 | name: metrics-certs 9 | readOnly: true 10 | 11 | # Add the --metrics-cert-path argument for the metrics server 12 | - op: add 13 | path: /spec/template/spec/containers/0/args/- 14 | value: --metrics-cert-path=/tmp/k8s-metrics-server/metrics-certs 15 | 16 | # Add the metrics-server certs volume configuration 17 | - op: add 18 | path: /spec/template/spec/volumes/- 19 | value: 20 | name: metrics-certs 21 | secret: 22 | secretName: metrics-server-cert 23 | optional: false 24 | items: 25 | - key: ca.crt 26 | path: ca.crt 27 | - key: tls.crt 28 | path: tls.crt 29 | - key: tls.key 30 | path: tls.key 31 | -------------------------------------------------------------------------------- /config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Adds namespace to all resources. 2 | namespace: cluster-connect-gateway-system 3 | 4 | # Value of this field is prepended to the 5 | # names of all resources, e.g. a deployment named 6 | # "wordpress" becomes "alices-wordpress". 7 | # Note that it should also match with the prefix (text before '-') of the namespace 8 | # field above. 9 | namePrefix: cluster-connect-gateway- 10 | 11 | # Labels to add to all resources and selectors. 12 | #labels: 13 | #- includeSelectors: true 14 | # pairs: 15 | # someName: someValue 16 | 17 | resources: 18 | - ../crd 19 | - ../rbac 20 | - ../manager 21 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 22 | # crd/kustomization.yaml 23 | #- ../webhook 24 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. 25 | #- ../certmanager 26 | # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. 27 | #- ../prometheus 28 | # [METRICS] Expose the controller manager metrics service. 29 | - metrics_service.yaml 30 | # [NETWORK POLICY] Protect the /metrics endpoint and Webhook Server with NetworkPolicy. 31 | # Only Pod(s) running a namespace labeled with 'metrics: enabled' will be able to gather the metrics. 32 | # Only CR(s) which requires webhooks and are applied on namespaces labeled with 'webhooks: enabled' will 33 | # be able to communicate with the Webhook Server. 34 | #- ../network-policy 35 | 36 | # Uncomment the patches line if you enable Metrics 37 | patches: 38 | # [METRICS] The following patch will enable the metrics endpoint using HTTPS and the port :8443. 39 | # More info: https://book.kubebuilder.io/reference/metrics 40 | - path: manager_metrics_patch.yaml 41 | target: 42 | kind: Deployment 43 | 44 | # Uncomment the patches line if you enable Metrics and CertManager 45 | # [METRICS-WITH-CERTS] To enable metrics protected with certManager, uncomment the following line. 46 | # This patch will protect the metrics with certManager self-signed certs. 47 | #- path: cert_metrics_manager_patch.yaml 48 | # target: 49 | # kind: Deployment 50 | 51 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 52 | # crd/kustomization.yaml 53 | #- path: manager_webhook_patch.yaml 54 | # target: 55 | # kind: Deployment 56 | 57 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. 58 | # Uncomment the following replacements to add the cert-manager CA injection annotations 59 | #replacements: 60 | # - source: # Uncomment the following block to enable certificates for metrics 61 | # kind: Service 62 | # version: v1 63 | # name: controller-manager-metrics-service 64 | # fieldPath: metadata.name 65 | # targets: 66 | # - select: 67 | # kind: Certificate 68 | # group: cert-manager.io 69 | # version: v1 70 | # name: metrics-certs 71 | # fieldPaths: 72 | # - spec.dnsNames.0 73 | # - spec.dnsNames.1 74 | # options: 75 | # delimiter: '.' 76 | # index: 0 77 | # create: true 78 | # 79 | # - source: 80 | # kind: Service 81 | # version: v1 82 | # name: controller-manager-metrics-service 83 | # fieldPath: metadata.namespace 84 | # targets: 85 | # - select: 86 | # kind: Certificate 87 | # group: cert-manager.io 88 | # version: v1 89 | # name: metrics-certs 90 | # fieldPaths: 91 | # - spec.dnsNames.0 92 | # - spec.dnsNames.1 93 | # options: 94 | # delimiter: '.' 95 | # index: 1 96 | # create: true 97 | # 98 | # - source: # Uncomment the following block if you have any webhook 99 | # kind: Service 100 | # version: v1 101 | # name: webhook-service 102 | # fieldPath: .metadata.name # Name of the service 103 | # targets: 104 | # - select: 105 | # kind: Certificate 106 | # group: cert-manager.io 107 | # version: v1 108 | # name: serving-cert 109 | # fieldPaths: 110 | # - .spec.dnsNames.0 111 | # - .spec.dnsNames.1 112 | # options: 113 | # delimiter: '.' 114 | # index: 0 115 | # create: true 116 | # - source: 117 | # kind: Service 118 | # version: v1 119 | # name: webhook-service 120 | # fieldPath: .metadata.namespace # Namespace of the service 121 | # targets: 122 | # - select: 123 | # kind: Certificate 124 | # group: cert-manager.io 125 | # version: v1 126 | # name: serving-cert 127 | # fieldPaths: 128 | # - .spec.dnsNames.0 129 | # - .spec.dnsNames.1 130 | # options: 131 | # delimiter: '.' 132 | # index: 1 133 | # create: true 134 | # 135 | # - source: # Uncomment the following block if you have a ValidatingWebhook (--programmatic-validation) 136 | # kind: Certificate 137 | # group: cert-manager.io 138 | # version: v1 139 | # name: serving-cert # This name should match the one in certificate.yaml 140 | # fieldPath: .metadata.namespace # Namespace of the certificate CR 141 | # targets: 142 | # - select: 143 | # kind: ValidatingWebhookConfiguration 144 | # fieldPaths: 145 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 146 | # options: 147 | # delimiter: '/' 148 | # index: 0 149 | # create: true 150 | # - source: 151 | # kind: Certificate 152 | # group: cert-manager.io 153 | # version: v1 154 | # name: serving-cert 155 | # fieldPath: .metadata.name 156 | # targets: 157 | # - select: 158 | # kind: ValidatingWebhookConfiguration 159 | # fieldPaths: 160 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 161 | # options: 162 | # delimiter: '/' 163 | # index: 1 164 | # create: true 165 | # 166 | # - source: # Uncomment the following block if you have a DefaultingWebhook (--defaulting ) 167 | # kind: Certificate 168 | # group: cert-manager.io 169 | # version: v1 170 | # name: serving-cert 171 | # fieldPath: .metadata.namespace # Namespace of the certificate CR 172 | # targets: 173 | # - select: 174 | # kind: MutatingWebhookConfiguration 175 | # fieldPaths: 176 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 177 | # options: 178 | # delimiter: '/' 179 | # index: 0 180 | # create: true 181 | # - source: 182 | # kind: Certificate 183 | # group: cert-manager.io 184 | # version: v1 185 | # name: serving-cert 186 | # fieldPath: .metadata.name 187 | # targets: 188 | # - select: 189 | # kind: MutatingWebhookConfiguration 190 | # fieldPaths: 191 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 192 | # options: 193 | # delimiter: '/' 194 | # index: 1 195 | # create: true 196 | # 197 | # - source: # Uncomment the following block if you have a ConversionWebhook (--conversion) 198 | # kind: Certificate 199 | # group: cert-manager.io 200 | # version: v1 201 | # name: serving-cert 202 | # fieldPath: .metadata.namespace # Namespace of the certificate CR 203 | # targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. 204 | # +kubebuilder:scaffold:crdkustomizecainjectionns 205 | # - source: 206 | # kind: Certificate 207 | # group: cert-manager.io 208 | # version: v1 209 | # name: serving-cert 210 | # fieldPath: .metadata.name 211 | # targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. 212 | # +kubebuilder:scaffold:crdkustomizecainjectionname 213 | -------------------------------------------------------------------------------- /config/default/manager_metrics_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch adds the args to allow exposing the metrics endpoint using HTTPS 2 | - op: add 3 | path: /spec/template/spec/containers/0/args/0 4 | value: --metrics-bind-address=:8443 5 | -------------------------------------------------------------------------------- /config/default/metrics_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | app.kubernetes.io/name: cluster-connect-gateway 7 | app.kubernetes.io/managed-by: kustomize 8 | name: controller-manager-metrics-service 9 | namespace: system 10 | spec: 11 | ports: 12 | - name: https 13 | port: 8443 14 | protocol: TCP 15 | targetPort: 8443 16 | selector: 17 | control-plane: controller-manager 18 | app.kubernetes.io/name: cluster-connect-gateway 19 | -------------------------------------------------------------------------------- /config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manager.yaml 3 | apiVersion: kustomize.config.k8s.io/v1beta1 4 | kind: Kustomization 5 | images: 6 | - name: controller 7 | -------------------------------------------------------------------------------- /config/manager/manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | app.kubernetes.io/name: cluster-connect-gateway 7 | app.kubernetes.io/managed-by: kustomize 8 | name: system 9 | --- 10 | apiVersion: apps/v1 11 | kind: Deployment 12 | metadata: 13 | name: controller-manager 14 | namespace: system 15 | labels: 16 | control-plane: controller-manager 17 | app.kubernetes.io/name: cluster-connect-gateway 18 | app.kubernetes.io/managed-by: kustomize 19 | spec: 20 | selector: 21 | matchLabels: 22 | control-plane: controller-manager 23 | app.kubernetes.io/name: cluster-connect-gateway 24 | replicas: 1 25 | template: 26 | metadata: 27 | annotations: 28 | kubectl.kubernetes.io/default-container: manager 29 | labels: 30 | control-plane: controller-manager 31 | app.kubernetes.io/name: cluster-connect-gateway 32 | spec: 33 | # TODO(user): Uncomment the following code to configure the nodeAffinity expression 34 | # according to the platforms which are supported by your solution. 35 | # It is considered best practice to support multiple architectures. You can 36 | # build your manager image using the makefile target docker-buildx. 37 | # affinity: 38 | # nodeAffinity: 39 | # requiredDuringSchedulingIgnoredDuringExecution: 40 | # nodeSelectorTerms: 41 | # - matchExpressions: 42 | # - key: kubernetes.io/arch 43 | # operator: In 44 | # values: 45 | # - amd64 46 | # - arm64 47 | # - ppc64le 48 | # - s390x 49 | # - key: kubernetes.io/os 50 | # operator: In 51 | # values: 52 | # - linux 53 | securityContext: 54 | # Projects are configured by default to adhere to the "restricted" Pod Security Standards. 55 | # This ensures that deployments meet the highest security requirements for Kubernetes. 56 | # For more details, see: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted 57 | runAsNonRoot: true 58 | seccompProfile: 59 | type: RuntimeDefault 60 | containers: 61 | - command: 62 | - /manager 63 | args: 64 | - --leader-elect 65 | - --health-probe-bind-address=:8081 66 | image: controller:latest 67 | name: manager 68 | ports: [] 69 | securityContext: 70 | allowPrivilegeEscalation: false 71 | capabilities: 72 | drop: 73 | - "ALL" 74 | livenessProbe: 75 | httpGet: 76 | path: /healthz 77 | port: 8081 78 | initialDelaySeconds: 15 79 | periodSeconds: 20 80 | readinessProbe: 81 | httpGet: 82 | path: /readyz 83 | port: 8081 84 | initialDelaySeconds: 5 85 | periodSeconds: 10 86 | # TODO(user): Configure the resources accordingly based on the project requirements. 87 | # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ 88 | resources: 89 | limits: 90 | cpu: 500m 91 | memory: 128Mi 92 | requests: 93 | cpu: 10m 94 | memory: 64Mi 95 | volumeMounts: [] 96 | volumes: [] 97 | serviceAccountName: controller-manager 98 | terminationGracePeriodSeconds: 10 99 | -------------------------------------------------------------------------------- /config/network-policy/allow-metrics-traffic.yaml: -------------------------------------------------------------------------------- 1 | # This NetworkPolicy allows ingress traffic 2 | # with Pods running on namespaces labeled with 'metrics: enabled'. Only Pods on those 3 | # namespaces are able to gather data from the metrics endpoint. 4 | apiVersion: networking.k8s.io/v1 5 | kind: NetworkPolicy 6 | metadata: 7 | labels: 8 | app.kubernetes.io/name: cluster-connect-gateway 9 | app.kubernetes.io/managed-by: kustomize 10 | name: allow-metrics-traffic 11 | namespace: system 12 | spec: 13 | podSelector: 14 | matchLabels: 15 | control-plane: controller-manager 16 | app.kubernetes.io/name: cluster-connect-gateway 17 | policyTypes: 18 | - Ingress 19 | ingress: 20 | # This allows ingress traffic from any namespace with the label metrics: enabled 21 | - from: 22 | - namespaceSelector: 23 | matchLabels: 24 | metrics: enabled # Only from namespaces with this label 25 | ports: 26 | - port: 8443 27 | protocol: TCP 28 | -------------------------------------------------------------------------------- /config/network-policy/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - allow-metrics-traffic.yaml 3 | -------------------------------------------------------------------------------- /config/prometheus/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - monitor.yaml 3 | 4 | # [PROMETHEUS-WITH-CERTS] The following patch configures the ServiceMonitor in ../prometheus 5 | # to securely reference certificates created and managed by cert-manager. 6 | # Additionally, ensure that you uncomment the [METRICS WITH CERTMANAGER] patch under config/default/kustomization.yaml 7 | # to mount the "metrics-server-cert" secret in the Manager Deployment. 8 | #patches: 9 | # - path: monitor_tls_patch.yaml 10 | # target: 11 | # kind: ServiceMonitor 12 | -------------------------------------------------------------------------------- /config/prometheus/monitor.yaml: -------------------------------------------------------------------------------- 1 | # Prometheus Monitor Service (Metrics) 2 | apiVersion: monitoring.coreos.com/v1 3 | kind: ServiceMonitor 4 | metadata: 5 | labels: 6 | control-plane: controller-manager 7 | app.kubernetes.io/name: cluster-connect-gateway 8 | app.kubernetes.io/managed-by: kustomize 9 | name: controller-manager-metrics-monitor 10 | namespace: system 11 | spec: 12 | endpoints: 13 | - path: /metrics 14 | port: https # Ensure this is the name of the port that exposes HTTPS metrics 15 | scheme: https 16 | bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token 17 | tlsConfig: 18 | # TODO(user): The option insecureSkipVerify: true is not recommended for production since it disables 19 | # certificate verification, exposing the system to potential man-in-the-middle attacks. 20 | # For production environments, it is recommended to use cert-manager for automatic TLS certificate management. 21 | # To apply this configuration, enable cert-manager and use the patch located at config/prometheus/servicemonitor_tls_patch.yaml, 22 | # which securely references the certificate from the 'metrics-server-cert' secret. 23 | insecureSkipVerify: true 24 | selector: 25 | matchLabels: 26 | control-plane: controller-manager 27 | app.kubernetes.io/name: cluster-connect-gateway 28 | -------------------------------------------------------------------------------- /config/prometheus/monitor_tls_patch.yaml: -------------------------------------------------------------------------------- 1 | # Patch for Prometheus ServiceMonitor to enable secure TLS configuration 2 | # using certificates managed by cert-manager 3 | apiVersion: monitoring.coreos.com/v1 4 | kind: ServiceMonitor 5 | metadata: 6 | name: controller-manager-metrics-monitor 7 | namespace: system 8 | spec: 9 | endpoints: 10 | - tlsConfig: 11 | insecureSkipVerify: false 12 | ca: 13 | secret: 14 | name: metrics-server-cert 15 | key: ca.crt 16 | cert: 17 | secret: 18 | name: metrics-server-cert 19 | key: tls.crt 20 | keySecret: 21 | name: metrics-server-cert 22 | key: tls.key 23 | -------------------------------------------------------------------------------- /config/rbac/clusterconnect_admin_role.yaml: -------------------------------------------------------------------------------- 1 | # This rule is not used by the project cluster-connect-gateway itself. 2 | # It is provided to allow the cluster admin to help manage permissions for users. 3 | # 4 | # Grants full permissions ('*') over cluster.edge-orchestrator.intel.com. 5 | # This role is intended for users authorized to modify roles and bindings within the cluster, 6 | # enabling them to delegate specific permissions to other users or groups as needed. 7 | 8 | apiVersion: rbac.authorization.k8s.io/v1 9 | kind: ClusterRole 10 | metadata: 11 | labels: 12 | app.kubernetes.io/name: cluster-connect-gateway 13 | app.kubernetes.io/managed-by: kustomize 14 | name: clusterconnection-admin-role 15 | rules: 16 | - apiGroups: 17 | - cluster.edge-orchestrator.intel.com 18 | resources: 19 | - clusterconnects 20 | verbs: 21 | - '*' 22 | - apiGroups: 23 | - cluster.edge-orchestrator.intel.com 24 | resources: 25 | - clusterconnects/status 26 | verbs: 27 | - get 28 | -------------------------------------------------------------------------------- /config/rbac/clusterconnect_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # This rule is not used by the project cluster-connect-gateway itself. 2 | # It is provided to allow the cluster admin to help manage permissions for users. 3 | # 4 | # Grants permissions to create, update, and delete resources within the cluster.edge-orchestrator.intel.com. 5 | # This role is intended for users who need to manage these resources 6 | # but should not control RBAC or manage permissions for others. 7 | 8 | apiVersion: rbac.authorization.k8s.io/v1 9 | kind: ClusterRole 10 | metadata: 11 | labels: 12 | app.kubernetes.io/name: cluster-connect-gateway 13 | app.kubernetes.io/managed-by: kustomize 14 | name: clusterconnect-editor-role 15 | rules: 16 | - apiGroups: 17 | - cluster.edge-orchestrator.intel.com 18 | resources: 19 | - clusterconnects 20 | verbs: 21 | - create 22 | - delete 23 | - get 24 | - list 25 | - patch 26 | - update 27 | - watch 28 | - apiGroups: 29 | - cluster.edge-orchestrator.intel.com 30 | resources: 31 | - clusterconnects/status 32 | verbs: 33 | - get 34 | -------------------------------------------------------------------------------- /config/rbac/clusterconnect_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # This rule is not used by the project cluster-connect-gateway itself. 2 | # It is provided to allow the cluster admin to help manage permissions for users. 3 | # 4 | # Grants read-only access to cluster.edge-orchestrator.intel.com resources. 5 | # This role is intended for users who need visibility into these resources 6 | # without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing. 7 | 8 | apiVersion: rbac.authorization.k8s.io/v1 9 | kind: ClusterRole 10 | metadata: 11 | labels: 12 | app.kubernetes.io/name: cluster-connect-gateway 13 | app.kubernetes.io/managed-by: kustomize 14 | name: clusterconnection-viewer-role 15 | rules: 16 | - apiGroups: 17 | - cluster.edge-orchestrator.intel.com 18 | resources: 19 | - clusterconnects 20 | verbs: 21 | - get 22 | - list 23 | - watch 24 | - apiGroups: 25 | - cluster.edge-orchestrator.intel.com 26 | resources: 27 | - clusterconnects/status 28 | verbs: 29 | - get 30 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | # All RBAC will be applied under this service account in 3 | # the deployment namespace. You may comment out this resource 4 | # if your manager will use a service account that exists at 5 | # runtime. Be sure to update RoleBinding and ClusterRoleBinding 6 | # subjects if changing service account names. 7 | - service_account.yaml 8 | - role.yaml 9 | - role_binding.yaml 10 | - leader_election_role.yaml 11 | - leader_election_role_binding.yaml 12 | # The following RBAC configurations are used to protect 13 | # the metrics endpoint with authn/authz. These configurations 14 | # ensure that only authorized users and service accounts 15 | # can access the metrics endpoint. Comment the following 16 | # permissions if you want to disable this protection. 17 | # More info: https://book.kubebuilder.io/reference/metrics.html 18 | - metrics_auth_role.yaml 19 | - metrics_auth_role_binding.yaml 20 | - metrics_reader_role.yaml 21 | # For each CRD, "Admin", "Editor" and "Viewer" roles are scaffolded by 22 | # default, aiding admins in cluster management. Those roles are 23 | # not used by the {{ .ProjectName }} itself. You can comment the following lines 24 | # if you do not want those helpers be installed with your Project. 25 | - clusterconnect_admin_role.yaml 26 | - clusterconnect_editor_role.yaml 27 | - clusterconnect_viewer_role.yaml 28 | 29 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: cluster-connect-gateway 7 | app.kubernetes.io/managed-by: kustomize 8 | name: leader-election-role 9 | rules: 10 | - apiGroups: 11 | - "" 12 | resources: 13 | - configmaps 14 | verbs: 15 | - get 16 | - list 17 | - watch 18 | - create 19 | - update 20 | - patch 21 | - delete 22 | - apiGroups: 23 | - coordination.k8s.io 24 | resources: 25 | - leases 26 | verbs: 27 | - get 28 | - list 29 | - watch 30 | - create 31 | - update 32 | - patch 33 | - delete 34 | - apiGroups: 35 | - "" 36 | resources: 37 | - events 38 | verbs: 39 | - create 40 | - patch 41 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: cluster-connect-gateway 6 | app.kubernetes.io/managed-by: kustomize 7 | name: leader-election-rolebinding 8 | roleRef: 9 | apiGroup: rbac.authorization.k8s.io 10 | kind: Role 11 | name: leader-election-role 12 | subjects: 13 | - kind: ServiceAccount 14 | name: controller-manager 15 | namespace: system 16 | -------------------------------------------------------------------------------- /config/rbac/metrics_auth_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: metrics-auth-role 5 | rules: 6 | - apiGroups: 7 | - authentication.k8s.io 8 | resources: 9 | - tokenreviews 10 | verbs: 11 | - create 12 | - apiGroups: 13 | - authorization.k8s.io 14 | resources: 15 | - subjectaccessreviews 16 | verbs: 17 | - create 18 | -------------------------------------------------------------------------------- /config/rbac/metrics_auth_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: metrics-auth-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: metrics-auth-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/metrics_reader_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: metrics-reader 5 | rules: 6 | - nonResourceURLs: 7 | - "/metrics" 8 | verbs: 9 | - get 10 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: manager-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - secrets 11 | verbs: 12 | - create 13 | - get 14 | - list 15 | - patch 16 | - update 17 | - watch 18 | - apiGroups: 19 | - apiextensions.k8s.io 20 | resources: 21 | - customresourcedefinitions 22 | verbs: 23 | - get 24 | - list 25 | - watch 26 | - apiGroups: 27 | - cluster.edge-orchestrator.intel.com 28 | resources: 29 | - clusterconnects 30 | verbs: 31 | - create 32 | - delete 33 | - get 34 | - list 35 | - patch 36 | - update 37 | - watch 38 | - apiGroups: 39 | - cluster.edge-orchestrator.intel.com 40 | resources: 41 | - clusterconnects/finalizers 42 | verbs: 43 | - update 44 | - apiGroups: 45 | - cluster.edge-orchestrator.intel.com 46 | resources: 47 | - clusterconnects/status 48 | verbs: 49 | - get 50 | - patch 51 | - update 52 | - apiGroups: 53 | - cluster.x-k8s.io 54 | resources: 55 | - clusters 56 | - clusters/status 57 | verbs: 58 | - get 59 | - list 60 | - patch 61 | - update 62 | - watch 63 | -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: cluster-connect-gateway 6 | app.kubernetes.io/managed-by: kustomize 7 | name: manager-rolebinding 8 | roleRef: 9 | apiGroup: rbac.authorization.k8s.io 10 | kind: ClusterRole 11 | name: manager-role 12 | subjects: 13 | - kind: ServiceAccount 14 | name: controller-manager 15 | namespace: system 16 | -------------------------------------------------------------------------------- /config/rbac/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: cluster-connect-gateway 6 | app.kubernetes.io/managed-by: kustomize 7 | name: controller-manager 8 | namespace: system 9 | -------------------------------------------------------------------------------- /config/samples/cluster_v1alpha1_clusterconnect.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: cluster.edge-orchestrator.intel.com/v1alpha1 2 | kind: ClusterConnect 3 | metadata: 4 | name: sample 5 | spec: 6 | # TODO(user): Add fields here 7 | -------------------------------------------------------------------------------- /config/samples/cluster_v1alpha1_clusterconnect_with_controlplane.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: cluster.edge-orchestrator.intel.com/v1alpha1 2 | kind: ClusterConnect 3 | metadata: 4 | name: sample-with-control-plane-ref 5 | spec: 6 | controlPlaneRef: 7 | apiVersion: controlplane.cluster.x-k8s.io/v1beta1 8 | kind: RKE2ControlPlane 9 | name: sample-control-plane 10 | namespace: default 11 | 12 | -------------------------------------------------------------------------------- /config/samples/rke2_v1beta1_control_plane.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: controlplane.cluster.x-k8s.io/v1beta1 2 | kind: RKE2ControlPlane 3 | metadata: 4 | name: sample-control-plane 5 | spec: 6 | replicas: 1 7 | version: v1.30.3+rke2r1 8 | serverConfig: 9 | cni: calico 10 | rolloutStrategy: 11 | type: "RollingUpdate" 12 | rollingUpdate: 13 | maxSurge: 1 -------------------------------------------------------------------------------- /deployment/charts/cluster-connect-gateway-crd/.helmignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | # Patterns to ignore when building packages. 6 | # This supports shell glob matching, relative path matching, and 7 | # negation (prefixed with !). Only one pattern per line. 8 | .DS_Store 9 | # Common VCS dirs 10 | .git/ 11 | .gitignore 12 | .bzr/ 13 | .bzrignore 14 | .hg/ 15 | .hgignore 16 | .svn/ 17 | # Common backup files 18 | *.swp 19 | *.bak 20 | *.tmp 21 | *.orig 22 | *~ 23 | # Various IDEs 24 | .project 25 | .idea/ 26 | *.tmproj 27 | .vscode/ 28 | -------------------------------------------------------------------------------- /deployment/charts/cluster-connect-gateway-crd/Chart.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | --- 5 | apiVersion: v2 6 | name: cluster-connect-gateway-crd 7 | description: A Helm chart for Cluster Connect Gateway CRDs 8 | type: application 9 | version: 1.2.0 10 | annotations: 11 | revision: 2db568703c546b519b82bf3ee3c1f121b8bd78ec 12 | created: "2025-06-06T08:31:15Z" 13 | appVersion: 1.2.0 14 | -------------------------------------------------------------------------------- /deployment/charts/cluster-connect-gateway-crd/templates/cluster.edge-orchestrator.intel.com_clusterconnects.yaml: -------------------------------------------------------------------------------- 1 | ../../../../config/crd/bases/cluster.edge-orchestrator.intel.com_clusterconnects.yaml -------------------------------------------------------------------------------- /deployment/charts/cluster-connect-gateway-crd/values.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | # This file is intentionally empty 6 | -------------------------------------------------------------------------------- /deployment/charts/cluster-connect-gateway/.helmignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | # Patterns to ignore when building packages. 6 | # This supports shell glob matching, relative path matching, and 7 | # negation (prefixed with !). Only one pattern per line. 8 | .DS_Store 9 | # Common VCS dirs 10 | .git/ 11 | .gitignore 12 | .bzr/ 13 | .bzrignore 14 | .hg/ 15 | .hgignore 16 | .svn/ 17 | # Common backup files 18 | *.swp 19 | *.bak 20 | *.tmp 21 | *.orig 22 | *~ 23 | # Various IDEs 24 | .project 25 | .idea/ 26 | *.tmproj 27 | .vscode/ 28 | -------------------------------------------------------------------------------- /deployment/charts/cluster-connect-gateway/Chart.lock: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | dependencies: 6 | - name: connect-agent 7 | repository: file://./charts/connect-agent 8 | version: 0.1.0 9 | digest: sha256:cfa1f8f1323909ade1c2ac06c3e2ab3e634a1ca9bdda3eb8775e7d98b17d32ec 10 | generated: "2025-04-25T11:36:03.022423105Z" 11 | -------------------------------------------------------------------------------- /deployment/charts/cluster-connect-gateway/Chart.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | --- 5 | apiVersion: v2 6 | name: cluster-connect-gateway 7 | description: A Helm chart for Cluster Connect Gateway 8 | type: application 9 | version: 1.2.0 10 | annotations: 11 | revision: 2db568703c546b519b82bf3ee3c1f121b8bd78ec 12 | created: "2025-06-06T08:31:15Z" 13 | appVersion: 1.2.0 14 | dependencies: 15 | - name: "connect-agent" 16 | repository: "file://./charts/connect-agent" 17 | version: 0.1.0 18 | -------------------------------------------------------------------------------- /deployment/charts/cluster-connect-gateway/charts/connect-agent/.helmignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | # Patterns to ignore when building packages. 6 | # This supports shell glob matching, relative path matching, and 7 | # negation (prefixed with !). Only one pattern per line. 8 | .DS_Store 9 | # Common VCS dirs 10 | .git/ 11 | .gitignore 12 | .bzr/ 13 | .bzrignore 14 | .hg/ 15 | .hgignore 16 | .svn/ 17 | # Common backup files 18 | *.swp 19 | *.bak 20 | *.tmp 21 | *.orig 22 | *~ 23 | # Various IDEs 24 | .project 25 | .idea/ 26 | *.tmproj 27 | .vscode/ 28 | -------------------------------------------------------------------------------- /deployment/charts/cluster-connect-gateway/charts/connect-agent/Chart.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | --- 5 | apiVersion: v2 6 | name: connect-agent 7 | description: A Helm chart for CCG Connect Agent 8 | type: application 9 | version: 0.1.0 10 | appVersion: 0.1.0 11 | 12 | -------------------------------------------------------------------------------- /deployment/charts/cluster-connect-gateway/charts/connect-agent/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{- /* 2 | SPDX-FileCopyrightText: (C) 2025 Intel Corporation 3 | 4 | SPDX-License-Identifier: Apache-2.0 5 | */ -}} 6 | 7 | {{/* 8 | Expand the name of the chart. 9 | */}} 10 | {{- define "connect-agent.name" -}} 11 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 12 | {{- end }} 13 | 14 | {{/* 15 | Create a default fully qualified app name. 16 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 17 | If release name contains chart name it will be used as a full name. 18 | */}} 19 | {{- define "connect-agent.fullname" -}} 20 | {{- if .Values.fullnameOverride }} 21 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 22 | {{- else }} 23 | {{- $name := default .Chart.Name .Values.nameOverride }} 24 | {{- if contains $name .Release.Name }} 25 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 26 | {{- else }} 27 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 28 | {{- end }} 29 | {{- end }} 30 | {{- end }} 31 | 32 | {{/* 33 | Create chart name and version as used by the chart label. 34 | */}} 35 | {{- define "connect-agent.chart" -}} 36 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 37 | {{- end }} 38 | 39 | {{/* 40 | Common labels 41 | */}} 42 | {{- define "connect-agent.labels" -}} 43 | helm.sh/chart: {{ include "connect-agent.chart" . }} 44 | {{ include "connect-agent.selectorLabels" . }} 45 | {{- if .Chart.AppVersion }} 46 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 47 | {{- end }} 48 | app.kubernetes.io/managed-by: {{ .Release.Service }} 49 | {{- end }} 50 | 51 | {{/* 52 | Selector labels 53 | */}} 54 | {{- define "connect-agent.selectorLabels" -}} 55 | app.kubernetes.io/name: {{ include "connect-agent.name" . }} 56 | app.kubernetes.io/instance: {{ .Release.Name }} 57 | {{- end }} 58 | 59 | -------------------------------------------------------------------------------- /deployment/charts/cluster-connect-gateway/charts/connect-agent/templates/configmap.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | apiVersion: v1 6 | kind: ConfigMap 7 | metadata: 8 | name: connect-agent-config 9 | labels: 10 | chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" 11 | release: "{{ .Release.Name }}" 12 | heritage: "{{ .Release.Service }}" 13 | data: 14 | AGENT_IMAGE: "{{- if hasKey .Values.agent.image.registry "name" }}{{ .Values.agent.image.registry.name }}/{{- end -}}{{ .Values.agent.image.repository }}:{{ .Values.agent.image.tag }}" 15 | AGENT_LOG_LEVEL: "{{ .Values.agent.logLevel }}" 16 | AGENT_TLS_MODE: "{{ .Values.agent.tlsMode }}" 17 | {{- with .Values.agent.extraEnv }} 18 | {{- range . }} 19 | {{- range $key, $value := . }} 20 | {{ $key }} : "{{ $value }}" 21 | {{- end }} 22 | {{- end }} 23 | {{- end }} 24 | 25 | -------------------------------------------------------------------------------- /deployment/charts/cluster-connect-gateway/charts/connect-agent/values.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | agent: 6 | image: 7 | registry: 8 | name: registry-rs.edgeorchestration.intel.com 9 | repository: edge-orch/cluster/connect-agent 10 | tag: 1.0.6 11 | # Available log levels are warn, info, debug. 12 | # Set to debug to print tunnel data to the logs. 13 | logLevel: info 14 | 15 | # Determines whether the agent should trust CA bundles from the operating system's trust store 16 | # when connecting to connect-gateway. True in `system-store` mode, false in `strict` mode. 17 | tlsMode: strict 18 | 19 | # Additional environment variables to pass. 20 | extraEnv: [] 21 | # - GATEWAY_CA: 22 | # - INSECURE_SKIP_VERIFY: 23 | # - HTTP_PROXY: 24 | # - HTTPS_PROXY: 25 | # - NO_PROXY: 26 | -------------------------------------------------------------------------------- /deployment/charts/cluster-connect-gateway/files/dashboards/dashboard.json.license: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 Intel Corporation 2 | # SPDX-License-Identifier: Apache-2.0 3 | -------------------------------------------------------------------------------- /deployment/charts/cluster-connect-gateway/files/openpolicyagent/policy.rego: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | package rbac 5 | 6 | default allow := false 7 | 8 | allow if { 9 | have_role 10 | } 11 | 12 | role := sprintf("%s_cl-rw", [input.project_id]) 13 | 14 | have_role if role == input.realm_access.roles[_] 15 | -------------------------------------------------------------------------------- /deployment/charts/cluster-connect-gateway/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{- /* 2 | SPDX-FileCopyrightText: (C) 2025 Intel Corporation 3 | 4 | SPDX-License-Identifier: Apache-2.0 5 | */ -}} 6 | 7 | {{/* 8 | Expand the name of the chart. 9 | */}} 10 | {{- define "cluster-connect-gateway.name" -}} 11 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 12 | {{- end }} 13 | 14 | {{/* 15 | Create a default fully qualified app name. 16 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 17 | If release name contains chart name it will be used as a full name. 18 | */}} 19 | {{- define "cluster-connect-gateway.fullname" -}} 20 | {{- if .Values.fullnameOverride }} 21 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 22 | {{- else }} 23 | {{- $name := default .Chart.Name .Values.nameOverride }} 24 | {{- if contains $name .Release.Name }} 25 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 26 | {{- else }} 27 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 28 | {{- end }} 29 | {{- end }} 30 | {{- end }} 31 | 32 | {{/* 33 | Create chart name and version as used by the chart label. 34 | */}} 35 | {{- define "cluster-connect-gateway.chart" -}} 36 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 37 | {{- end }} 38 | 39 | {{/* 40 | Common labels 41 | */}} 42 | {{- define "cluster-connect-gateway.labels" -}} 43 | helm.sh/chart: {{ include "cluster-connect-gateway.chart" . }} 44 | {{ include "cluster-connect-gateway.selectorLabels" . }} 45 | {{- if .Chart.AppVersion }} 46 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 47 | {{- end }} 48 | app.kubernetes.io/managed-by: {{ .Release.Service }} 49 | {{- end }} 50 | 51 | {{/* 52 | Controller's Metrics service labels 53 | */}} 54 | {{- define "cluster-connect-gateway.controllerMetricsServiceLabels" -}} 55 | {{- with .Values.controller.metrics.serviceLabels }} 56 | {{- toYaml . }} 57 | {{- end }} 58 | {{- end }} 59 | 60 | {{/* 61 | Gateway's service labels 62 | */}} 63 | {{- define "cluster-connect-gateway.gatewayMetricsServiceLabels" -}} 64 | {{- with .Values.gateway.service.labels }} 65 | {{- toYaml . }} 66 | {{- end }} 67 | {{- end }} 68 | 69 | 70 | {{/* 71 | Selector labels 72 | */}} 73 | {{- define "cluster-connect-gateway.selectorLabels" -}} 74 | app.kubernetes.io/name: {{ include "cluster-connect-gateway.name" . }} 75 | app.kubernetes.io/instance: {{ .Release.Name }} 76 | {{- end }} 77 | 78 | {{/* 79 | Create the name of the service account to use 80 | */}} 81 | {{- define "cluster-connect-gateway.serviceAccountName" -}} 82 | {{- if .Values.serviceAccount.create }} 83 | {{- default (include "cluster-connect-gateway.fullname" .) .Values.serviceAccount.name }} 84 | {{- else }} 85 | {{- default "default" .Values.serviceAccount.name }} 86 | {{- end }} 87 | {{- end }} 88 | -------------------------------------------------------------------------------- /deployment/charts/cluster-connect-gateway/templates/configmap.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | {{ if .Values.openpolicyagent.enabled }} 6 | apiVersion: v1 7 | kind: ConfigMap 8 | metadata: 9 | name: {{ template "cluster-connect-gateway.fullname" . }}-opa-rego-v2 10 | labels: 11 | chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" 12 | release: "{{ .Release.Name }}" 13 | heritage: "{{ .Release.Service }}" 14 | data: 15 | {{ (.Files.Glob "files/openpolicyagent/*.rego").AsConfig | indent 2 }} 16 | {{- end}} 17 | --- 18 | apiVersion: v1 19 | kind: ConfigMap 20 | metadata: 21 | name: {{ template "cluster-connect-gateway.fullname" . }}-dashboards-orchestrator 22 | labels: 23 | grafana_dashboard: "orchestrator" 24 | {{- include "cluster-connect-gateway.labels" . | nindent 4 }} 25 | {{- with .Values.gateway.metrics.dashboardAdminFolder }} 26 | annotations: 27 | grafana_folder: {{ . }} 28 | {{- end }} 29 | data: 30 | {{ (.Files.Glob "files/dashboards/*.json").AsConfig | indent 2 }} 31 | -------------------------------------------------------------------------------- /deployment/charts/cluster-connect-gateway/templates/deployment-controller.yaml: -------------------------------------------------------------------------------- 1 | # yamllint disable-file 2 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | 6 | {{- $registry := .Values.global.registry -}} 7 | {{- if .Values.controller.image.registry -}} 8 | {{- $registry = .Values.controller.image.registry -}} 9 | {{- end -}} 10 | 11 | --- 12 | apiVersion: apps/v1 13 | kind: Deployment 14 | metadata: 15 | name: {{ template "cluster-connect-gateway.fullname" . }}-controller 16 | {{- with .Values.controller.podAnnotations }} 17 | annotations: 18 | {{- toYaml . | nindent 2 }} 19 | {{- end }} 20 | labels: 21 | app.kubernetes.io/component: controller 22 | {{- include "cluster-connect-gateway.labels" . | nindent 4 }} 23 | spec: 24 | selector: 25 | matchLabels: 26 | app.kubernetes.io/component: controller 27 | {{- include "cluster-connect-gateway.selectorLabels" . | nindent 6 }} 28 | replicas: {{ .Values.controller.replicaCount }} 29 | template: 30 | metadata: 31 | labels: 32 | app.kubernetes.io/component: controller 33 | {{- include "cluster-connect-gateway.labels" . | nindent 8 }} 34 | spec: 35 | securityContext: 36 | {{- toYaml .Values.controller.podSecurityContext | nindent 8 }} 37 | {{- with $registry.imagePullSecrets }} 38 | imagePullSecrets: 39 | {{- toYaml . | nindent 8 }} 40 | {{- end }} 41 | containers: 42 | - name: connect-controller 43 | command: 44 | - /connect-controller 45 | args: 46 | - --leader-elect 47 | - --health-probe-bind-address=:8081 48 | - --connection-probe-timeout={{ .Values.controller.connectionProbeTimeout }} 49 | {{- if .Values.controller.metrics.enabled }} 50 | - --metrics-bind-address=:{{ .Values.controller.metrics.port }} 51 | - --metrics-secure=false 52 | {{- end }} 53 | {{- with .Values.controller.extraArgs }} 54 | {{- toYaml . | nindent 10 }} 55 | {{- end }} 56 | {{- with .Values.controller.image }} 57 | image: "{{- if hasKey $registry "name" }}{{ $registry.name }}/{{- end -}}{{ .repository }}:{{ default $.Chart.AppVersion .tag }}" 58 | {{- end }} 59 | imagePullPolicy: {{ .Values.controller.image.pullPolicy }} 60 | env: 61 | - name: PRIVATE_CA_ENABLED 62 | value: {{ .Values.controller.privateCA.enabled | quote }} 63 | - name: PRIVATE_CA_SECRET_NAME 64 | value: {{ .Values.controller.privateCA.secretName | quote }} 65 | - name: PRIVATE_CA_SECRET_NAMESPACE 66 | value: {{ .Values.controller.privateCA.secretNamespace | quote }} 67 | - name: SECRET_NAMESPACE 68 | value: {{ .Release.Namespace | quote }} 69 | - name: GATEWAY_EXTERNAL_URL 70 | value: {{ .Values.gateway.externalUrl | quote }} 71 | - name: GATEWAY_INTERNAL_URL 72 | value: "http://{{ template "cluster-connect-gateway.fullname" . }}.{{ .Release.Namespace }}.svc:{{ .Values.gateway.service.port}}" 73 | - name: AGENT_JWT_TOKEN_PATH 74 | value: {{ .Values.security.agent.jwtTokenPath }} 75 | - name: "AGENT_AUTH_MODE" 76 | value: {{ .Values.security.agent.authMode }} 77 | - name: AGENT_IMAGE 78 | valueFrom: 79 | configMapKeyRef: 80 | name: connect-agent-config 81 | key: AGENT_IMAGE 82 | - name: AGENT_LOG_LEVEL 83 | valueFrom: 84 | configMapKeyRef: 85 | name: connect-agent-config 86 | key: AGENT_LOG_LEVEL 87 | - name: AGENT_TLS_MODE 88 | valueFrom: 89 | configMapKeyRef: 90 | name: connect-agent-config 91 | key: AGENT_TLS_MODE 92 | {{- with .Values.controller.extraEnv }} 93 | {{- toYaml . | nindent 8 }} 94 | {{- end }} 95 | ports: 96 | {{- if .Values.controller.metrics.enabled }} 97 | - name: metrics 98 | containerPort: {{ .Values.controller.metrics.port }} 99 | protocol: TCP 100 | {{- end }} 101 | securityContext: 102 | {{- toYaml .Values.controller.containerSecurityContext | nindent 10 }} 103 | livenessProbe: 104 | httpGet: 105 | path: /healthz 106 | port: 8081 107 | initialDelaySeconds: 15 108 | periodSeconds: 20 109 | readinessProbe: 110 | httpGet: 111 | path: /readyz 112 | port: 8081 113 | initialDelaySeconds: 5 114 | periodSeconds: 10 115 | {{- with .Values.controller.resources }} 116 | resources: 117 | {{- toYaml . | nindent 10 }} 118 | {{- end }} 119 | volumeMounts: [] 120 | volumes: [] 121 | serviceAccountName: {{ template "cluster-connect-gateway.serviceAccountName" . }} 122 | terminationGracePeriodSeconds: 10 123 | -------------------------------------------------------------------------------- /deployment/charts/cluster-connect-gateway/templates/deployment-gateway.yaml: -------------------------------------------------------------------------------- 1 | # yamllint disable-file 2 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | 6 | {{- $registry := .Values.global.registry -}} 7 | {{- if .Values.gateway.image.registry -}} 8 | {{- $registry = .Values.gateway.image.registry -}} 9 | {{- end -}} 10 | 11 | --- 12 | apiVersion: apps/v1 13 | kind: Deployment 14 | metadata: 15 | name: {{ template "cluster-connect-gateway.fullname" . }}-gateway 16 | annotations: 17 | {{- with .Values.gateway.podAnnotations }} 18 | {{- toYaml . | nindent 2 }} 19 | {{- end }} 20 | labels: 21 | app.kubernetes.io/component: gateway 22 | {{- include "cluster-connect-gateway.labels" . | nindent 4 }} 23 | spec: 24 | selector: 25 | matchLabels: 26 | app.kubernetes.io/component: gateway 27 | {{- include "cluster-connect-gateway.selectorLabels" . | nindent 6 }} 28 | replicas: {{ .Values.gateway.replicaCount }} 29 | template: 30 | metadata: 31 | labels: 32 | app.kubernetes.io/component: gateway 33 | {{- include "cluster-connect-gateway.labels" . | nindent 8 }} 34 | app: {{template "cluster-connect-gateway.fullname" .}}-gateway 35 | spec: 36 | securityContext: 37 | {{- toYaml .Values.gateway.podSecurityContext | nindent 8 }} 38 | containers: 39 | - name: connect-gateway 40 | {{- with .Values.gateway.image }} 41 | image: "{{- if hasKey $registry "name" }}{{ $registry.name }}/{{- end -}}{{ .repository }}:{{ default $.Chart.AppVersion .tag }}" 42 | {{- end }} 43 | imagePullPolicy: {{ .Values.gateway.image.pullPolicy }} 44 | command: [ "/connect-gateway" ] 45 | env: 46 | {{- if .Values.gateway.metrics.enable }} 47 | - name: CATTLE_PROMETHEUS_METRICS 48 | value: "true" 49 | {{- end }} 50 | {{- if eq .Values.gateway.logLevel "debug" }} 51 | - name: CATTLE_TUNNEL_DATA_DEBUG 52 | value: "true" 53 | {{- end }} 54 | {{- if or .Values.gateway.oidc.enabled (eq .Values.security.agent.authMode "jwt") }} 55 | - name: OIDC_SERVER_URL 56 | value: {{ .Values.gateway.oidc.issuer }} 57 | - name: OIDC_TLS_INSECURE_SKIP_VERIFY 58 | value: {{ .Values.gateway.oidc.insecureSkipVerify | quote }} 59 | {{- end }} 60 | {{- with .Values.gateway.extraEnv }} 61 | {{- range $key, $value := . }} 62 | - name: {{ $key }} 63 | value: {{ $value | quote }} 64 | {{- end }} 65 | {{- end }} 66 | - name: SECRET_NAMESPACE 67 | value: {{ .Release.Namespace | quote }} 68 | args: 69 | - "--address={{ .Values.gateway.listenAddress }}" 70 | - "--port={{ .Values.gateway.listenPort }}" 71 | {{- if .Values.gateway.oidc.enabled }} 72 | - "--enable-auth=true" 73 | - "--oidc-issuer-url={{ .Values.gateway.oidc.issuer }}" 74 | - "--oidc-insecure-skip-verify={{ .Values.gateway.oidc.insecureSkipVerify }}" 75 | {{- end }} 76 | - "--enable-metrics={{ .Values.gateway.metrics.enable }}" 77 | - "--log-level={{ .Values.gateway.logLevel }}" 78 | - "--external-host={{ .Values.gateway.ingress.hostname }}" 79 | - "--tunnel-auth-mode={{ .Values.security.agent.authMode }}" 80 | - "--connection-probe-interval={{ .Values.gateway.connectionProbeInterval }}" 81 | {{- with .Values.gateway.extraArgs }} 82 | {{- range . }} 83 | - {{ . | quote }} 84 | {{- end }} 85 | {{- end }} 86 | ports: 87 | - containerPort: {{ .Values.gateway.listenPort }} 88 | securityContext: 89 | {{- toYaml .Values.gateway.containerSecurityContext | nindent 12 }} 90 | {{- with .Values.controller.resources }} 91 | resources: 92 | {{- toYaml . | nindent 12 }} 93 | {{- end }} 94 | volumeMounts: [] 95 | {{- if .Values.openpolicyagent.enabled }} 96 | - name: openpolicyagent 97 | securityContext: 98 | {{- toYaml .Values.openpolicyagent.securityContext | nindent 12 }} 99 | resources: 100 | {{- toYaml .Values.resources | nindent 12 }} 101 | {{- with .Values.openpolicyagent }} 102 | image: "{{- if .registry -}}{{ .registry }}/{{- end -}}{{ .image }}:{{ .tag }}" 103 | {{- end}} 104 | imagePullPolicy: {{ .Values.openpolicyagent.pullPolicy }} 105 | ports: 106 | - name: opa 107 | containerPort: {{ .Values.openpolicyagent.port }} 108 | args: 109 | - "run" 110 | - "--server" 111 | - "--bundle" 112 | - "/rego/v2" 113 | - "--log-level" 114 | - {{ .Values.openpolicyagent.loglevel }} 115 | - "--disable-telemetry" 116 | - "--addr" 117 | - "0.0.0.0:{{ .Values.openpolicyagent.port }}" 118 | livenessProbe: 119 | httpGet: 120 | path: /health?bundle=true # Include bundle activation in readiness 121 | scheme: HTTP 122 | port: {{ .Values.openpolicyagent.port }} 123 | initialDelaySeconds: 10 124 | periodSeconds: 10 125 | readinessProbe: 126 | httpGet: 127 | path: /health?bundle=true # Include bundle activation in readiness 128 | scheme: HTTP 129 | port: {{ .Values.openpolicyagent.port }} 130 | initialDelaySeconds: 10 131 | periodSeconds: 10 132 | volumeMounts: 133 | - name: openpolicyagent-v2 134 | mountPath: /rego/v2 135 | readOnly: true 136 | {{- end }} 137 | volumes: 138 | {{- if .Values.openpolicyagent.enabled }} 139 | - name: openpolicyagent-v2 140 | configMap: 141 | name: {{ template "cluster-connect-gateway.fullname" . }}-opa-rego-v2 142 | {{- end }} 143 | serviceAccountName: {{ template "cluster-connect-gateway.serviceAccountName" . }} 144 | terminationGracePeriodSeconds: 10 145 | -------------------------------------------------------------------------------- /deployment/charts/cluster-connect-gateway/templates/rbac.yaml: -------------------------------------------------------------------------------- 1 | # yamllint disable-file 2 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | --- 6 | apiVersion: rbac.authorization.k8s.io/v1 7 | kind: ClusterRole 8 | metadata: 9 | labels: 10 | {{- include "cluster-connect-gateway.labels" . | nindent 4 }} 11 | name: {{ template "cluster-connect-gateway.fullname" . }}-controller 12 | rules: 13 | - apiGroups: 14 | - "" 15 | resources: 16 | - secrets 17 | verbs: 18 | - create 19 | - get 20 | - list 21 | - patch 22 | - update 23 | - watch 24 | - apiGroups: 25 | - apiextensions.k8s.io 26 | resources: 27 | - customresourcedefinitions 28 | verbs: 29 | - get 30 | - list 31 | - watch 32 | - apiGroups: 33 | - cluster.edge-orchestrator.intel.com 34 | resources: 35 | - clusterconnects 36 | verbs: 37 | - create 38 | - delete 39 | - get 40 | - list 41 | - patch 42 | - update 43 | - watch 44 | - apiGroups: 45 | - cluster.edge-orchestrator.intel.com 46 | resources: 47 | - clusterconnects/finalizers 48 | verbs: 49 | - update 50 | - apiGroups: 51 | - cluster.edge-orchestrator.intel.com 52 | resources: 53 | - clusterconnects/status 54 | verbs: 55 | - get 56 | - patch 57 | - update 58 | - apiGroups: 59 | - cluster.x-k8s.io 60 | resources: 61 | - clusters 62 | - clusters/status 63 | verbs: 64 | - get 65 | - list 66 | - patch 67 | - update 68 | - watch 69 | - apiGroups: 70 | - "" 71 | resources: 72 | - configmaps 73 | verbs: 74 | - get 75 | - list 76 | - watch 77 | - create 78 | - update 79 | - patch 80 | - delete 81 | - apiGroups: 82 | - coordination.k8s.io 83 | resources: 84 | - leases 85 | verbs: 86 | - get 87 | - list 88 | - watch 89 | - create 90 | - update 91 | - patch 92 | - delete 93 | - apiGroups: 94 | - "" 95 | resources: 96 | - events 97 | verbs: 98 | - create 99 | - patch 100 | - apiGroups: 101 | - authentication.k8s.io 102 | resources: 103 | - tokenreviews 104 | verbs: 105 | - create 106 | - apiGroups: 107 | - authorization.k8s.io 108 | resources: 109 | - subjectaccessreviews 110 | verbs: 111 | - create 112 | - nonResourceURLs: 113 | - "/metrics" 114 | verbs: 115 | - get 116 | --- 117 | apiVersion: rbac.authorization.k8s.io/v1 118 | kind: ClusterRoleBinding 119 | metadata: 120 | labels: 121 | {{- include "cluster-connect-gateway.labels" . | nindent 4 }} 122 | name: {{ template "cluster-connect-gateway.fullname" . }}-controller 123 | roleRef: 124 | apiGroup: rbac.authorization.k8s.io 125 | kind: ClusterRole 126 | name: {{ template "cluster-connect-gateway.fullname" . }}-controller 127 | subjects: 128 | - kind: ServiceAccount 129 | name: {{ template "cluster-connect-gateway.serviceAccountName" . }} 130 | namespace: {{ .Release.Namespace }} 131 | -------------------------------------------------------------------------------- /deployment/charts/cluster-connect-gateway/templates/service.yaml: -------------------------------------------------------------------------------- 1 | # yamllint disable-file 2 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | --- 6 | apiVersion: v1 7 | kind: Service 8 | metadata: 9 | name: {{ template "cluster-connect-gateway.fullname" . }} 10 | labels: 11 | {{- include "cluster-connect-gateway.gatewayMetricsServiceLabels" . | nindent 4 }} 12 | spec: 13 | selector: 14 | app: {{ template "cluster-connect-gateway.fullname" . }}-gateway 15 | type: {{ .Values.gateway.service.type }} 16 | ports: 17 | - protocol: TCP 18 | port: {{ .Values.gateway.service.port }} 19 | targetPort: {{ .Values.gateway.listenPort }} 20 | name: http-gateway 21 | -------------------------------------------------------------------------------- /deployment/charts/cluster-connect-gateway/templates/service_metrics.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | {{ if .Values.controller.metrics.enabled -}} 5 | apiVersion: v1 6 | kind: Service 7 | metadata: 8 | name: {{ include "cluster-connect-gateway.fullname" . }}-controller-metrics 9 | namespace: {{ .Release.Namespace }} 10 | labels: 11 | {{- include "cluster-connect-gateway.labels" . | nindent 4 }} 12 | {{- include "cluster-connect-gateway.controllerMetricsServiceLabels" . | nindent 4 }} 13 | spec: 14 | ports: 15 | - name: http-metrics-controller 16 | port: {{ .Values.controller.metrics.port }} 17 | protocol: TCP 18 | targetPort: metrics 19 | selector: 20 | app.kubernetes.io/component: controller 21 | {{- include "cluster-connect-gateway.selectorLabels" . | nindent 4 -}} 22 | {{- end -}} 23 | -------------------------------------------------------------------------------- /deployment/charts/cluster-connect-gateway/templates/service_monitor.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | {{- if .Values.controller.metrics.serviceMonitor.enabled -}} 5 | # Prometheus Monitor Service (Metrics) 6 | apiVersion: monitoring.coreos.com/v1 7 | kind: ServiceMonitor 8 | metadata: 9 | name: {{ include "cluster-connect-gateway.fullname" . }}-controller 10 | namespace: {{ .Release.Namespace }} 11 | labels: 12 | {{- include "cluster-connect-gateway.labels" . | nindent 4 }} 13 | spec: 14 | endpoints: 15 | - path: /metrics 16 | port: http-metrics-controller 17 | scheme: http 18 | namespaceSelector: 19 | matchNames: 20 | - {{ .Release.Namespace }} 21 | selector: 22 | matchExpressions: 23 | - key: prometheus.io/service-monitor 24 | operator: NotIn 25 | values: 26 | - "false" 27 | matchLabels: 28 | {{- include "cluster-connect-gateway.controllerMetricsServiceLabels" . | nindent 6 }} 29 | --- 30 | {{- end -}} 31 | {{- if .Values.gateway.metrics.serviceMonitor.enabled -}} 32 | # Prometheus Monitor Service (Metrics) 33 | apiVersion: monitoring.coreos.com/v1 34 | kind: ServiceMonitor 35 | metadata: 36 | name: {{ include "cluster-connect-gateway.fullname" . }}-gateway 37 | namespace: {{ .Release.Namespace }} 38 | labels: 39 | {{- include "cluster-connect-gateway.labels" . | nindent 4 }} 40 | spec: 41 | endpoints: 42 | - path: /metrics 43 | port: http-gateway 44 | scheme: http 45 | namespaceSelector: 46 | matchNames: 47 | - {{ .Release.Namespace }} 48 | selector: 49 | matchExpressions: 50 | - key: prometheus.io/service-monitor 51 | operator: NotIn 52 | values: 53 | - "false" 54 | matchLabels: 55 | {{- include "cluster-connect-gateway.gatewayMetricsServiceLabels" . | nindent 6 }} 56 | --- 57 | {{- end -}} -------------------------------------------------------------------------------- /deployment/charts/cluster-connect-gateway/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | # yamllint disable-file 2 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | 6 | {{- if .Values.serviceAccount.create }} 7 | apiVersion: v1 8 | kind: ServiceAccount 9 | metadata: 10 | labels: 11 | {{- include "cluster-connect-gateway.labels" . | nindent 4 }} 12 | name: {{ template "cluster-connect-gateway.serviceAccountName" . }} 13 | {{- end }} -------------------------------------------------------------------------------- /deployment/charts/cluster-connect-gateway/templates/traefik-ingress.yaml: -------------------------------------------------------------------------------- 1 | # yamllint disable-file 2 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | 6 | {{- if .Values.gateway.ingress.enabled }} 7 | --- 8 | apiVersion: traefik.containo.us/v1alpha1 9 | kind: IngressRoute 10 | metadata: 11 | name: cluster-connect-gateway-kubeapi 12 | namespace: {{ .Values.gateway.ingress.namespace }} 13 | spec: 14 | routes: 15 | - match: Host(`{{ required "Traefik route match is required!" .Values.gateway.ingress.hostname }}`) && PathPrefix(`/kubernetes`) 16 | kind: Rule 17 | {{- if .Values.gateway.ingress.authMiddleware }} 18 | middlewares: 19 | - name: validate-jwt 20 | {{- end }} 21 | services: 22 | - name: {{ template "cluster-connect-gateway.fullname" . }} 23 | namespace: {{ .Release.Namespace }} 24 | port: {{ .Values.gateway.service.port }} 25 | scheme: http 26 | --- 27 | # ingress for edge-connect-agent websocket connection 28 | apiVersion: traefik.containo.us/v1alpha1 29 | kind: IngressRoute 30 | metadata: 31 | name: cluster-connect-gateway-ws 32 | namespace: {{ .Values.gateway.ingress.namespace }} 33 | spec: 34 | routes: 35 | - match: Host(`{{ required "Traefik route match is required!" .Values.gateway.ingress.hostname }}`) && PathPrefix(`/connect`) 36 | kind: Rule 37 | services: 38 | - name: {{ template "cluster-connect-gateway.fullname" . }} 39 | namespace: {{ .Release.Namespace }} 40 | port: {{ .Values.gateway.service.port }} 41 | scheme: http 42 | {{- end }} 43 | -------------------------------------------------------------------------------- /deployment/charts/cluster-connect-gateway/values.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | global: 6 | registry: {} # TODO: Add correct registry 7 | # name: 8 | # imagePullSecrets: [] 9 | 10 | # This is to override the chart name. 11 | nameOverride: "" 12 | fullnameOverride: "" 13 | 14 | # This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/ 15 | serviceAccount: 16 | # Specifies whether a service account should be created 17 | create: true 18 | # The name of the service account to use. 19 | # If not set and create is true, a name is generated using the fullname template. 20 | name: "" 21 | 22 | security: # TODO move variables to this section to remove duplicates 23 | 24 | agent: 25 | authMode: "token" 26 | # path to jwt token that is used for agent auth to gateway. 27 | jwtTokenPath: "/etc/intel_edge_node/tokens/connect-agent/access_token" 28 | 29 | gateway: 30 | image: 31 | registry: 32 | name: registry-rs.edgeorchestration.intel.com 33 | # imagePullSecrets: [] 34 | repository: edge-orch/cluster/connect-gateway 35 | pullPolicy: IfNotPresent 36 | tag: "" 37 | 38 | replicaCount: 1 39 | 40 | resources: 41 | gateway: 42 | limits: 43 | cpu: 1 44 | memory: 512Mi 45 | requests: 46 | cpu: 10m 47 | memory: 128Mi 48 | 49 | # This is for setting Kubernetes Annotations to a Pod. 50 | podAnnotations: {} 51 | 52 | # This is for setting Kubernetes Labels to a Pod. 53 | podLabels: {} 54 | 55 | ## Configure Pods Security Context 56 | podSecurityContext: 57 | #runAsGroup: 58 | runAsNonRoot: true 59 | 60 | ## Configure Container Security Context (only main container) 61 | containerSecurityContext: 62 | allowPrivilegeEscalation: false 63 | readOnlyRootFilesystem: true 64 | seccompProfile: 65 | type: RuntimeDefault 66 | capabilities: 67 | drop: 68 | - "ALL" 69 | 70 | # Available log levels are warn, info, debug. 71 | # Set to debug to print tunnel data to the logs. 72 | logLevel: info 73 | 74 | # The address and port the gateway listens on 75 | listenAddress: "0.0.0.0" 76 | listenPort: 8080 77 | 78 | # Enable prometheus metrics 79 | metrics: 80 | enable: true 81 | serviceMonitor: 82 | enabled: false 83 | dashboardAdminFolder: orchestrator 84 | 85 | # Format: protocol://domain[:port]. Usually: 86 | # 1) if "exposureType" is "ingress", the "domain" should be the value of "ingress.hostname" 87 | # 2) if "exposureType" is "service" and "service.type" is "ClusterIP", the "domain" should be the service name and namespace 88 | # 3) if "exposureType" is "service" and "service.type" is "NodePort", the "domain" should be the IP address of K8s node 89 | # 4) if "exposureType" is "service" and "service.type" is "LoadBalancer", the "domain" should be the LoadBalancer IP 90 | externalUrl: ws://cluster-connect-gateway.default.svc:8080 91 | 92 | service: 93 | type: ClusterIP 94 | port: 8080 95 | labels: 96 | app: gateway-svc 97 | 98 | # TODO: Support standard ingress 99 | ingress: 100 | enabled: false 101 | authMiddleware: false 102 | hostname: connect-gateway.kind.internal 103 | namespace: orch-gateway 104 | 105 | oidc: 106 | enabled: false 107 | issuer: "http://platform-keycloak.orch-platform.svc:8080/realms/master" 108 | insecureSkipVerify: true 109 | 110 | # TODO: add ingress configuration 111 | 112 | # Additional command line flags to pass. 113 | extraArgs: [] 114 | 115 | # Additional environment variables to pass. 116 | extraEnv: [] 117 | 118 | # Interval for connection probe to downstream clusters 119 | connectionProbeInterval: "1m" 120 | 121 | openpolicyagent: 122 | enabled: false 123 | port: 8181 124 | image: openpolicyagent/opa 125 | tag: 1.2.0 126 | loglevel: debug 127 | pullPolicy: IfNotPresent 128 | securityContext: 129 | allowPrivilegeEscalation: false 130 | readOnlyRootFilesystem: true 131 | seccompProfile: 132 | type: RuntimeDefault 133 | capabilities: 134 | drop: 135 | - "ALL" 136 | 137 | controller: 138 | image: 139 | registry: 140 | name: registry-rs.edgeorchestration.intel.com 141 | # imagePullSecrets: [] 142 | repository: edge-orch/cluster/connect-controller #TODO: Add correct repository name 143 | # This sets the pull policy for images. 144 | pullPolicy: IfNotPresent 145 | # Overrides the image tag whose default is the chart appVersion. 146 | tag: "" 147 | 148 | metrics: 149 | enabled: true 150 | port: 8080 151 | # Labels applied to the service exposing metrics. 152 | serviceLabels: 153 | app: controller-metrics-svc 154 | serviceMonitor: 155 | enabled: false 156 | 157 | replicaCount: 1 158 | 159 | resources: 160 | limits: 161 | cpu: 1 162 | memory: 512Mi 163 | requests: 164 | cpu: 10m 165 | memory: 128Mi 166 | 167 | # This is for setting Kubernetes Annotations to a Pod. 168 | podAnnotations: {} 169 | 170 | # This is for setting Kubernetes Labels to a Pod. 171 | podLabels: {} 172 | 173 | ## Configure Pods Security Context 174 | podSecurityContext: 175 | #runAsGroup: 176 | runAsNonRoot: true 177 | 178 | ## Configure Container Security Context (only main container) 179 | containerSecurityContext: 180 | allowPrivilegeEscalation: false 181 | readOnlyRootFilesystem: true 182 | seccompProfile: 183 | type: RuntimeDefault 184 | capabilities: 185 | drop: 186 | - "ALL" 187 | 188 | # Set the verbosity. Range of 0 - 6 with 6 being the most verbose. 189 | logLevel: 2 190 | 191 | # Additional command line flags to pass. 192 | extraArgs: [] 193 | 194 | # Additional environment variables to pass. 195 | extraEnv: [] 196 | 197 | # Enabling private CA will set an orchestration self-signed certificate in the kubeConfig secret 198 | # which can be used by the downstream cluster fleet-agent to access the Kubernetes API service in the orchestration cluster 199 | privateCA: 200 | enabled: true 201 | secretName: "tls-orch" 202 | secretNamespace : "orch-gateway" 203 | 204 | # Timeout for connection probe to downstream clusters 205 | connectionProbeTimeout: "5m" 206 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/open-edge-platform/cluster-connect-gateway 2 | 3 | go 1.23.6 4 | 5 | require ( 6 | github.com/atomix/dazl v1.1.4 7 | github.com/atomix/dazl/zap v1.0.1 8 | github.com/golang-jwt/jwt/v5 v5.2.2 9 | github.com/gorilla/mux v1.8.1 10 | github.com/gorilla/websocket v1.5.1 11 | github.com/onsi/ginkgo/v2 v2.22.2 12 | github.com/onsi/gomega v1.36.2 13 | github.com/open-edge-platform/orch-library/go v0.5.29 14 | github.com/pkg/errors v0.9.1 15 | github.com/prometheus/client_golang v1.20.5 16 | github.com/rancher/remotedialer v0.4.1 17 | github.com/sirupsen/logrus v1.9.3 18 | github.com/stretchr/testify v1.10.0 19 | go.uber.org/zap v1.27.0 20 | k8s.io/api v0.32.3 21 | k8s.io/apiextensions-apiserver v0.32.1 22 | k8s.io/apimachinery v0.32.3 23 | k8s.io/client-go v0.32.2 24 | sigs.k8s.io/cluster-api v1.9.5 25 | sigs.k8s.io/controller-runtime v0.20.2 26 | ) 27 | 28 | require ( 29 | cel.dev/expr v0.19.0 // indirect 30 | github.com/antlr4-go/antlr/v4 v4.13.0 // indirect 31 | github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect 32 | github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect 33 | github.com/beorn7/perks v1.0.1 // indirect 34 | github.com/blang/semver/v4 v4.0.0 // indirect 35 | github.com/cenkalti/backoff/v3 v3.0.0 // indirect 36 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 37 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 38 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 39 | github.com/emicklei/go-restful/v3 v3.12.1 // indirect 40 | github.com/evanphx/json-patch/v5 v5.9.11 // indirect 41 | github.com/felixge/httpsnoop v1.0.4 // indirect 42 | github.com/fsnotify/fsnotify v1.8.0 // indirect 43 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 44 | github.com/go-jose/go-jose/v3 v3.0.4 // indirect 45 | github.com/go-jose/go-jose/v4 v4.0.5 // indirect 46 | github.com/go-logr/logr v1.4.2 // indirect 47 | github.com/go-logr/stdr v1.2.2 // indirect 48 | github.com/go-logr/zapr v1.3.0 // indirect 49 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 50 | github.com/go-openapi/jsonreference v0.20.2 // indirect 51 | github.com/go-openapi/swag v0.23.0 // indirect 52 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 53 | github.com/gobuffalo/flect v1.0.3 // indirect 54 | github.com/gogo/protobuf v1.3.2 // indirect 55 | github.com/golang/protobuf v1.5.4 // indirect 56 | github.com/google/btree v1.1.3 // indirect 57 | github.com/google/cel-go v0.22.0 // indirect 58 | github.com/google/gnostic-models v0.6.8 // indirect 59 | github.com/google/go-cmp v0.6.0 // indirect 60 | github.com/google/gofuzz v1.2.0 // indirect 61 | github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect 62 | github.com/google/uuid v1.6.0 // indirect 63 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 // indirect 64 | github.com/hashicorp/errwrap v1.1.0 // indirect 65 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 66 | github.com/hashicorp/go-multierror v1.1.1 // indirect 67 | github.com/hashicorp/go-retryablehttp v0.7.7 // indirect 68 | github.com/hashicorp/go-rootcerts v1.0.2 // indirect 69 | github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 // indirect 70 | github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect 71 | github.com/hashicorp/go-sockaddr v1.0.2 // indirect 72 | github.com/hashicorp/hcl v1.0.0 // indirect 73 | github.com/hashicorp/vault/api v1.14.0 // indirect 74 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 75 | github.com/josharian/intern v1.0.0 // indirect 76 | github.com/json-iterator/go v1.1.12 // indirect 77 | github.com/klauspost/compress v1.17.11 // indirect 78 | github.com/mailru/easyjson v0.7.7 // indirect 79 | github.com/mitchellh/go-homedir v1.1.0 // indirect 80 | github.com/mitchellh/mapstructure v1.5.0 // indirect 81 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 82 | github.com/modern-go/reflect2 v1.0.2 // indirect 83 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 84 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect 85 | github.com/oapi-codegen/runtime v1.1.1 // indirect 86 | github.com/open-edge-platform/orch-library/go/dazl v0.5.1 // indirect 87 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 88 | github.com/prometheus/client_model v0.6.1 // indirect 89 | github.com/prometheus/common v0.55.0 // indirect 90 | github.com/prometheus/procfs v0.15.1 // indirect 91 | github.com/ryanuber/go-glob v1.0.0 // indirect 92 | github.com/spf13/cast v1.7.1 // indirect 93 | github.com/spf13/cobra v1.8.1 // indirect 94 | github.com/spf13/pflag v1.0.6 // indirect 95 | github.com/stoewer/go-strcase v1.3.0 // indirect 96 | github.com/stretchr/objx v0.5.2 // indirect 97 | github.com/x448/float16 v0.8.4 // indirect 98 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 99 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect 100 | go.opentelemetry.io/otel v1.34.0 // indirect 101 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect 102 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect 103 | go.opentelemetry.io/otel/metric v1.34.0 // indirect 104 | go.opentelemetry.io/otel/sdk v1.34.0 // indirect 105 | go.opentelemetry.io/otel/trace v1.34.0 // indirect 106 | go.opentelemetry.io/proto/otlp v1.5.0 // indirect 107 | go.uber.org/mock v0.5.0 // indirect 108 | go.uber.org/multierr v1.11.0 // indirect 109 | golang.org/x/crypto v0.36.0 // indirect 110 | golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect 111 | golang.org/x/net v0.38.0 // indirect 112 | golang.org/x/oauth2 v0.25.0 // indirect 113 | golang.org/x/sync v0.12.0 // indirect 114 | golang.org/x/sys v0.31.0 // indirect 115 | golang.org/x/term v0.30.0 // indirect 116 | golang.org/x/text v0.23.0 // indirect 117 | golang.org/x/time v0.9.0 // indirect 118 | golang.org/x/tools v0.30.1-0.20250221230316-5055f70f240c // indirect 119 | gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect 120 | google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect 121 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect 122 | google.golang.org/grpc v1.70.0 // indirect 123 | google.golang.org/protobuf v1.36.3 // indirect 124 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 125 | gopkg.in/inf.v0 v0.9.1 // indirect 126 | gopkg.in/yaml.v3 v3.0.1 // indirect 127 | k8s.io/apiserver v0.32.1 // indirect 128 | k8s.io/component-base v0.32.1 // indirect 129 | k8s.io/klog/v2 v2.130.1 // indirect 130 | k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect 131 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect 132 | sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 // indirect 133 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect 134 | sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect 135 | sigs.k8s.io/yaml v1.4.0 // indirect 136 | ) 137 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | // SPDX-License-Identifier: Apache-2.0 -------------------------------------------------------------------------------- /internal/agent/agent.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package agent 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "net" 10 | "net/http" 11 | "os" 12 | "strings" 13 | "time" 14 | 15 | "github.com/gorilla/websocket" 16 | "github.com/rancher/remotedialer" 17 | "go.uber.org/zap" 18 | 19 | "github.com/open-edge-platform/cluster-connect-gateway/internal/utils/certutil" 20 | ) 21 | 22 | type ConnectAgent struct { 23 | AuthToken string 24 | Closed chan struct{} 25 | GatewayUrl string 26 | InsecureSkipVerify bool 27 | TunnelId string 28 | TokenPath string 29 | TunnelAuthMode string 30 | } 31 | 32 | const ( 33 | TunnelIdHeader = "X-Tunnel-Id" // #nosec G101 34 | TokenHeader = "X-API-Tunnel-Token" // #nosec G101 35 | ) 36 | 37 | func (c *ConnectAgent) Run(ctx context.Context) { 38 | // Use Go's built-in resolver to resolve DNS names. We may want to revisit this. 39 | resolver := &net.Resolver{ 40 | PreferGo: true, 41 | } 42 | 43 | // Create a new dialer with the resolver and the TLS configuration 44 | dialer := &websocket.Dialer{ 45 | HandshakeTimeout: 10 * time.Minute, 46 | NetDialContext: (&net.Dialer{ 47 | Timeout: 30 * time.Second, 48 | KeepAlive: 30 * time.Second, 49 | Resolver: resolver, 50 | }).DialContext, 51 | TLSClientConfig: certutil.GetTLSConfigs(c.InsecureSkipVerify), 52 | } 53 | headers := http.Header{ 54 | TunnelIdHeader: {c.TunnelId}, 55 | } 56 | switch c.TunnelAuthMode { 57 | case "token": 58 | zap.L().Info("Token auth to gateway enabled") 59 | headers.Add(TokenHeader, c.AuthToken) 60 | case "jwt": 61 | zap.L().Info("Jwt auth to gateway enabled") 62 | // Read jwt token provided by node-agent 63 | token, err := os.ReadFile(c.TokenPath) 64 | if err != nil { 65 | zap.L().Fatal("Error reading token file", zap.Error(err)) 66 | } 67 | 68 | // Convert the token to a string and trim any whitespace 69 | jwtToken := string(token) 70 | jwtToken = strings.TrimSpace(jwtToken) 71 | 72 | headers.Add("Authorization", fmt.Sprintf("Bearer %s", jwtToken)) 73 | } 74 | 75 | connAuthorizer := func(proto, address string) bool { 76 | switch proto { 77 | case "tcp": 78 | return true 79 | case "unix": 80 | return address == "/var/run/docker.sock" 81 | case "npipe": 82 | return address == "//./pipe/docker_engine" 83 | } 84 | return false 85 | } 86 | 87 | onConnect := func(ctx context.Context, _ *remotedialer.Session) error { 88 | // Do nothing on successful connection now 89 | // Periodic checks can be added here later 90 | zap.L().Info("Connected to gateway") 91 | return nil 92 | } 93 | 94 | if err := remotedialer.ClientConnect(ctx, 95 | c.GatewayUrl, 96 | headers, 97 | dialer, 98 | connAuthorizer, 99 | onConnect); err != nil { 100 | errMsg := fmt.Errorf("Unable to connect to %s (%s): %v", c.GatewayUrl, c.TunnelId, err) 101 | zap.L().Error(errMsg.Error()) 102 | panic(errMsg) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /internal/agentconfig/agentconfig.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package agentconfig 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | "html/template" 10 | "net/url" 11 | "os" 12 | ) 13 | 14 | const ( 15 | agentTemplateText = `apiVersion: v1 16 | kind: Pod 17 | metadata: 18 | name: connect-agent 19 | namespace: kube-system 20 | spec: 21 | containers: 22 | - name: connect-agent 23 | image: "{{.Image}}" 24 | {{- if ne .HttpProxy "" }} 25 | env: 26 | - name: HTTP_PROXY 27 | value: {{.HttpProxy}} 28 | {{- end }} 29 | {{- if ne .HttpsProxy "" }} 30 | - name: HTTPS_PROXY 31 | value: {{.HttpsProxy}} 32 | {{- end }} 33 | {{- if ne .NoProxy "" }} 34 | - name: NO_PROXY 35 | value: {{.NoProxy}} 36 | {{- end }} 37 | command: [ "/connect-agent" ] 38 | args: 39 | - "--gateway-url={{.GatewayURL}}" 40 | - "--tunnel-id={{.TunnelID}}" 41 | - "--auth-token={{.Token}}" 42 | - "--insecure-skip-verify={{.InsecureSkipVerify}}" 43 | - "--log-level={{.LogLevel}}" 44 | - "--token-path={{.TokenPath}}" 45 | - "--tunnel-auth-mode={{.AgentAuthMode}}" 46 | securityContext: 47 | {{- if eq .AgentAuthMode "jwt" }} 48 | runAsUser: 501 49 | runAsGroup: 500 50 | {{- end }} 51 | allowPrivilegeEscalation: false 52 | capabilities: 53 | drop: 54 | - ALL 55 | readOnlyRootFilesystem: true 56 | seccompProfile: 57 | type: RuntimeDefault 58 | resources: 59 | limits: {} 60 | requests: 61 | cpu: 100m 62 | memory: 128Mi 63 | volumeMounts: 64 | {{- if eq .TLSMode "system-store" }} 65 | - name: server-ca 66 | mountPath: /etc/secrets/ca/cert 67 | readOnly: true 68 | {{- end }} 69 | {{- if eq .AgentAuthMode "jwt" }} 70 | - name: jwt-token 71 | mountPath: {{.TokenPath}} 72 | readOnly: true 73 | {{- end }} 74 | volumes: 75 | {{- if eq .TLSMode "system-store" }} 76 | - name: server-ca 77 | hostPath: 78 | path: /usr/local/share/ca-certificates/ca.crt 79 | type: File 80 | {{- end }} 81 | {{- if eq .AgentAuthMode "jwt" }} 82 | - name: jwt-token 83 | hostPath: 84 | path: {{.TokenPath}} 85 | type: File 86 | {{- end }}` 87 | ) 88 | 89 | var ( 90 | agentconfig config 91 | agentTemplate = template.Must(template.New("agentTemplate").Parse(agentTemplateText)) 92 | ) 93 | 94 | type config struct { 95 | Image string 96 | GatewayURL string 97 | GatewayCA string 98 | InsecureSkipVerify string 99 | TunnelID string 100 | LogLevel string 101 | HttpProxy string 102 | HttpsProxy string 103 | NoProxy string 104 | Token string 105 | TLSMode string 106 | TokenPath string 107 | AgentAuthMode string 108 | } 109 | 110 | // InitAgentConfig initializes the agent configuration by reading environment variables. 111 | // It returns an error if mandatory configurations are not set. 112 | func InitAgentConfig() error { 113 | agentconfig = config{} 114 | 115 | // Mandatory configs, return error if not set 116 | agentconfig.Image = os.Getenv("AGENT_IMAGE") 117 | if agentconfig.Image == "" { 118 | return fmt.Errorf("AGENT_IMAGE is not set") 119 | } 120 | 121 | gatewayURL := os.Getenv("GATEWAY_EXTERNAL_URL") 122 | if gatewayURL == "" { 123 | return fmt.Errorf("GATEWAY_EXTERNAL_URL is not set") 124 | } 125 | parsedURL, err := url.Parse(gatewayURL) 126 | if err != nil { 127 | return fmt.Errorf("GATEWAY_EXTERNAL_URL is invalid") 128 | } 129 | switch parsedURL.Scheme { 130 | case "http": 131 | parsedURL.Scheme = "ws" 132 | case "https": 133 | parsedURL.Scheme = "wss" 134 | case "ws", "wss": 135 | // Do nothing 136 | default: 137 | return fmt.Errorf("GATEWAY_EXTERNAL_URL has unsupported scheme") 138 | } 139 | parsedURL.Path = "/connect" 140 | agentconfig.GatewayURL = parsedURL.String() 141 | 142 | agentconfig.TokenPath = os.Getenv("AGENT_JWT_TOKEN_PATH") 143 | if agentconfig.TokenPath == "" { 144 | return fmt.Errorf("AGENT_JWT_TOKEN_PATH is not set") 145 | } 146 | 147 | // Optional configs, can be empty 148 | agentconfig.GatewayCA = os.Getenv("GATEWAY_CA") 149 | agentconfig.InsecureSkipVerify = getEnv("INSECURE_SKIP_VERIFY", "true") // TODO: enable by default 150 | agentconfig.LogLevel = getEnv("AGENT_LOG_LEVEL", "info") 151 | agentconfig.HttpProxy = os.Getenv("HTTP_PROXY") 152 | agentconfig.HttpsProxy = os.Getenv("HTTPS_PROXY") 153 | agentconfig.NoProxy = os.Getenv("NO_PROXY") 154 | agentconfig.TLSMode = getEnv("TLS_MODE", "strict") 155 | agentconfig.AgentAuthMode = getEnv("AGENT_AUTH_MODE", "token") 156 | 157 | return nil 158 | } 159 | 160 | // GenerateAgentConfig generates the connect-agent pod manifest in YAML for a given tunnel ID and token. 161 | // It returns the generated manifest as a string and any error encountered during template execution. 162 | func GenerateAgentConfig(tunnelId, token string) (string, error) { 163 | // Do not modify the original config 164 | config := agentconfig 165 | config.TunnelID = tunnelId 166 | config.Token = token 167 | 168 | buf := new(bytes.Buffer) 169 | err := agentTemplate.Execute(buf, &config) 170 | return buf.String(), err 171 | } 172 | 173 | func getEnv(key string, defaultValue string) string { 174 | value := os.Getenv(key) 175 | if value == "" { 176 | return defaultValue 177 | } 178 | return value 179 | } 180 | -------------------------------------------------------------------------------- /internal/agentconfig/agentconfig_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package agentconfig 5 | 6 | import ( 7 | "os" 8 | "testing" 9 | ) 10 | 11 | //nolint:errcheck 12 | func TestGenerateAgentConfig(t *testing.T) { 13 | expected := `apiVersion: v1 14 | kind: Pod 15 | metadata: 16 | name: connect-agent 17 | namespace: kube-system 18 | spec: 19 | containers: 20 | - name: connect-agent 21 | image: "connect-gateway:latest" 22 | command: [ "/connect-agent" ] 23 | args: 24 | - "--gateway-url=wss://connect-gateway.kind.internal/connect" 25 | - "--tunnel-id=test-tunnel-id" 26 | - "--auth-token=test-token" 27 | - "--insecure-skip-verify=true" 28 | - "--log-level=info" 29 | - "--token-path=/testpath" 30 | - "--tunnel-auth-mode=token" 31 | securityContext: 32 | allowPrivilegeEscalation: false 33 | capabilities: 34 | drop: 35 | - ALL 36 | readOnlyRootFilesystem: true 37 | seccompProfile: 38 | type: RuntimeDefault 39 | resources: 40 | limits: {} 41 | requests: 42 | cpu: 100m 43 | memory: 128Mi 44 | volumeMounts: 45 | volumes:` 46 | 47 | os.Setenv("AGENT_IMAGE", "connect-gateway:latest") 48 | os.Setenv("GATEWAY_EXTERNAL_URL", "https://connect-gateway.kind.internal") 49 | os.Setenv("AGENT_JWT_TOKEN_PATH", "/testpath") 50 | 51 | // Save original values 52 | originalHTTPProxy := os.Getenv("HTTP_PROXY") 53 | originalHTTPSProxy := os.Getenv("HTTPS_PROXY") 54 | originalNoProxy := os.Getenv("NO_PROXY") 55 | 56 | // Unset the variables 57 | os.Unsetenv("HTTP_PROXY") 58 | os.Unsetenv("HTTPS_PROXY") 59 | os.Unsetenv("NO_PROXY") 60 | 61 | defer func() { 62 | // Reset the variables to their original values 63 | os.Setenv("HTTP_PROXY", originalHTTPProxy) 64 | os.Setenv("HTTPS_PROXY", originalHTTPSProxy) 65 | os.Setenv("NO_PROXY", originalNoProxy) 66 | }() 67 | 68 | err := InitAgentConfig() 69 | if err != nil { 70 | t.Fatalf("InitAgentConfig() error = %v", err) 71 | } 72 | 73 | tunnelId := "test-tunnel-id" 74 | token := "test-token" 75 | configStr, err := GenerateAgentConfig(tunnelId, token) 76 | 77 | if err != nil { 78 | t.Fatalf("GenerateAgentConfig() error = %v", err) 79 | } 80 | 81 | if configStr != expected { 82 | t.Errorf("expected \n%s\ngot \n%s", expected, configStr) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /internal/auth/agent_token_authorizer.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package auth 5 | 6 | import ( 7 | "fmt" 8 | "net/http" 9 | 10 | "github.com/golang-jwt/jwt/v5" 11 | "github.com/golang-jwt/jwt/v5/request" 12 | "github.com/open-edge-platform/cluster-connect-gateway/internal/agent" 13 | ) 14 | 15 | func extractTunnelId(req *http.Request) (string, error) { 16 | id := req.Header.Get(agent.TunnelIdHeader) 17 | if id == "" { 18 | return id, fmt.Errorf("Empty header with tunnel id") 19 | } 20 | return id, nil 21 | } 22 | 23 | type Authenticator interface { 24 | ParseAndValidate(tokenString string) (jwt.Claims, error) 25 | } 26 | 27 | type JwtTokenAuthorizer struct { 28 | JwtAuth Authenticator 29 | } 30 | 31 | func (j *JwtTokenAuthorizer) Authorizer(req *http.Request) (clientKey string, authed bool, err error) { 32 | id, err := extractTunnelId(req) 33 | if err != nil { 34 | return id, false, err 35 | } 36 | 37 | token, err := request.BearerExtractor{}.ExtractToken(req) 38 | if err != nil { 39 | return id, false, err 40 | } 41 | 42 | _, err = j.JwtAuth.ParseAndValidate(token) 43 | 44 | if err != nil { 45 | return id, false, err 46 | } 47 | 48 | return id, true, nil 49 | } 50 | 51 | type SecretTokenAuthorizer struct { 52 | TokenManager TokenManager 53 | } 54 | 55 | func (s *SecretTokenAuthorizer) Authorizer(req *http.Request) (clientKey string, authed bool, err error) { 56 | id, err := extractTunnelId(req) 57 | if id == "" { 58 | return id, false, err 59 | } 60 | authToken := req.Header.Get(agent.TokenHeader) 61 | token, err := s.TokenManager.GetToken(req.Context(), id) 62 | if err != nil { 63 | return id, false, err 64 | } 65 | if token.Value == authToken { 66 | return id, true, nil 67 | } 68 | return id, false, nil 69 | } 70 | -------------------------------------------------------------------------------- /internal/auth/agent_token_authorizer_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package auth 5 | 6 | import ( 7 | "errors" 8 | "net/http" 9 | "testing" 10 | 11 | "github.com/golang-jwt/jwt/v5" 12 | "github.com/onsi/ginkgo/v2" 13 | "github.com/onsi/gomega" 14 | "github.com/open-edge-platform/cluster-connect-gateway/internal/agent" 15 | ) 16 | 17 | // MockAuthenticator is a mock implementation of the Authenticator interface 18 | type MockAuthenticator struct { 19 | ParseAndValidateFunc func(tokenString string) (jwt.Claims, error) 20 | } 21 | 22 | func (m *MockAuthenticator) ParseAndValidate(tokenString string) (jwt.Claims, error) { 23 | return m.ParseAndValidateFunc(tokenString) 24 | } 25 | 26 | var _ = ginkgo.Describe("Auth Package", func() { 27 | var ( 28 | req *http.Request 29 | jwtAuthorizer JwtTokenAuthorizer 30 | mockAuth *MockAuthenticator 31 | ) 32 | 33 | ginkgo.BeforeEach(func() { 34 | req = &http.Request{ 35 | Header: make(http.Header), 36 | } 37 | mockAuth = &MockAuthenticator{} 38 | jwtAuthorizer = JwtTokenAuthorizer{ 39 | JwtAuth: mockAuth, 40 | } 41 | }) 42 | 43 | ginkgo.Describe("extractTunnelId", func() { 44 | ginkgo.It("should return an error if the TunnelIdHeader is empty", func() { 45 | id, err := extractTunnelId(req) 46 | gomega.Expect(id).To(gomega.BeEmpty()) 47 | gomega.Expect(err).To(gomega.HaveOccurred()) 48 | }) 49 | 50 | ginkgo.It("should return the tunnel ID if the TunnelIdHeader is present", func() { 51 | expectedID := "test-tunnel-id" 52 | req.Header.Set(agent.TunnelIdHeader, expectedID) 53 | id, err := extractTunnelId(req) 54 | gomega.Expect(id).To(gomega.Equal(expectedID)) 55 | gomega.Expect(err).NotTo(gomega.HaveOccurred()) 56 | }) 57 | }) 58 | 59 | ginkgo.Describe("JwtTokenAuthorizer.Authorizer", func() { 60 | ginkgo.It("should return an error if the TunnelIdHeader is empty", func() { 61 | clientKey, authed, err := jwtAuthorizer.Authorizer(req) 62 | gomega.Expect(clientKey).To(gomega.BeEmpty()) 63 | gomega.Expect(authed).To(gomega.BeFalse()) 64 | gomega.Expect(err).To(gomega.HaveOccurred()) 65 | }) 66 | 67 | ginkgo.It("should return an error if the token is invalid", func() { 68 | req.Header.Set(agent.TunnelIdHeader, "test-tunnel-id") 69 | req.Header.Set("Authorization", "Bearer invalid-token") 70 | 71 | mockAuth.ParseAndValidateFunc = func(tokenString string) (jwt.Claims, error) { 72 | return nil, errors.New("invalid token") 73 | } 74 | 75 | clientKey, authed, err := jwtAuthorizer.Authorizer(req) 76 | gomega.Expect(clientKey).To(gomega.Equal("test-tunnel-id")) 77 | gomega.Expect(authed).To(gomega.BeFalse()) 78 | gomega.Expect(err).To(gomega.HaveOccurred()) 79 | }) 80 | 81 | ginkgo.It("should authorize a valid token", func() { 82 | req.Header.Set(agent.TunnelIdHeader, "test-tunnel-id") 83 | req.Header.Set("Authorization", "Bearer valid-token") 84 | 85 | mockAuth.ParseAndValidateFunc = func(tokenString string) (jwt.Claims, error) { 86 | return jwt.MapClaims{"foo": "bar"}, nil 87 | } 88 | 89 | clientKey, authed, err := jwtAuthorizer.Authorizer(req) 90 | gomega.Expect(clientKey).To(gomega.Equal("test-tunnel-id")) 91 | gomega.Expect(authed).To(gomega.BeTrue()) 92 | gomega.Expect(err).NotTo(gomega.HaveOccurred()) 93 | }) 94 | }) 95 | 96 | }) 97 | 98 | func TestAuth(t *testing.T) { 99 | gomega.RegisterFailHandler(ginkgo.Fail) 100 | ginkgo.RunSpecs(t, "Auth Suite") 101 | } 102 | -------------------------------------------------------------------------------- /internal/auth/mocks/token_manager_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.47.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | context "context" 7 | 8 | auth "github.com/open-edge-platform/cluster-connect-gateway/internal/auth" 9 | 10 | mock "github.com/stretchr/testify/mock" 11 | 12 | v1alpha1 "github.com/open-edge-platform/cluster-connect-gateway/api/v1alpha1" 13 | ) 14 | 15 | // MockTokenManager is an autogenerated mock type for the TokenManager type 16 | type MockTokenManager struct { 17 | mock.Mock 18 | } 19 | 20 | // CreateAndStoreToken provides a mock function with given fields: ctx, tunnelID, cc 21 | func (_m *MockTokenManager) CreateAndStoreToken(ctx context.Context, tunnelID string, cc *v1alpha1.ClusterConnect) error { 22 | ret := _m.Called(ctx, tunnelID, cc) 23 | 24 | if len(ret) == 0 { 25 | panic("no return value specified for CreateAndStoreToken") 26 | } 27 | 28 | var r0 error 29 | if rf, ok := ret.Get(0).(func(context.Context, string, *v1alpha1.ClusterConnect) error); ok { 30 | r0 = rf(ctx, tunnelID, cc) 31 | } else { 32 | r0 = ret.Error(0) 33 | } 34 | 35 | return r0 36 | } 37 | 38 | // DeleteToken provides a mock function with given fields: ctx, tunnelID 39 | func (_m *MockTokenManager) DeleteToken(ctx context.Context, tunnelID string) error { 40 | ret := _m.Called(ctx, tunnelID) 41 | 42 | if len(ret) == 0 { 43 | panic("no return value specified for DeleteToken") 44 | } 45 | 46 | var r0 error 47 | if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { 48 | r0 = rf(ctx, tunnelID) 49 | } else { 50 | r0 = ret.Error(0) 51 | } 52 | 53 | return r0 54 | } 55 | 56 | // GetToken provides a mock function with given fields: ctx, tunnelID 57 | func (_m *MockTokenManager) GetToken(ctx context.Context, tunnelID string) (*auth.Token, error) { 58 | ret := _m.Called(ctx, tunnelID) 59 | 60 | if len(ret) == 0 { 61 | panic("no return value specified for GetToken") 62 | } 63 | 64 | var r0 *auth.Token 65 | var r1 error 66 | if rf, ok := ret.Get(0).(func(context.Context, string) (*auth.Token, error)); ok { 67 | return rf(ctx, tunnelID) 68 | } 69 | if rf, ok := ret.Get(0).(func(context.Context, string) *auth.Token); ok { 70 | r0 = rf(ctx, tunnelID) 71 | } else { 72 | if ret.Get(0) != nil { 73 | r0 = ret.Get(0).(*auth.Token) 74 | } 75 | } 76 | 77 | if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { 78 | r1 = rf(ctx, tunnelID) 79 | } else { 80 | r1 = ret.Error(1) 81 | } 82 | 83 | return r0, r1 84 | } 85 | 86 | // TokenExist provides a mock function with given fields: ctx, tunnelID 87 | func (_m *MockTokenManager) TokenExist(ctx context.Context, tunnelID string) (bool, error) { 88 | ret := _m.Called(ctx, tunnelID) 89 | 90 | if len(ret) == 0 { 91 | panic("no return value specified for TokenExist") 92 | } 93 | 94 | var r0 bool 95 | var r1 error 96 | if rf, ok := ret.Get(0).(func(context.Context, string) (bool, error)); ok { 97 | return rf(ctx, tunnelID) 98 | } 99 | if rf, ok := ret.Get(0).(func(context.Context, string) bool); ok { 100 | r0 = rf(ctx, tunnelID) 101 | } else { 102 | r0 = ret.Get(0).(bool) 103 | } 104 | 105 | if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { 106 | r1 = rf(ctx, tunnelID) 107 | } else { 108 | r1 = ret.Error(1) 109 | } 110 | 111 | return r0, r1 112 | } 113 | 114 | // NewMockTokenManager creates a new instance of MockTokenManager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 115 | // The first argument is typically a *testing.T value. 116 | func NewMockTokenManager(t interface { 117 | mock.TestingT 118 | Cleanup(func()) 119 | }) *MockTokenManager { 120 | mock := &MockTokenManager{} 121 | mock.Mock.Test(t) 122 | 123 | t.Cleanup(func() { mock.AssertExpectations(t) }) 124 | 125 | return mock 126 | } 127 | -------------------------------------------------------------------------------- /internal/auth/secret_token_manager.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package auth 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "os" 10 | 11 | corev1 "k8s.io/api/core/v1" 12 | apierrors "k8s.io/apimachinery/pkg/api/errors" 13 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | "k8s.io/client-go/kubernetes" 15 | v1 "k8s.io/client-go/kubernetes/typed/core/v1" 16 | "k8s.io/client-go/rest" 17 | 18 | "github.com/open-edge-platform/cluster-connect-gateway/api/v1alpha1" 19 | ) 20 | 21 | const ( 22 | DefaultSecretNamespace = "connect-gateway-secrets" 23 | DefaultTokenLength = 54 24 | ) 25 | 26 | var GetClusterConfig = rest.InClusterConfig 27 | 28 | // NewInClusterSecretTokenManager creates a new TokenManager implementation 29 | // that uses local Kubernetes Secrets as a store for the token. 30 | func NewTokenManager() (TokenManager, error) { 31 | restconfig, err := GetClusterConfig() 32 | if err != nil { 33 | return nil, fmt.Errorf("failed to obtain in-cluster config %v", err) 34 | } 35 | 36 | clientset, err := kubernetes.NewForConfig(restconfig) 37 | if err != nil { 38 | return nil, fmt.Errorf("failed to create kubernetes client %v", err) 39 | } 40 | 41 | namespace := DefaultSecretNamespace 42 | ns, ok := os.LookupEnv("SECRET_NAMESPACE") 43 | if ok && ns != "" { 44 | namespace = ns 45 | } 46 | 47 | return &manager{ 48 | client: clientset.CoreV1().Secrets(namespace), 49 | namespace: namespace, 50 | }, nil 51 | } 52 | 53 | // manager is the implementation of TokenManager interface 54 | type manager struct { 55 | client v1.SecretInterface 56 | namespace string 57 | 58 | // TODO: Implement cache 59 | // cache sync.Map 60 | } 61 | 62 | // TokenExist returns true if the token secret for a given tunnel ID exists 63 | func (m *manager) TokenExist(ctx context.Context, tunnelID string) (bool, error) { 64 | if _, err := m.client.Get(ctx, getTokenSecretName(tunnelID), metav1.GetOptions{}); err != nil { 65 | if apierrors.IsNotFound(err) { 66 | return false, nil 67 | } else { 68 | return false, err 69 | } 70 | } 71 | return true, nil 72 | } 73 | 74 | // GetTokenSecretWithTunnelID returns the token value for a given tunnel ID. 75 | func (m *manager) GetToken(ctx context.Context, tunnelID string) (*Token, error) { 76 | // Find it from Cache. If not exists or expired, retrieve from K8s Secret. 77 | 78 | // Attempt to get secret 79 | secret, err := m.client.Get(ctx, getTokenSecretName(tunnelID), metav1.GetOptions{}) 80 | if err != nil { 81 | return nil, fmt.Errorf("failed to get token for %s (%v)", tunnelID, err) 82 | } 83 | 84 | return &Token{Value: string(secret.Data["token"])}, nil 85 | } 86 | 87 | // CreateAndStoreToken generates a token value and create a Secret with it for a given tunnel ID. 88 | func (m *manager) CreateAndStoreToken(ctx context.Context, tunnelID string, cc *v1alpha1.ClusterConnect) error { 89 | token, err := GenerateToken(DefaultTokenLength) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | secret := &corev1.Secret{ 95 | ObjectMeta: metav1.ObjectMeta{ 96 | Name: getTokenSecretName(tunnelID), 97 | Namespace: m.namespace, 98 | OwnerReferences: []metav1.OwnerReference{ 99 | { 100 | APIVersion: v1alpha1.GroupVersion.String(), 101 | Kind: v1alpha1.ClusterConnectKind, 102 | Name: cc.Name, 103 | UID: cc.UID, 104 | }, 105 | }, 106 | }, 107 | Data: map[string][]byte{ 108 | "token": []byte(token), 109 | // TODO: Add expiration 110 | }, 111 | } 112 | 113 | if _, err := m.client.Create(ctx, secret, metav1.CreateOptions{}); err != nil { 114 | if !apierrors.IsAlreadyExists(err) { 115 | return fmt.Errorf("failed to create token secret (%v)", err) 116 | } 117 | } 118 | 119 | return nil 120 | } 121 | 122 | // Delete token removes the token secret for a given tunnel ID. 123 | func (m *manager) DeleteToken(ctx context.Context, tunnelID string) error { 124 | if err := m.client.Delete(ctx, getTokenSecretName(tunnelID), metav1.DeleteOptions{}); err != nil { 125 | return fmt.Errorf("failed to delete token secret (%v)", err) 126 | } 127 | return nil 128 | } 129 | 130 | // GetTokenSecretName returns the token secret name for a given ClusterConnect object. 131 | func getTokenSecretName(tunnelId string) string { 132 | // TODO: need to think about a case where tunnel ID exceeds 247 characters 133 | // which makes the Secret name exceeds K8s resource name limit 253 134 | return tunnelId + "-agent-token" 135 | } 136 | -------------------------------------------------------------------------------- /internal/auth/secret_token_manager_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package auth 5 | 6 | import ( 7 | "context" 8 | "testing" 9 | 10 | corev1 "k8s.io/api/core/v1" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | "k8s.io/client-go/kubernetes/fake" 13 | 14 | "github.com/open-edge-platform/cluster-connect-gateway/api/v1alpha1" 15 | "github.com/stretchr/testify/assert" 16 | ) 17 | 18 | const testTunnelID = "test-tunnel-id" 19 | 20 | func TestGetToken(t *testing.T) { 21 | scrtName := getTokenSecretName(testTunnelID) 22 | 23 | fakeClient := fake.NewSimpleClientset(&corev1.Secret{ 24 | ObjectMeta: metav1.ObjectMeta{ 25 | Namespace: "test-ns", 26 | Name: scrtName, 27 | }, 28 | Data: map[string][]byte{ 29 | "token": []byte("mockToken"), 30 | }, 31 | }) 32 | 33 | tokenManager := &manager{client: fakeClient.CoreV1().Secrets("test-ns"), namespace: "test-ns"} 34 | token, err := tokenManager.GetToken(context.TODO(), testTunnelID) 35 | if err != nil { 36 | t.Errorf("Failed to get token: %v", err) 37 | } 38 | 39 | assert.Equal(t, token.Value, "mockToken", "Token value mismatch") 40 | } 41 | 42 | func TestCreateAndStoreToken(t *testing.T) { 43 | fakeClient := fake.NewSimpleClientset() 44 | tokenManager := &manager{client: fakeClient.CoreV1().Secrets("test-ns"), namespace: "test-ns"} 45 | 46 | cc := &v1alpha1.ClusterConnect{ 47 | ObjectMeta: metav1.ObjectMeta{ 48 | Name: testTunnelID, 49 | UID: "fake-uid", 50 | }, 51 | } 52 | 53 | err := tokenManager.CreateAndStoreToken(context.TODO(), testTunnelID, cc) 54 | if err != nil { 55 | t.Errorf("Failed to create and store token: %v", err) 56 | } 57 | 58 | secret, err := tokenManager.client.Get(context.TODO(), getTokenSecretName(testTunnelID), metav1.GetOptions{}) 59 | if err != nil { 60 | t.Errorf("Failed to get token: %v", err) 61 | } 62 | 63 | assert.NotEmpty(t, secret.GetOwnerReferences()) 64 | assert.Equal(t, string(secret.GetOwnerReferences()[0].UID), "fake-uid", "Token secret owner UID mismatch") 65 | assert.Equal(t, secret.GetOwnerReferences()[0].Name, testTunnelID, "Token secret owner name mismatch") 66 | } 67 | 68 | func TestTokenExist(t *testing.T) { 69 | scrtName := getTokenSecretName(testTunnelID) 70 | 71 | // Test the case that the token secret exists. 72 | fakeClientWithSecret := fake.NewSimpleClientset(&corev1.Secret{ 73 | ObjectMeta: metav1.ObjectMeta{ 74 | Namespace: "test-ns", 75 | Name: scrtName, 76 | }, 77 | Data: map[string][]byte{ 78 | "token": []byte("mockToken"), 79 | }, 80 | }) 81 | 82 | tokenManager := &manager{client: fakeClientWithSecret.CoreV1().Secrets("test-ns"), namespace: "test-ns"} 83 | exists, err := tokenManager.TokenExist(context.TODO(), testTunnelID) 84 | if err != nil { 85 | t.Errorf("Failed to check token existence: %v", err) 86 | } 87 | if !exists { 88 | t.Errorf("Token for tunnelID %s should exist but not found", testTunnelID) 89 | } 90 | 91 | // Test the case that the token does not exists. 92 | fakeClientWithoutSecret := fake.NewSimpleClientset() 93 | tokenManager = &manager{client: fakeClientWithoutSecret.CoreV1().Secrets("test-ns"), namespace: "test-ns"} 94 | exists, err = tokenManager.TokenExist(context.TODO(), testTunnelID) 95 | if err != nil { 96 | t.Errorf("Failed to check token existence: %v", err) 97 | } 98 | if exists { 99 | t.Errorf("Token for tunnelID %s should not exist but found", testTunnelID) 100 | } 101 | } 102 | 103 | func TestDeleteToken(t *testing.T) { 104 | scrtName := getTokenSecretName(testTunnelID) 105 | 106 | fakeClient := fake.NewSimpleClientset(&corev1.Secret{ 107 | ObjectMeta: metav1.ObjectMeta{ 108 | Namespace: "test-ns", 109 | Name: scrtName, 110 | }, 111 | Data: map[string][]byte{ 112 | "token": []byte("mockToken"), 113 | }, 114 | }) 115 | 116 | tokenManager := &manager{client: fakeClient.CoreV1().Secrets("test-ns"), namespace: "test-ns"} 117 | 118 | err := tokenManager.DeleteToken(context.TODO(), testTunnelID) 119 | if err != nil { 120 | t.Errorf("Failed to delete token: %v", err) 121 | } 122 | 123 | _, err = tokenManager.client.Get(context.TODO(), getTokenSecretName(testTunnelID), metav1.GetOptions{}) 124 | if err == nil { 125 | t.Errorf("Get secret should fail but succeed") 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /internal/auth/token_manager.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package auth 5 | 6 | import ( 7 | "context" 8 | "crypto/rand" 9 | "encoding/hex" 10 | 11 | "github.com/open-edge-platform/cluster-connect-gateway/api/v1alpha1" 12 | ) 13 | 14 | // TokenManager interface defines methods for retrieving, storing, and deleting tokens. 15 | // 16 | //go:generate mockery --name TokenManager --filename token_manager_mock.go --structname MockTokenManager --output ./mocks 17 | type TokenManager interface { 18 | GetToken(ctx context.Context, tunnelID string) (*Token, error) // GetToken retrieves token for a given tunnel ID. 19 | TokenExist(ctx context.Context, tunnelID string) (bool, error) // TokenExist returns true if the token for a given tunnel ID alreay exists. 20 | CreateAndStoreToken(ctx context.Context, tunnelID string /* , tokenTTLHours int, */, cc *v1alpha1.ClusterConnect) error // CreateToken creates and stores a token with its value and TTL in hours. 21 | DeleteToken(ctx context.Context, tunnelID string) error // DeleteToken deletes a token for a given tunnel ID. 22 | // RefreshToken(ctx context.Context, tunnelID string, tokenTTLHours int) error // TODO: implement 23 | } 24 | 25 | // Token struct represents a token with its value, updated time, and time-to-live in hours. 26 | type Token struct { 27 | Value string 28 | 29 | // TODO: Add expiration 30 | // Expire time.Time 31 | } 32 | 33 | // GenerateToken generates a random string to be used as a token for authenticating the connect-agent. 34 | func GenerateToken(size int) (string, error) { 35 | token := make([]byte, size) 36 | 37 | _, err := rand.Read(token) 38 | if err != nil { 39 | return "", err 40 | } 41 | 42 | return hex.EncodeToString(token), err 43 | } 44 | -------------------------------------------------------------------------------- /internal/auth/token_manager_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | // SPDX-License-Identifier: Apache-2.0 3 | package auth_test 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | 10 | "github.com/open-edge-platform/cluster-connect-gateway/internal/auth" 11 | ) 12 | 13 | func TestGenerateToken(t *testing.T) { 14 | size := 8 // Example size for testing 15 | token, err := auth.GenerateToken(size) 16 | assert.NoError(t, err) 17 | assert.Equal(t, size*2, len(token)) // Token length in hex encoding 18 | } 19 | -------------------------------------------------------------------------------- /internal/controller/conditions.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package controller 5 | 6 | import ( 7 | v1alpha1 "github.com/open-edge-platform/cluster-connect-gateway/api/v1alpha1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | v1beta2conditions "sigs.k8s.io/cluster-api/util/conditions/v1beta2" 10 | ) 11 | 12 | // requiredConditionTypes lists the conditions that must be met for the 13 | // ClusterConnect status to be considered Ready. 14 | var requiredConditionTypes = []string{ 15 | v1alpha1.AuthTokenReadyCondition, 16 | v1alpha1.AgentManifestGeneratedCondition, 17 | v1alpha1.ControlPlaneEndpointSetCondition, 18 | v1alpha1.ClusterSpecUpdatedCondition, 19 | v1alpha1.TopologyReconciledCondition, 20 | v1alpha1.ConnectionProbeCondition, 21 | } 22 | 23 | // initConditions initializes conditions with Unknown if conditions are not set. 24 | func initConditions(cc *v1alpha1.ClusterConnect) { 25 | if len(cc.GetConditions()) == 0 { 26 | for _, condition := range requiredConditionTypes { 27 | // Skip ClusterSpecUpdatedCondition and TopologyReconciledCondition if ClusterRef is not set. 28 | if cc.Spec.ClusterRef == nil { 29 | if condition == v1alpha1.ClusterSpecUpdatedCondition || 30 | condition == v1alpha1.TopologyReconciledCondition { 31 | continue 32 | } 33 | } 34 | 35 | // Set condition to Unknown otherwise. 36 | v1beta2conditions.Set(cc, metav1.Condition{ 37 | Type: condition, 38 | Status: metav1.ConditionUnknown, 39 | Reason: v1alpha1.ReadyUnknownReason, 40 | }) 41 | } 42 | } 43 | } 44 | 45 | func setAuthTokenReadyConditionTrue(cc *v1alpha1.ClusterConnect, message ...string) { 46 | conditionMessage := "" 47 | if len(message) > 0 { 48 | conditionMessage = message[0] 49 | } 50 | v1beta2conditions.Set(cc, metav1.Condition{ 51 | Type: v1alpha1.AuthTokenReadyCondition, 52 | Status: metav1.ConditionTrue, 53 | Reason: v1alpha1.ReadyReason, 54 | Message: conditionMessage, 55 | }) 56 | } 57 | 58 | func setAuthTokenReadyConditionFalse(cc *v1alpha1.ClusterConnect, message ...string) { 59 | conditionMessage := "" 60 | if len(message) > 0 { 61 | conditionMessage = message[0] 62 | } 63 | v1beta2conditions.Set(cc, metav1.Condition{ 64 | Type: v1alpha1.AuthTokenReadyCondition, 65 | Status: metav1.ConditionFalse, 66 | Reason: v1alpha1.NotReadyReason, 67 | Message: conditionMessage, 68 | }) 69 | } 70 | 71 | func setAgentManifestGeneratedConditionTrue(cc *v1alpha1.ClusterConnect, message ...string) { 72 | conditionMessage := "" 73 | if len(message) > 0 { 74 | conditionMessage = message[0] 75 | } 76 | v1beta2conditions.Set(cc, metav1.Condition{ 77 | Type: v1alpha1.AgentManifestGeneratedCondition, 78 | Status: metav1.ConditionTrue, 79 | Reason: v1alpha1.ReadyReason, 80 | Message: conditionMessage, 81 | }) 82 | } 83 | 84 | func setAgentManifestGeneratedConditionFalse(cc *v1alpha1.ClusterConnect, message ...string) { 85 | conditionMessage := "" 86 | if len(message) > 0 { 87 | conditionMessage = message[0] 88 | } 89 | v1beta2conditions.Set(cc, metav1.Condition{ 90 | Type: v1alpha1.AgentManifestGeneratedCondition, 91 | Status: metav1.ConditionFalse, 92 | Reason: v1alpha1.NotReadyReason, 93 | Message: conditionMessage, 94 | }) 95 | } 96 | 97 | func setControlPlaneEndpointSetConditionTrue(cc *v1alpha1.ClusterConnect, message ...string) { 98 | conditionMessage := "" 99 | if len(message) > 0 { 100 | conditionMessage = message[0] 101 | } 102 | v1beta2conditions.Set(cc, metav1.Condition{ 103 | Type: v1alpha1.ControlPlaneEndpointSetCondition, 104 | Status: metav1.ConditionTrue, 105 | Reason: v1alpha1.ReadyReason, 106 | Message: conditionMessage, 107 | }) 108 | } 109 | 110 | func setClusterSpecReayConditionTrue(cc *v1alpha1.ClusterConnect, message ...string) { 111 | conditionMessage := "" 112 | if len(message) > 0 { 113 | conditionMessage = message[0] 114 | } 115 | v1beta2conditions.Set(cc, metav1.Condition{ 116 | Type: v1alpha1.ClusterSpecUpdatedCondition, 117 | Status: metav1.ConditionTrue, 118 | Reason: v1alpha1.ReadyReason, 119 | Message: conditionMessage, 120 | }) 121 | } 122 | 123 | func setClusterSpecUpdatedConditionFalse(cc *v1alpha1.ClusterConnect, message ...string) { 124 | conditionMessage := "" 125 | if len(message) > 0 { 126 | conditionMessage = message[0] 127 | } 128 | v1beta2conditions.Set(cc, metav1.Condition{ 129 | Type: v1alpha1.ClusterSpecUpdatedCondition, 130 | Status: metav1.ConditionFalse, 131 | Reason: v1alpha1.NotReadyReason, 132 | Message: conditionMessage, 133 | }) 134 | } 135 | 136 | func setTopologyReconciledConditionTrue(cc *v1alpha1.ClusterConnect, message ...string) { 137 | conditionMessage := "" 138 | if len(message) > 0 { 139 | conditionMessage = message[0] 140 | } 141 | v1beta2conditions.Set(cc, metav1.Condition{ 142 | Type: v1alpha1.TopologyReconciledCondition, 143 | Status: metav1.ConditionTrue, 144 | Reason: v1alpha1.ReadyReason, 145 | Message: conditionMessage, 146 | }) 147 | } 148 | 149 | func setTopologyReconciledConditionFalse(cc *v1alpha1.ClusterConnect, message ...string) { 150 | conditionMessage := "" 151 | if len(message) > 0 { 152 | conditionMessage = message[0] 153 | } 154 | v1beta2conditions.Set(cc, metav1.Condition{ 155 | Type: v1alpha1.TopologyReconciledCondition, 156 | Status: metav1.ConditionFalse, 157 | Reason: v1alpha1.NotReadyReason, 158 | Message: conditionMessage, 159 | }) 160 | } 161 | 162 | func setKubeconfigReadyConditionTrue(cc *v1alpha1.ClusterConnect, message ...string) { 163 | conditionMessage := "" 164 | if len(message) > 0 { 165 | conditionMessage = message[0] 166 | } 167 | v1beta2conditions.Set(cc, metav1.Condition{ 168 | Type: v1alpha1.KubeconfigReadyCondition, 169 | Status: metav1.ConditionTrue, 170 | Reason: v1alpha1.ReadyReason, 171 | Message: conditionMessage, 172 | }) 173 | } 174 | 175 | func setConnectionProbeConditionTrue(cc *v1alpha1.ClusterConnect, message ...string) { 176 | conditionMessage := "" 177 | if len(message) > 0 { 178 | conditionMessage = message[0] 179 | } 180 | v1beta2conditions.Set(cc, metav1.Condition{ 181 | Type: v1alpha1.ConnectionProbeCondition, 182 | Status: metav1.ConditionTrue, 183 | Reason: v1alpha1.ConnectionProbeSucceededReason, 184 | Message: conditionMessage, 185 | }) 186 | } 187 | func setConnectionProbeConditionFalse(cc *v1alpha1.ClusterConnect, message ...string) { 188 | conditionMessage := "" 189 | if len(message) > 0 { 190 | conditionMessage = message[0] 191 | } 192 | v1beta2conditions.Set(cc, metav1.Condition{ 193 | Type: v1alpha1.ConnectionProbeCondition, 194 | Status: metav1.ConditionFalse, 195 | Reason: v1alpha1.ConnectionProbeFailedReason, 196 | Message: conditionMessage, 197 | }) 198 | } 199 | -------------------------------------------------------------------------------- /internal/controller/suite_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package controller 5 | 6 | import ( 7 | "context" 8 | "os" 9 | "path/filepath" 10 | "testing" 11 | "time" 12 | 13 | . "github.com/onsi/ginkgo/v2" 14 | . "github.com/onsi/gomega" 15 | 16 | "k8s.io/client-go/kubernetes/scheme" 17 | "k8s.io/client-go/rest" 18 | clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 19 | ctrl "sigs.k8s.io/controller-runtime" 20 | "sigs.k8s.io/controller-runtime/pkg/client" 21 | "sigs.k8s.io/controller-runtime/pkg/envtest" 22 | logf "sigs.k8s.io/controller-runtime/pkg/log" 23 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 24 | metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" 25 | 26 | v1alpha1 "github.com/open-edge-platform/cluster-connect-gateway/api/v1alpha1" 27 | "github.com/open-edge-platform/cluster-connect-gateway/internal/agentconfig" 28 | "github.com/open-edge-platform/cluster-connect-gateway/internal/auth" 29 | // +kubebuilder:scaffold:imports 30 | ) 31 | 32 | // These tests use Ginkgo (BDD-style Go testing framework). Refer to 33 | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. 34 | 35 | var ( 36 | ctx context.Context 37 | cancel context.CancelFunc 38 | testEnv *envtest.Environment 39 | cfg *rest.Config 40 | k8sClient client.Client 41 | ) 42 | 43 | func TestControllers(t *testing.T) { 44 | RegisterFailHandler(Fail) 45 | 46 | RunSpecs(t, "Controller Suite") 47 | } 48 | 49 | var _ = BeforeSuite(func() { 50 | logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) 51 | 52 | ctx, cancel = context.WithCancel(context.TODO()) 53 | 54 | var err error 55 | 56 | err = v1alpha1.AddToScheme(scheme.Scheme) 57 | Expect(err).NotTo(HaveOccurred()) 58 | err = clusterv1.AddToScheme(scheme.Scheme) 59 | Expect(err).NotTo(HaveOccurred()) 60 | 61 | // +kubebuilder:scaffold:scheme 62 | 63 | By("bootstrapping test environment") 64 | testEnv = &envtest.Environment{ 65 | CRDDirectoryPaths: []string{ 66 | filepath.Join("..", "..", "config", "crd", "bases"), 67 | filepath.Join("..", "..", "config", "crd", "deps"), 68 | }, 69 | ErrorIfCRDPathMissing: true, 70 | } 71 | 72 | // Retrieve the first found binary directory to allow running tests from IDEs 73 | if getFirstFoundEnvTestBinaryDir() != "" { 74 | testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir() 75 | } 76 | 77 | // cfg is defined in this file globally. 78 | cfg, err = testEnv.Start() 79 | Expect(err).NotTo(HaveOccurred()) 80 | Expect(cfg).NotTo(BeNil()) 81 | 82 | k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) 83 | Expect(err).NotTo(HaveOccurred()) 84 | Expect(k8sClient).NotTo(BeNil()) 85 | 86 | k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ 87 | Scheme: scheme.Scheme, 88 | Metrics: metricsserver.Options{BindAddress: "0"}, 89 | }) 90 | Expect(err).ToNot(HaveOccurred()) 91 | 92 | Expect(os.Setenv("SECRET_NAMESPACE", "default")).To(Succeed()) 93 | Expect(os.Setenv("AGENT_IMAGE", "connect-agent:latest")).To(Succeed()) 94 | Expect(os.Setenv("GATEWAY_EXTERNAL_URL", "https://connect-gateway.fake.com:443")).To(Succeed()) 95 | Expect(os.Setenv("GATEWAY_INTERNAL_URL", "http://connect-gateway.default.svc:8080")).To(Succeed()) 96 | Expect(os.Setenv("AGENT_JWT_TOKEN_PATH", "/testpath")).To(Succeed()) 97 | 98 | err = agentconfig.InitAgentConfig() 99 | Expect(err).ToNot(HaveOccurred()) 100 | 101 | auth.GetClusterConfig = func() (*rest.Config, error) { 102 | return cfg, nil 103 | } 104 | 105 | err = (&ClusterConnectReconciler{ 106 | Client: k8sManager.GetClient(), 107 | Scheme: k8sManager.GetScheme(), 108 | }).SetupWithManager(ctx, k8sManager, 1*time.Second) 109 | Expect(err).ToNot(HaveOccurred()) 110 | 111 | go func() { 112 | defer GinkgoRecover() 113 | err = k8sManager.Start(ctx) 114 | Expect(err).ToNot(HaveOccurred(), "failed to run manager") 115 | }() 116 | }) 117 | 118 | var _ = AfterSuite(func() { 119 | By("tearing down the test environment") 120 | cancel() 121 | err := testEnv.Stop() 122 | Expect(err).NotTo(HaveOccurred()) 123 | }) 124 | 125 | // getFirstFoundEnvTestBinaryDir locates the first binary in the specified path. 126 | // ENVTEST-based tests depend on specific binaries, usually located in paths set by 127 | // controller-runtime. When running tests directly (e.g., via an IDE) without using 128 | // Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured. 129 | // 130 | // This function streamlines the process by finding the required binaries, similar to 131 | // setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are 132 | // properly set up, run 'make setup-envtest' beforehand. 133 | func getFirstFoundEnvTestBinaryDir() string { 134 | basePath := filepath.Join("..", "..", "bin", "k8s") 135 | entries, err := os.ReadDir(basePath) 136 | if err != nil { 137 | logf.Log.Error(err, "Failed to read directory", "path", basePath) 138 | return "" 139 | } 140 | for _, entry := range entries { 141 | if entry.IsDir() { 142 | return filepath.Join(basePath, entry.Name()) 143 | } 144 | } 145 | return "" 146 | } 147 | -------------------------------------------------------------------------------- /internal/metrics/metrics.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package metrics 5 | 6 | import ( 7 | "github.com/prometheus/client_golang/prometheus" 8 | ) 9 | 10 | var ( 11 | ConnectionCounter = prometheus.NewCounterVec( 12 | prometheus.CounterOpts{ 13 | Name: "websocket_connections_total", 14 | Help: "Total number of WebSocket connections, partitioned by status.", 15 | }, 16 | []string{"status"}, 17 | ) 18 | RequestLatency = prometheus.NewHistogram(prometheus.HistogramOpts{ 19 | Name: "request_latency_seconds", 20 | Help: "Request latency of /kubernetes endpoint in seconds", 21 | Buckets: prometheus.DefBuckets, 22 | }) 23 | KubeconfigRetrievalDuration = prometheus.NewHistogram(prometheus.HistogramOpts{ 24 | Namespace: "kubernetes", 25 | Subsystem: "secret", 26 | Name: "kubeconfig_retrieval_duration_seconds", 27 | Help: "Duration in seconds to retrieve kubeconfig from Kubernetes secret", 28 | Buckets: prometheus.DefBuckets, 29 | }) 30 | ProxiedHttpResponseCounter = prometheus.NewCounterVec( 31 | prometheus.CounterOpts{ 32 | Name: "proxied_http_response_codes", 33 | Help: "Count of HTTP response codes for proxied requests", 34 | }, 35 | []string{"code"}, 36 | ) 37 | ) 38 | 39 | func init() { 40 | prometheus.MustRegister(ConnectionCounter) 41 | prometheus.MustRegister(RequestLatency) //TODO: refactor 42 | prometheus.MustRegister(KubeconfigRetrievalDuration) 43 | prometheus.MustRegister(ProxiedHttpResponseCounter) 44 | } 45 | -------------------------------------------------------------------------------- /internal/middleware/middleware.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package middleware 5 | 6 | import ( 7 | "bytes" 8 | "encoding/json" 9 | "errors" 10 | "fmt" 11 | "net/http" 12 | "strings" 13 | 14 | "github.com/atomix/dazl" 15 | _ "github.com/atomix/dazl/zap" 16 | "github.com/golang-jwt/jwt/v5" 17 | "github.com/golang-jwt/jwt/v5/request" 18 | "github.com/open-edge-platform/orch-library/go/pkg/openpolicyagent" 19 | "github.com/rancher/remotedialer" 20 | ) 21 | 22 | var log = dazl.GetPackageLogger() 23 | 24 | type JwtAuthenticator interface { 25 | ParseAndValidate(string) (jwt.Claims, error) 26 | } 27 | 28 | type JwtAuthorization struct { 29 | JwtAuthenticator JwtAuthenticator 30 | OpaClient openpolicyagent.ClientWithResponsesInterface 31 | RbacEnabled bool 32 | } 33 | 34 | func extractTunnelId(req *http.Request) (string, error) { 35 | segments := strings.Split(req.URL.Path, "/") 36 | if len(segments) >= 3 && segments[1] == "kubernetes" && segments[2] != "" { 37 | return segments[2], nil 38 | } 39 | return "", errors.New("invalid path format") 40 | } 41 | 42 | // The tunnelId string contains a UUID followed by a hyphen and a cluster name. 43 | // Parse out the UUID and return it. 44 | func extractProjectIdFromTunnel(tunnelId string) (string, error) { 45 | segments := strings.Split(tunnelId, "-") 46 | if len(segments) < 6 { 47 | return "", errors.New("invalid tunnel ID format") 48 | } 49 | projectId := strings.Join(segments[0:5], "-") 50 | return projectId, nil 51 | } 52 | 53 | func (ja *JwtAuthorization) checkOpaPolicies(req *http.Request, claims jwt.Claims) error { 54 | tunnelId, err := extractTunnelId(req) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | projectId, err := extractProjectIdFromTunnel(tunnelId) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | if claims, ok := claims.(jwt.MapClaims); ok { 65 | claimsMap := map[string]interface{}(claims) 66 | claimsMap["project_id"] = projectId 67 | 68 | inputJSON, err := json.Marshal(openpolicyagent.OpaInput{Input: claimsMap}) 69 | if err != nil { 70 | return err 71 | } 72 | bodyReader := bytes.NewReader(inputJSON) 73 | resp, err := ja.OpaClient.PostV1DataPackageRuleWithBodyWithResponse(req.Context(), "rbac", "allow", &openpolicyagent.PostV1DataPackageRuleParams{}, "application/json", bodyReader) 74 | if err != nil { 75 | return err 76 | } 77 | // API reference: 78 | // https://github.com/open-edge-platform/orch-library/blob/main/go/pkg/openpolicyagent/openapi.yaml 79 | // Here we will always use "Result1" since the "allow" rule from "accessproxy" package 80 | // will always a boolean value. 81 | allowed, err := resp.JSON200.Result.AsOpaResponseResult1() 82 | if err != nil { 83 | return fmt.Errorf("unable to evaluate policy %w", err) 84 | } 85 | 86 | if allowed { 87 | return nil 88 | } else { 89 | return fmt.Errorf("access denied") 90 | } 91 | } 92 | return fmt.Errorf("Invalid JWT token") 93 | } 94 | 95 | func (ja *JwtAuthorization) AuthMiddleware(next http.Handler) http.Handler { 96 | return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 97 | token, err := request.BearerExtractor{}.ExtractToken(req) 98 | if err != nil { 99 | log.Infow("Unauthorized") 100 | rw.WriteHeader(http.StatusUnauthorized) 101 | if _, err := rw.Write([]byte(err.Error())); err != nil { 102 | log.Errorw("Failed to write response", dazl.Error(err)) 103 | } 104 | return 105 | } 106 | claims, err := ja.JwtAuthenticator.ParseAndValidate(token) 107 | if err != nil { 108 | remotedialer.DefaultErrorWriter(rw, req, http.StatusUnauthorized, err) 109 | log.Infow("Unauthorized", dazl.Error(err)) 110 | return 111 | } 112 | if ja.RbacEnabled { 113 | err = ja.checkOpaPolicies(req, claims) 114 | if err != nil { 115 | log.Infow("Unauthorized", dazl.Error(err)) 116 | remotedialer.DefaultErrorWriter(rw, req, http.StatusUnauthorized, err) 117 | return 118 | } 119 | } 120 | 121 | next.ServeHTTP(rw, req) 122 | }) 123 | } 124 | 125 | // SizeLimitMiddleware returns a middleware function that limits request body size 126 | // The limit parameter specifies the maximum allowed size in bytes. 127 | func SizeLimitMiddleware(limit int64) func(http.Handler) http.Handler { 128 | return func(next http.Handler) http.Handler { 129 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 130 | // Limit the size of the request body to the specified limit 131 | r.Body = http.MaxBytesReader(w, r.Body, limit) 132 | 133 | // Call the next handler, which can be another middleware in the chain, or the final handler. 134 | next.ServeHTTP(w, r) 135 | }) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /internal/middleware/middleware_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package middleware 5 | 6 | import ( 7 | "errors" 8 | "net/http" 9 | "net/http/httptest" 10 | "testing" 11 | 12 | "github.com/golang-jwt/jwt/v5" 13 | . "github.com/onsi/ginkgo/v2" 14 | . "github.com/onsi/gomega" 15 | ) 16 | 17 | var _ = Describe("JwtAuthorization", func() { 18 | var ( 19 | jwtAuth *JwtAuthorization 20 | ) 21 | 22 | const ( 23 | projectId = "d60b7a96-6e85-457b-a0af-dead9234074e" 24 | tunnelId = projectId + "-clustername-abcdef" 25 | ) 26 | 27 | BeforeEach(func() { 28 | jwtAuth = &JwtAuthorization{ 29 | JwtAuthenticator: &mockJwtAuthenticator{}, 30 | } 31 | }) 32 | 33 | Describe("extractTunnelId", func() { 34 | It("should extract tunnel ID from a valid path", func() { 35 | req := httptest.NewRequest(http.MethodGet, "/kubernetes/"+tunnelId, nil) 36 | id, err := extractTunnelId(req) 37 | Expect(err).ToNot(HaveOccurred()) 38 | Expect(id).To(Equal(tunnelId)) 39 | }) 40 | 41 | It("should return an error for an invalid path", func() { 42 | req := httptest.NewRequest(http.MethodGet, "/invalid/path", nil) 43 | _, err := extractTunnelId(req) 44 | Expect(err).To(HaveOccurred()) 45 | }) 46 | }) 47 | 48 | Describe("extractProjectIdFromTunnel", func() { 49 | It("should extract project ID from a valid tunnel ID", func() { 50 | id, err := extractProjectIdFromTunnel(tunnelId) 51 | Expect(err).ToNot(HaveOccurred()) 52 | Expect(id).To(Equal(projectId)) 53 | }) 54 | 55 | It("should return an error for an invalid tunnel ID", func() { 56 | _, err := extractProjectIdFromTunnel("invalidtunnelid") 57 | Expect(err).To(HaveOccurred()) 58 | }) 59 | }) 60 | 61 | Describe("AuthMiddleware", func() { 62 | It("should return unauthorized for invalid token", func() { 63 | req := httptest.NewRequest(http.MethodGet, "/kubernetes/"+tunnelId, nil) 64 | req.Header.Set("Authorization", "Bearer invalid-token") 65 | rr := httptest.NewRecorder() 66 | 67 | handler := jwtAuth.AuthMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 68 | w.WriteHeader(http.StatusOK) 69 | })) 70 | 71 | handler.ServeHTTP(rr, req) 72 | 73 | Expect(rr.Code).To(Equal(http.StatusUnauthorized)) 74 | }) 75 | }) 76 | }) 77 | 78 | // mockJwtAuthenticator is a mock implementation of the JwtAuthenticator interface 79 | type mockJwtAuthenticator struct{} 80 | 81 | func (m *mockJwtAuthenticator) ParseAndValidate(token string) (jwt.Claims, error) { 82 | if token == "valid-token" { 83 | return jwt.MapClaims{"sub": "1234567890"}, nil 84 | } 85 | return nil, errors.New("invalid token") 86 | } 87 | func TestServer(t *testing.T) { 88 | RegisterFailHandler(Fail) 89 | RunSpecs(t, "Server Suite") 90 | } 91 | -------------------------------------------------------------------------------- /internal/opa/opa.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package opa 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/atomix/dazl" 10 | "github.com/open-edge-platform/orch-library/go/pkg/openpolicyagent" 11 | ) 12 | 13 | var log = dazl.GetPackageLogger() 14 | 15 | type OpaConfig struct { 16 | OpaAddress string 17 | OpaPort int 18 | } 19 | 20 | func NewOPAClient(opaConfig OpaConfig) openpolicyagent.ClientWithResponsesInterface { 21 | opaServerAddr := fmt.Sprintf("%s:%d", opaConfig.OpaAddress, opaConfig.OpaPort) 22 | log.Infow("OPA is enabled, creating an OPA client", dazl.String("OPA server addr", opaServerAddr)) 23 | 24 | opaClient, err := openpolicyagent.NewClientWithResponses(opaServerAddr) 25 | if err != nil { 26 | log.Fatalw("OPA client cannot be created", dazl.Error(err)) 27 | return nil 28 | } 29 | return opaClient 30 | } 31 | -------------------------------------------------------------------------------- /internal/provider/provider_manager.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package provider 5 | 6 | // ProviderManager is an interface that defines methods for managing CAPI control plane providers. 7 | // 8 | //go:generate mockery --name ProviderManager --filename provider_manager_mock.go --structname MockProviderManager --output ./mocks 9 | type ProviderManager interface { 10 | // Register adds a static pod manifest path for a specific kind to the manager. 11 | Register(kind string, path string) 12 | StaticPodManifestPath(kind string) string 13 | } 14 | 15 | // ProviderManagerBuilder is a builder for ProviderManager. 16 | type ProviderManagerBuilder struct { 17 | manager *manager 18 | } 19 | 20 | // NewProviderManagerBuilder creates a new builder for ProviderManager. 21 | func NewProviderManager() *ProviderManagerBuilder { 22 | return &ProviderManagerBuilder{ 23 | manager: &manager{ 24 | pathMap: make(map[string]string), 25 | }, 26 | } 27 | } 28 | 29 | // WithInjectStaticPodManifest adds an static pod manifest path for a specific kind to the manager. 30 | func (b *ProviderManagerBuilder) WithProvider(kind string, path string) *ProviderManagerBuilder { 31 | b.manager.pathMap[kind] = path 32 | return b 33 | } 34 | 35 | // Build returns the constructed ProviderManager. 36 | func (b *ProviderManagerBuilder) Build() ProviderManager { 37 | return b.manager 38 | } 39 | 40 | type manager struct { 41 | pathMap map[string]string 42 | } 43 | 44 | func (m *manager) Register(kind string, path string) { 45 | m.pathMap[kind] = path 46 | } 47 | 48 | func (m *manager) StaticPodManifestPath(kind string) string { 49 | path, ok := m.pathMap[kind] 50 | if !ok { 51 | return "" 52 | } 53 | 54 | return path 55 | } 56 | -------------------------------------------------------------------------------- /internal/server/error_responder.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package server 5 | 6 | import ( 7 | "fmt" 8 | "net/http" 9 | 10 | "github.com/open-edge-platform/cluster-connect-gateway/internal/metrics" 11 | ) 12 | 13 | type errorResponder struct { 14 | } 15 | 16 | func (e *errorResponder) Error(w http.ResponseWriter, req *http.Request, err error) { 17 | log.Debugf("Error response: %v", err) 18 | 19 | code := http.StatusInternalServerError 20 | label := fmt.Sprintf("%d", code) 21 | metrics.ProxiedHttpResponseCounter.WithLabelValues(label).Inc() 22 | 23 | w.WriteHeader(code) // nolint: errcheck 24 | w.Write([]byte(err.Error())) // nolint: errcheck 25 | } 26 | -------------------------------------------------------------------------------- /internal/server/server_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package server 6 | 7 | import ( 8 | "testing" 9 | 10 | . "github.com/onsi/ginkgo/v2" 11 | . "github.com/onsi/gomega" 12 | ) 13 | 14 | func TestServer(t *testing.T) { 15 | RegisterFailHandler(Fail) 16 | RunSpecs(t, "Server Suite") 17 | } 18 | 19 | var _ = Describe("Server", func() { 20 | //var ( 21 | // testServer *Server 22 | // addr string 23 | //) 24 | 25 | //BeforeEach(func() { 26 | // addr = "127.0.0.1:8123" 27 | // cfg = &config.Config{K8sUrl: "https://kubernetes.default.svc", EnableAuth: false} 28 | //}) 29 | 30 | //Describe("New Server", func() { 31 | // Context("When a server is created", func() { 32 | // It("Should create server successfully", func() { 33 | // testServer = NewServer(addr, cfg) 34 | // Expect(testServer).ToNot(BeNil()) 35 | // Expect(testServer.addr).To(Equal(addr)) 36 | // }) 37 | // }) 38 | //}) 39 | }) 40 | -------------------------------------------------------------------------------- /internal/utils/certutil/cert.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package certutil 5 | 6 | import ( 7 | "crypto/tls" 8 | "crypto/x509" 9 | "encoding/pem" 10 | "errors" 11 | "fmt" 12 | "os" 13 | 14 | "github.com/sirupsen/logrus" 15 | kerrors "k8s.io/apimachinery/pkg/util/errors" 16 | ) 17 | 18 | var ( 19 | CertFilePath = "/etc/secrets/ca/cert" // #nosec G101 20 | ) 21 | 22 | func GetTLSConfigs(insecure bool) *tls.Config { 23 | if insecure { 24 | return &tls.Config{ 25 | InsecureSkipVerify: true, // #nosec G402 26 | } 27 | } else if _, err := os.Stat(CertFilePath); err == nil { 28 | info, err := os.Lstat(CertFilePath) 29 | if err == nil && info.Mode()&os.ModeSymlink == os.ModeSymlink { 30 | errMsg := errors.New("Cert file is a symlink") 31 | logrus.Error(errMsg) 32 | panic(errMsg) 33 | } 34 | caCertPEM, err := os.ReadFile(CertFilePath) 35 | if err != nil { 36 | errMsg := fmt.Errorf("Failed to read cert file: %v", err) 37 | logrus.Error(errMsg) 38 | panic(errMsg) 39 | } 40 | logrus.Infof("Validating CA at %s", CertFilePath) 41 | if err := ValidateCert(caCertPEM); err != nil { 42 | errMsg := fmt.Errorf("%s", "Failed to validate certificate") 43 | logrus.Error(errMsg) 44 | panic(errMsg) 45 | } 46 | 47 | certPool, err := x509.SystemCertPool() 48 | if err != nil { 49 | logrus.Errorf("Failed to configure certPool: %v", err) 50 | os.Exit(1) 51 | } 52 | 53 | ok := certPool.AppendCertsFromPEM(caCertPEM) 54 | if !ok { 55 | logrus.Errorf("Failed to parse root certificate") 56 | os.Exit(1) 57 | } 58 | 59 | return &tls.Config{ 60 | RootCAs: certPool, 61 | MinVersion: tls.VersionTLS13, 62 | MaxVersion: tls.VersionTLS13, 63 | } 64 | } 65 | return &tls.Config{ 66 | MinVersion: tls.VersionTLS13, 67 | MaxVersion: tls.VersionTLS13, 68 | } 69 | } 70 | 71 | func ValidateCert(caPEM []byte) error { 72 | var blocks [][]byte 73 | for { 74 | var certDERBlock *pem.Block 75 | certDERBlock, caPEM = pem.Decode(caPEM) 76 | if certDERBlock == nil { 77 | break 78 | } 79 | if certDERBlock.Type == "CERTIFICATE" { 80 | blocks = append(blocks, certDERBlock.Bytes) 81 | } 82 | } 83 | 84 | logrus.Infof("Found %d certificates", len(blocks)) 85 | if len(blocks) == 0 { 86 | return fmt.Errorf("No valid certificates found") 87 | } else if len(blocks) > 1 { 88 | logrus.Warnf("Found %d certificates, should be 1", len(blocks)) 89 | } 90 | 91 | blockcount := 0 92 | errs := []error{} 93 | for _, block := range blocks { 94 | _, err := x509.ParseCertificate(block) 95 | if err != nil { 96 | logrus.Error(err) 97 | errs = append(errs, err) 98 | continue 99 | } 100 | logrus.Infof("Certificate #%d", blockcount) 101 | blockcount = blockcount + 1 102 | } 103 | 104 | return kerrors.NewAggregate(errs) 105 | } 106 | -------------------------------------------------------------------------------- /internal/utils/certutil/cert_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package certutil 5 | 6 | import ( 7 | "bytes" 8 | "crypto/rand" 9 | "crypto/rsa" 10 | "crypto/x509" 11 | "encoding/pem" 12 | "math/big" 13 | "os" 14 | "path/filepath" 15 | "testing" 16 | 17 | "github.com/stretchr/testify/assert" 18 | ) 19 | 20 | func testGenCert() ([]byte, error) { 21 | priv, err := rsa.GenerateKey(rand.Reader, 2048) 22 | if err != nil { 23 | return nil, err 24 | } 25 | template := &x509.Certificate{ 26 | SerialNumber: new(big.Int).SetInt64(0), 27 | } 28 | 29 | raw, err := x509.CreateCertificate(rand.Reader, template, template, &priv.PublicKey, priv) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | cert := make([]byte, 0) 35 | buf := bytes.NewBuffer(cert) 36 | 37 | err = pem.Encode(buf, &pem.Block{Type: "CERTIFICATE", Bytes: raw}) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | return buf.Bytes(), nil 43 | } 44 | 45 | func TestGetTLSConfigs_Insecure(t *testing.T) { 46 | tls := GetTLSConfigs(true) 47 | assert.NotNil(t, tls) 48 | } 49 | 50 | func TestGetTLSConfigs_Secure_FailedGetCertFilePath(t *testing.T) { 51 | tls := GetTLSConfigs(false) 52 | assert.NotNil(t, tls) 53 | } 54 | 55 | func TestGetTLSConfigs_Secure_FailedWrongCert(t *testing.T) { 56 | cert := []byte("test") 57 | 58 | origCertFilePath := CertFilePath 59 | CertFilePath = filepath.Join(t.TempDir(), "cert") 60 | defer func() { 61 | // need to be reverted back to original value for other tests 62 | CertFilePath = origCertFilePath 63 | }() 64 | 65 | err := os.WriteFile(CertFilePath, cert, 0600) 66 | assert.NoError(t, err) 67 | 68 | defer func() { 69 | if r := recover(); r == nil { 70 | t.Error("call agent.Run succeed - expected: failed") 71 | } 72 | }() 73 | _ = GetTLSConfigs(false) 74 | 75 | } 76 | 77 | func TestGetTLSConfigs_Secure(t *testing.T) { 78 | cert, err := testGenCert() 79 | assert.NoError(t, err) 80 | 81 | origCertFilePath := CertFilePath 82 | CertFilePath = filepath.Join(t.TempDir(), "cert") 83 | defer func() { 84 | // need to be reverted back to original value for other tests 85 | CertFilePath = origCertFilePath 86 | }() 87 | 88 | err = os.WriteFile(CertFilePath, cert, 0600) 89 | assert.NoError(t, err) 90 | 91 | tls := GetTLSConfigs(false) 92 | 93 | assert.NotNil(t, tls) 94 | } 95 | 96 | func TestValidateCert_NilCaPEM(t *testing.T) { 97 | err := ValidateCert(nil) 98 | assert.NotNil(t, err) 99 | } 100 | -------------------------------------------------------------------------------- /internal/utils/kubeutil/kubeconfig.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package kubeutil 5 | 6 | import ( 7 | "context" 8 | "crypto" 9 | "crypto/x509" 10 | "os" 11 | 12 | "github.com/pkg/errors" 13 | corev1 "k8s.io/api/core/v1" 14 | "k8s.io/client-go/tools/clientcmd" 15 | "k8s.io/client-go/tools/clientcmd/api" 16 | "sigs.k8s.io/cluster-api/util/certs" 17 | "sigs.k8s.io/controller-runtime/pkg/client" 18 | ) 19 | 20 | const ( 21 | // ClusterCA is the secret name suffix for APIServer CA. 22 | ClusterCA = "ca" 23 | 24 | // ClientClusterCA is the secret name suffix for APIServer CA. 25 | ClientClusterCA = "cca" 26 | 27 | // TLSKeyDataName is the key used to store a TLS private key in the secret's data field. 28 | TLSKeyDataName = "tls.key" 29 | 30 | // TLSCrtDataName is the key used to store a TLS certificate in the secret's data field. 31 | TLSCrtDataName = "tls.crt" 32 | 33 | KubeconfigDataName = "value" 34 | 35 | ApiServerCA = "apiServerCA" 36 | ) 37 | 38 | const ( 39 | privateCASecNameEnv = "PRIVATE_CA_SECRET_NAME" 40 | privateCASecNamespaceEnv = "PRIVATE_CA_SECRET_NAMESPACE" 41 | ) 42 | 43 | func GenerateKubeconfig(ctx context.Context, c client.Client, clusterName, clusterNamespace, server string) ([]byte, error) { 44 | serverCA, err := getCertSecret(ctx, c, clusterName, clusterNamespace, ClusterCA) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | clientClusterCA, err := getCertSecret(ctx, c, clusterName, clusterNamespace, ClientClusterCA) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | clientCACert, err := certs.DecodeCertPEM(clientClusterCA.Data[TLSCrtDataName]) 55 | if err != nil { 56 | return nil, errors.Wrap(err, "failed to decode CA Cert") 57 | } else if clientCACert == nil { 58 | return nil, errors.New("certificate not found in config") 59 | } 60 | 61 | clientCAKey, err := certs.DecodePrivateKeyPEM(clientClusterCA.Data[TLSKeyDataName]) 62 | if err != nil { 63 | return nil, errors.Wrap(err, "failed to decode private key") 64 | } else if clientCAKey == nil { 65 | return nil, errors.New("CA private key not found") 66 | } 67 | 68 | serverCACert, err := certs.DecodeCertPEM(serverCA.Data[TLSCrtDataName]) 69 | if err != nil { 70 | return nil, errors.Wrap(err, "failed to decode CA Cert") 71 | } else if serverCACert == nil { 72 | return nil, errors.New("certificate not found in config") 73 | } 74 | 75 | cfg, err := newKubeconfig(clusterName, server, clientCACert, clientCAKey, serverCACert) 76 | if err != nil { 77 | return nil, errors.Wrap(err, "failed to generate a kubeconfig") 78 | } 79 | 80 | out, err := clientcmd.Write(*cfg) 81 | if err != nil { 82 | return nil, errors.Wrap(err, "failed to serialize config to yaml") 83 | } 84 | 85 | return out, nil 86 | } 87 | 88 | func GetAPIServerCA(ctx context.Context, c client.Client) ([]byte, error) { 89 | 90 | privateCASecretName := os.Getenv(privateCASecNameEnv) 91 | privateCASecretNamespace := os.Getenv(privateCASecNamespaceEnv) 92 | if privateCASecretName == "" || privateCASecretNamespace == "" { 93 | return nil, errors.New("private CA secret name or namespace not set") 94 | } 95 | 96 | secret := &corev1.Secret{} 97 | secretKey := client.ObjectKey{Namespace: privateCASecretNamespace, Name: privateCASecretName} 98 | 99 | if err := c.Get(ctx, secretKey, secret); err != nil { 100 | return nil, err 101 | } 102 | 103 | caCrt, exists := secret.Data["ca.crt"] 104 | if !exists { 105 | caCrt, exists = secret.Data["tls.crt"] 106 | if !exists { 107 | return nil, errors.New("neither ca.crt nor tls.crt found in secret") 108 | } 109 | } 110 | 111 | return caCrt, nil 112 | } 113 | 114 | func newKubeconfig(clusterName, server string, clientCACert *x509.Certificate, clientCAKey crypto.Signer, serverCACert *x509.Certificate) (*api.Config, error) { 115 | cfg := &certs.Config{ 116 | CommonName: "kubernetes-admin", 117 | Organization: []string{"system:masters"}, 118 | Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, 119 | } 120 | 121 | clientKey, err := certs.NewPrivateKey() 122 | if err != nil { 123 | return nil, errors.Wrap(err, "unable to create private key") 124 | } 125 | 126 | clientCert, err := cfg.NewSignedCert(clientKey, clientCACert, clientCAKey) 127 | if err != nil { 128 | return nil, errors.Wrap(err, "unable to sign certificate") 129 | } 130 | 131 | userName := clusterName + "-admin" 132 | contextName := userName + "@" + clusterName 133 | 134 | return &api.Config{ 135 | Clusters: map[string]*api.Cluster{ 136 | clusterName: { 137 | Server: server, 138 | CertificateAuthorityData: certs.EncodeCertPEM(serverCACert), 139 | }, 140 | }, 141 | Contexts: map[string]*api.Context{ 142 | contextName: { 143 | Cluster: clusterName, 144 | AuthInfo: userName, 145 | }, 146 | }, 147 | AuthInfos: map[string]*api.AuthInfo{ 148 | userName: { 149 | ClientKeyData: certs.EncodePrivateKeyPEM(clientKey), 150 | ClientCertificateData: certs.EncodeCertPEM(clientCert), 151 | }, 152 | }, 153 | CurrentContext: contextName, 154 | }, nil 155 | } 156 | 157 | func getCertSecret(ctx context.Context, c client.Client, clusterName, clusterNamespace, purpose string) (*corev1.Secret, error) { 158 | secret := &corev1.Secret{} 159 | secretKey := client.ObjectKey{Namespace: clusterNamespace, Name: clusterName + "-" + purpose} 160 | 161 | if err := c.Get(ctx, secretKey, secret); err != nil { 162 | return nil, err 163 | } 164 | 165 | return secret, nil 166 | } 167 | -------------------------------------------------------------------------------- /logging.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # SPDX-License-Identifier: Apache-2.0 3 | encoders: 4 | json: 5 | fields: 6 | - message 7 | - level: 8 | format: uppercase 9 | - caller: 10 | format: short 11 | - timestamp: 12 | format: iso8601 13 | 14 | writers: 15 | stdout: 16 | encoder: json 17 | 18 | rootLogger: 19 | level: info 20 | outputs: 21 | - stdout 22 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | # license checking 6 | python-debian==0.1.44 7 | reuse~=1.0.0 8 | 9 | # lint yaml 10 | yamllint~=1.27.1 -------------------------------------------------------------------------------- /test/e2e/deployment_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package e2e 5 | 6 | import ( 7 | "os/exec" 8 | "time" 9 | 10 | . "github.com/onsi/ginkgo/v2" 11 | . "github.com/onsi/gomega" 12 | 13 | "github.com/open-edge-platform/cluster-connect-gateway/test/utils" 14 | ) 15 | 16 | var _ = Describe("Manager", Ordered, func() { 17 | var controllerPodName string 18 | 19 | SetDefaultEventuallyTimeout(2 * time.Minute) 20 | SetDefaultEventuallyPollingInterval(time.Second) 21 | 22 | Context("Manager", func() { 23 | It("should run successfully", func() { 24 | By("validating that the connect-controller pod is running as expected") 25 | verifyControllerUp := func(g Gomega) { 26 | // Get the name of the controller-manager pod 27 | cmd := exec.Command("kubectl", "get", 28 | "pods", "-l", "app.kubernetes.io/component=controller", 29 | "-o", "go-template={{ range .items }}"+ 30 | "{{ if not .metadata.deletionTimestamp }}"+ 31 | "{{ .metadata.name }}"+ 32 | "{{ \"\\n\" }}{{ end }}{{ end }}", 33 | "-n", namespace, 34 | ) 35 | 36 | podOutput, err := utils.Run(cmd) 37 | g.Expect(err).NotTo(HaveOccurred(), "Failed to retrieve connect-controller pod information") 38 | podNames := utils.GetNonEmptyLines(podOutput) 39 | g.Expect(podNames).To(HaveLen(1), "expected 1 controller pod running") 40 | controllerPodName = podNames[0] 41 | g.Expect(controllerPodName).To(ContainSubstring("controller")) 42 | 43 | // Validate the pod's status 44 | cmd = exec.Command("kubectl", "get", 45 | "pods", controllerPodName, "-o", "jsonpath={.status.phase}", 46 | "-n", namespace, 47 | ) 48 | output, err := utils.Run(cmd) 49 | g.Expect(err).NotTo(HaveOccurred()) 50 | g.Expect(output).To(Equal("Running"), "Incorrect connect-controller pod status") 51 | } 52 | Eventually(verifyControllerUp).Should(Succeed()) 53 | 54 | By("validating that the connect-gateway pod is running as expected") 55 | verifyGatewayUp := func(g Gomega) { 56 | // Get the name of the controller-manager pod 57 | cmd := exec.Command("kubectl", "get", 58 | "pods", "-l", "app.kubernetes.io/component=gateway", 59 | "-o", "go-template={{ range .items }}"+ 60 | "{{ if not .metadata.deletionTimestamp }}"+ 61 | "{{ .metadata.name }}"+ 62 | "{{ \"\\n\" }}{{ end }}{{ end }}", 63 | "-n", namespace, 64 | ) 65 | 66 | podOutput, err := utils.Run(cmd) 67 | g.Expect(err).NotTo(HaveOccurred(), "Failed to retrieve controller-manager pod information") 68 | podNames := utils.GetNonEmptyLines(podOutput) 69 | g.Expect(podNames).To(HaveLen(1), "expected 1 controller pod running") 70 | controllerPodName = podNames[0] 71 | g.Expect(controllerPodName).To(ContainSubstring("gateway")) 72 | 73 | // Validate the pod's status 74 | cmd = exec.Command("kubectl", "get", 75 | "pods", controllerPodName, "-o", "jsonpath={.status.phase}", 76 | "-n", namespace, 77 | ) 78 | output, err := utils.Run(cmd) 79 | g.Expect(err).NotTo(HaveOccurred()) 80 | g.Expect(output).To(Equal("Running"), "Incorrect connect-gateway pod status") 81 | } 82 | Eventually(verifyGatewayUp).Should(Succeed()) 83 | }) 84 | }) 85 | //Context("Connect Agent", Ordered, func() { 86 | // It("should connect successfully", func() { 87 | // By("deploying agent manifest from ClusterConnect") 88 | // var agentManifestOutput string 89 | // Eventually(func() error { 90 | // cmdGetClusterConnect := exec.Command("kubectl", "get", "clusterconnect", "capd-rke2-test", "-o", "jsonpath={.status.agentManifest}") 91 | // var err error 92 | // agentManifestOutput, err = utils.Run(cmdGetClusterConnect) 93 | // return err 94 | // }).Should(Succeed(), "Failed to retrieve cluster connect agent pod manifest from ClusterConnect") 95 | 96 | // cmdDeploy := exec.Command("kubectl", "apply", "-f", "-") 97 | // cmdDeploy.Stdin = strings.NewReader(agentManifestOutput) 98 | // output, err := utils.Run(cmdDeploy) 99 | // Expect(err).NotTo(HaveOccurred(), "Failed to deploy cluster connect agent pod manifest from ClusterConnect") 100 | // Expect(output).To(Equal("pod/connect-agent created\n"), "Failed to deploy cluster connect agent pod manifest from ClusterConnect") 101 | 102 | // By("validating that agent is not restarting") 103 | // podName := "connect-agent" 104 | // cmd := exec.Command("kubectl", "get", "pod", podName, "-n", "kube-system", "-o", "go-template={{range .status.containerStatuses}}{{if eq .name \""+podName+"\"}}{{.restartCount}}{{end}}{{end}}") 105 | // restartCount, err := utils.Run(cmd) 106 | // Expect(err).NotTo(HaveOccurred(), "Failed to get cluster connect agent pod restart count") 107 | // if restartCount != "0" { 108 | // Fail("is restarting") 109 | // } 110 | // }) 111 | //}) 112 | }) 113 | -------------------------------------------------------------------------------- /test/e2e/e2e_suite_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package e2e 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "os/exec" 10 | "testing" 11 | "time" 12 | 13 | . "github.com/onsi/ginkgo/v2" 14 | . "github.com/onsi/gomega" 15 | 16 | "github.com/open-edge-platform/cluster-connect-gateway/test/utils" 17 | ) 18 | 19 | var ( 20 | // Optional Environment Variables: 21 | skipKindCleanup = os.Getenv("SKIP_KIND_CLEANUP") == "true" 22 | skipCertManagerInstall = os.Getenv("CERT_MANAGER_INSTALL_SKIP") == "true" 23 | skipClusterAPIInstall = os.Getenv("CLUSTER_API_INSTALL_SKIP") == "true" 24 | 25 | isCertManagerAlreadyInstalled = false 26 | isClusterAPIOperatorAlreadyInstalled = false 27 | isClusterAPIProviderAlreadyInstalled = false 28 | 29 | namespace = os.Getenv("NAMESPACE") 30 | ) 31 | 32 | const ( 33 | // timeout and interval for Gomega Eventually 34 | timeout = time.Second * 120 35 | interval = time.Second * 1 36 | 37 | // namespace where the project is deployed in 38 | 39 | testDataPath = "test/resources/testdata/" 40 | ) 41 | 42 | // TestE2E runs the end-to-end (e2e) test suite for the project. These tests execute in an isolated, 43 | // temporary environment to validate project changes with the the purposed to be used in CI jobs. 44 | // The default setup requires Kind, builds/loads the Manager Docker image locally, and installs 45 | // CertManager and Cluster API operator and providers. 46 | func TestE2E(t *testing.T) { 47 | RegisterFailHandler(Fail) 48 | _, _ = fmt.Fprintf(GinkgoWriter, "Starting cluster-connect-gateway integration test suite\n") 49 | RunSpecs(t, "e2e suite") 50 | } 51 | 52 | var _ = BeforeSuite(func() { 53 | By("building the manager image") 54 | cmd := exec.Command("make", "docker-build") 55 | _, err := utils.Run(cmd) 56 | ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to build the images") 57 | 58 | By("loading the manager image on Kind") 59 | cmd = exec.Command("make", "docker-load") 60 | _, err = utils.Run(cmd) 61 | ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to load the images into Kind") 62 | 63 | // The tests-e2e are intended to run on a temporary cluster that is created and destroyed for testing. 64 | // To prevent errors when tests run in environments with CertManager and Cluster API already installed, 65 | // we check for their presence before execution. 66 | // Setup CertManager and Cluster API before the suite if not skipped and if not already installed 67 | if !skipCertManagerInstall { 68 | By("checking if cert manager is installed already") 69 | isCertManagerAlreadyInstalled = utils.IsCertManagerCRDsInstalled() 70 | if !isCertManagerAlreadyInstalled { 71 | _, _ = fmt.Fprintf(GinkgoWriter, "Installing CertManager...\n") 72 | Expect(utils.InstallCertManager()).To(Succeed(), "Failed to install CertManager") 73 | } else { 74 | _, _ = fmt.Fprintf(GinkgoWriter, "WARNING: CertManager is already installed. Skipping installation...\n") 75 | } 76 | } 77 | 78 | // Setup Cluster API before the suite if not skipped and if not already installed 79 | if !skipClusterAPIInstall { 80 | By("checking if cluster api operator is installed already") 81 | isClusterAPIOperatorAlreadyInstalled = utils.IsClusterAPIOperatorCRDsInstalled() 82 | if !isClusterAPIOperatorAlreadyInstalled { 83 | _, _ = fmt.Fprintf(GinkgoWriter, "Installing Cluster API Operator...\n") 84 | Eventually(utils.InstallClusterAPIOperator(), timeout, interval).Should(Succeed(), "Failed to install Cluster API Operator") 85 | } else { 86 | _, _ = fmt.Fprintf(GinkgoWriter, "WARNING: Cluster API Operator is already installed. Skipping installation...\n") 87 | } 88 | 89 | By("checking if cluster api provider is installed already") 90 | isClusterAPIProviderAlreadyInstalled = utils.IsClusterAPIProviderCRDsInstalled() 91 | if !isClusterAPIProviderAlreadyInstalled { 92 | _, _ = fmt.Fprintf(GinkgoWriter, "Installing Cluster API Provider...\n") 93 | Eventually(utils.InstallClusterAPIProvider(), timeout, interval).Should(Succeed(), "Failed to install Cluster API provider") 94 | } else { 95 | _, _ = fmt.Fprintf(GinkgoWriter, "WARNING: Cluster API Provider is already installed. Skipping installation...\n") 96 | } 97 | } 98 | 99 | By("installing cluster-connect-gateway helm charts") 100 | Expect(utils.InstallEdgeConnectGateway(namespace)).To(Succeed(), "Failed to install Edge Connect Gateway") 101 | 102 | By("creating namespace for test resources") 103 | cmd = exec.Command("kubectl", "apply", "-f", testDataPath+"namespace.yaml") 104 | _, err = utils.Run(cmd) 105 | Expect(err).NotTo(HaveOccurred(), "Failed to create test namespace") 106 | }) 107 | 108 | var _ = AfterSuite(func() { 109 | // Teardown CertManager after the suite if not skipped and if they were not already installed 110 | if !skipKindCleanup && !skipCertManagerInstall && !isCertManagerAlreadyInstalled { 111 | _, _ = fmt.Fprintf(GinkgoWriter, "Uninstalling CertManager...\n") 112 | utils.UninstallCertManager() 113 | } 114 | 115 | // Teardown Cluster API operator and providers after the suite if not skipped and if they were not already installed 116 | if !skipKindCleanup && !skipClusterAPIInstall && !isClusterAPIOperatorAlreadyInstalled { 117 | _, _ = fmt.Fprintf(GinkgoWriter, "Uninstalling Cluster API Operator...\n") 118 | utils.UninstallClusterAPIOperator() 119 | } 120 | if !skipKindCleanup && !skipClusterAPIInstall && !isClusterAPIProviderAlreadyInstalled { 121 | _, _ = fmt.Fprintf(GinkgoWriter, "Uninstalling Cluster API Provider...\n") 122 | utils.UninstallClusterAPIProvider() 123 | } 124 | 125 | // Teardown the helm chart. 126 | if !skipKindCleanup { 127 | By("uninstalling cluster-connect-gateway helm charts") 128 | cmd := exec.Command("make", "helm-uninstall") 129 | _, _ = utils.Run(cmd) 130 | } 131 | }) 132 | -------------------------------------------------------------------------------- /test/kind-config.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | kind: Cluster 6 | apiVersion: kind.x-k8s.io/v1alpha4 7 | nodes: 8 | - role: control-plane 9 | extraMounts: 10 | - hostPath: /var/run/docker.sock 11 | containerPath: /var/run/docker.sock 12 | -------------------------------------------------------------------------------- /test/local-e2e.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 4 | # 5 | # SPDX-License-Identifier: Apache-2.0 6 | 7 | source "$(dirname "$0")/setup.sh" 8 | 9 | export KIND_CLUSTER="${KIND_CLUSTER:-"local-e2e"}" 10 | export KIND_K8S_VERSION="${KIND_K8S_VERSION:-"v1.31.0"}" 11 | export NAMESPACE="${NAMESPACE:-"e2e-test"}" 12 | 13 | export PROMETHEUS_INSTALL_SKIP=true 14 | export CERT_MANAGER_INSTALL_SKIP=false 15 | export CLUSTER_API_INSTALL_SKIP=false 16 | 17 | install_kind 18 | create_cluster ${KIND_K8S_VERSION} 19 | if [ -z "${SKIP_KIND_CLEANUP:-}" ]; then 20 | trap delete_cluster EXIT 21 | fi 22 | 23 | kubeconfig=$(cd "$(dirname "$0")" && pwd)/kubeconfig 24 | kind export kubeconfig --kubeconfig $kubeconfig --name $KIND_CLUSTER 25 | 26 | KUBECONFIG=$kubeconfig test_e2e -v -ginkgo.v 27 | -------------------------------------------------------------------------------- /test/resources/capiproviders/core-provider.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | apiVersion: v1 6 | kind: Namespace 7 | metadata: 8 | labels: 9 | clusterctl.cluster.x-k8s.io/core: capi-operator 10 | control-plane: controller-manager 11 | name: capi-system 12 | --- 13 | apiVersion: operator.cluster.x-k8s.io/v1alpha2 14 | kind: CoreProvider 15 | metadata: 16 | name: cluster-api 17 | namespace: capi-system 18 | spec: 19 | version: v1.9.4 20 | -------------------------------------------------------------------------------- /test/resources/capiproviders/docker-infra-provider.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | apiVersion: v1 6 | kind: Namespace 7 | metadata: 8 | labels: 9 | clusterctl.cluster.x-k8s.io/core: capi-operator 10 | control-plane: controller-manager 11 | name: capd-system 12 | --- 13 | apiVersion: operator.cluster.x-k8s.io/v1alpha2 14 | kind: InfrastructureProvider 15 | metadata: 16 | name: docker 17 | namespace: capd-system 18 | spec: 19 | version: v1.9.4 20 | -------------------------------------------------------------------------------- /test/resources/capiproviders/rke2-provider.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | apiVersion: v1 6 | kind: Namespace 7 | metadata: 8 | labels: 9 | clusterctl.cluster.x-k8s.io/core: capi-operator 10 | control-plane: controller-manager 11 | name: capr-system 12 | --- 13 | apiVersion: operator.cluster.x-k8s.io/v1alpha2 14 | kind: ControlPlaneProvider 15 | metadata: 16 | name: rke2 17 | namespace: capr-system 18 | spec: 19 | version: v0.11.0 20 | --- 21 | apiVersion: operator.cluster.x-k8s.io/v1alpha2 22 | kind: BootstrapProvider 23 | metadata: 24 | name: rke2 25 | namespace: capr-system 26 | spec: 27 | version: v0.11.0 28 | -------------------------------------------------------------------------------- /test/resources/testdata/namespace.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | apiVersion: v1 6 | kind: Namespace 7 | metadata: 8 | name: e2e-test 9 | -------------------------------------------------------------------------------- /test/resources/testdata/test-cluster-connect.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | apiVersion: cluster.edge-orchestrator.intel.com/v1alpha1 6 | kind: ClusterConnect 7 | metadata: 8 | name: capd-rke2-test 9 | spec: 10 | serverCertRef: 11 | name: rke2-controlplane-webhook-service-cert 12 | namespace: capr-system 13 | clientCertRef: 14 | name: rke2-controlplane-webhook-service-cert 15 | namespace: capr-system 16 | -------------------------------------------------------------------------------- /test/resources/testdata/test-cluster-controlplane-rke2.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | apiVersion: cluster.x-k8s.io/v1beta1 6 | kind: Cluster 7 | metadata: 8 | name: capd-rke2-test 9 | namespace: e2e-test 10 | spec: 11 | controlPlaneRef: 12 | apiVersion: controlplane.cluster.x-k8s.io/v1beta1 13 | kind: RKE2ControlPlane 14 | name: capd-rke2-test 15 | infrastructureRef: 16 | apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 17 | kind: DockerCluster 18 | name: capd-rke2-test 19 | --- 20 | apiVersion: controlplane.cluster.x-k8s.io/v1beta1 21 | kind: RKE2ControlPlane 22 | metadata: 23 | name: capd-rke2-test 24 | namespace: e2e-test 25 | spec: 26 | files: 27 | - content: | 28 | #!/bin/bash 29 | set -e 30 | echo "Hello, World!" 31 | path: /etc/hello-world.sh 32 | owner: root:root 33 | infrastructureRef: 34 | apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 35 | kind: DockerMachineTemplate 36 | name: capd-rke2-test 37 | replicas: 1 38 | rolloutStrategy: 39 | rollingUpdate: 40 | maxSurge: 1 41 | type: RollingUpdate 42 | version: v1.31.3+rke2r1 43 | -------------------------------------------------------------------------------- /test/resources/testdata/test-cluster-infra-docker.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 6 | kind: DockerCluster 7 | metadata: 8 | name: capd-rke2-test 9 | namespace: e2e-test 10 | spec: 11 | loadBalancer: 12 | customHAProxyConfigTemplateRef: 13 | name: capd-rke2-test 14 | --- 15 | apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 16 | kind: DockerMachineTemplate 17 | metadata: 18 | name: capd-rke2-test 19 | namespace: e2e-test 20 | spec: 21 | template: 22 | spec: 23 | bootstrapTimeout: 15m 24 | --- 25 | apiVersion: v1 26 | kind: ConfigMap 27 | metadata: 28 | name: capd-rke2-test 29 | namespace: e2e-test 30 | data: 31 | value: |- 32 | # generated by kind 33 | global 34 | log /dev/log local0 35 | log /dev/log local1 notice 36 | daemon 37 | # limit memory usage to approximately 18 MB 38 | # (see https://github.com/kubernetes-sigs/kind/pull/3115) 39 | maxconn 100000 40 | 41 | resolvers docker 42 | nameserver dns 127.0.0.11:53 43 | 44 | defaults 45 | log global 46 | mode tcp 47 | option dontlognull 48 | # TODO: tune these 49 | timeout connect 5000 50 | timeout client 50000 51 | timeout server 50000 52 | # allow to boot despite dns don't resolve backends 53 | default-server init-addr none 54 | 55 | frontend stats 56 | bind *:8404 57 | stats enable 58 | stats uri / 59 | stats refresh 10s 60 | 61 | frontend control-plane 62 | bind *:{{ .FrontendControlPlanePort }} 63 | {{ if .IPv6 -}} 64 | bind :::{{ .FrontendControlPlanePort }}; 65 | {{- end }} 66 | default_backend kube-apiservers 67 | 68 | backend kube-apiservers 69 | option httpchk GET /healthz 70 | http-check expect status 401 71 | # TODO: we should be verifying (!) 72 | {{range $server, $address := .BackendServers}} 73 | server {{ $server }} {{ JoinHostPort $address $.BackendControlPlanePort }} check check-ssl verify none resolvers docker resolve-prefer {{ if $.IPv6 -}} ipv6 {{- else -}} ipv4 {{- end }} 74 | {{- end}} 75 | 76 | frontend rke2-join 77 | bind *:9345 78 | {{ if .IPv6 -}} 79 | bind :::9345; 80 | {{- end }} 81 | default_backend rke2-servers 82 | 83 | backend rke2-servers 84 | option httpchk GET /v1-rke2/readyz 85 | http-check expect status 403 86 | {{range $server, $address := .BackendServers}} 87 | server {{ $server }} {{ $address }}:9345 check check-ssl verify none 88 | {{- end}} 89 | -------------------------------------------------------------------------------- /test/setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 4 | # 5 | # SPDX-License-Identifier: Apache-2.0 6 | 7 | # Creates a named kind cluster given a k8s version. 8 | # The KIND_CLUSTER variable defines the cluster name and 9 | # is expected to be defined in the calling environment. 10 | # 11 | # Usage: 12 | # 13 | # export KIND_CLUSTER= 14 | # create_cluster 15 | function create_cluster { 16 | echo "Getting kind config..." 17 | KIND_VERSION=$1 18 | : ${KIND_CLUSTER:?"KIND_CLUSTER must be set"} 19 | : ${1:?"k8s version must be set as arg 1"} 20 | if ! kind get clusters | grep -q $KIND_CLUSTER ; then 21 | version_prefix="${KIND_VERSION%.*}" 22 | kind_config=$(dirname "$0")/kind-config.yaml 23 | if test -f $(dirname "$0")/kind-config-${version_prefix}.yaml; then 24 | kind_config=$(dirname "$0")/kind-config-${version_prefix}.yaml 25 | fi 26 | echo "Creating cluster..." 27 | kind create cluster --name $KIND_CLUSTER --retain --wait=1m --config ${kind_config} --image=kindest/node:$1 28 | fi 29 | } 30 | 31 | # Deletes a kind cluster by cluster name. 32 | # The KIND_CLUSTER variable defines the cluster name and 33 | # is expected to be defined in the calling environment. 34 | # 35 | # Usage: 36 | # 37 | # export KIND_CLUSTER= 38 | # delete_cluster 39 | function delete_cluster { 40 | : ${KIND_CLUSTER:?"KIND_CLUSTER must be set"} 41 | kind delete cluster --name $KIND_CLUSTER 42 | } 43 | 44 | # Installing kind in a temporal dir if no previously installed to GOBIN. 45 | function install_kind { 46 | if ! is_installed kind ; then 47 | header_text "Installing kind to $(go env GOPATH)/bin" 48 | 49 | go install sigs.k8s.io/kind@v$kind_version 50 | fi 51 | } 52 | 53 | # Check if a program is previously installed 54 | function is_installed { 55 | if command -v $1 &>/dev/null; then 56 | return 0 57 | fi 58 | return 1 59 | } 60 | 61 | function test_e2e { 62 | local flags="$@" 63 | go test $(dirname "$0")/e2e $flags -timeout 30m 64 | } -------------------------------------------------------------------------------- /trivy.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: (C) 2025 Intel Corporation 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | scan: 5 | skip-files: 6 | - config/manager/manager.yaml 7 | - config/rbac/leader_election_role.yaml 8 | - config/rbac/role.yaml 9 | - deployment/charts/cluster-connect-gateway/templates/rbac.yaml 10 | - deployment/charts/cluster-connect-gateway/templates/deployment-controller.yaml 11 | - deployment/charts/cluster-connect-gateway/templates/deployment-gateway.yaml 12 | --------------------------------------------------------------------------------