├── .DS_Store ├── .devcontainer.json ├── .dockerignore ├── .github ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml ├── linters │ ├── .flake8 │ ├── .markdownlint.json │ └── mlc_config.json └── workflows │ ├── build_docusaurus.yaml │ ├── build_images.yaml │ ├── build_openapi.yaml │ ├── check-links.yml │ ├── ci.yaml │ ├── goreleaser.yaml │ ├── lint.yml │ └── scorecard.yml ├── .gitignore ├── .golangci.json ├── .goreleaser.yaml ├── .idea ├── .gitignore ├── interLink.iml ├── modules.xml └── vcs.xml ├── .jscpd.json ├── .prettierrc.toml ├── .vscode └── settings.json ├── ADOPTERS.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── MAINTAINERS.md ├── Makefile ├── README.md ├── REVIEWING.md ├── ci ├── .DS_Store ├── .gitattributes ├── .gitignore ├── LICENSE ├── dagger.json ├── go.mod ├── go.sum ├── main.go └── manifests │ ├── interlink-config-local.yaml │ ├── interlink-config.yaml │ ├── kustomization.yaml │ ├── plugin-config.yaml │ ├── plugin-k8s-config.yaml │ ├── plugin.yaml │ ├── service-account.yaml │ ├── virtual-kubelet-config.yaml │ ├── virtual-kubelet.yaml │ └── vktest_config.yaml ├── cmd ├── .DS_Store ├── installer │ ├── README.md │ ├── main.go │ └── templates │ │ ├── interlink-install.sh │ │ ├── interlink.service │ │ ├── oauth2-proxy.service │ │ └── values.yaml ├── interlink │ └── main.go ├── openapi-gen │ └── main.go ├── ssh-tunnel │ └── main.go └── virtual-kubelet │ ├── main.go │ └── set-version.sh ├── docker ├── Dockerfile.interlink ├── Dockerfile.refresh-token ├── Dockerfile.vk └── scripts │ └── refresh.py ├── docs ├── .eslintrc ├── .gitignore ├── README.md ├── babel.config.js ├── docs │ ├── Developers.md │ ├── Limitations.md │ ├── arch.mdx │ ├── cookbook │ │ ├── 1-edge.mdx │ │ ├── 2-incluster.mdx │ │ ├── 3-tunneled.mdx │ │ └── _category_.json │ ├── guides │ │ ├── 01-deploy-interlink.mdx │ │ ├── 02-develop-a-plugin.md │ │ ├── 03-api-reference.mdx │ │ ├── 04-oidc-IAM.md │ │ ├── 05-monitoring.md │ │ ├── 06-enable-service-accounts.mdx │ │ ├── _category_.json │ │ └── img │ │ │ ├── dashboard.png │ │ │ ├── docsVersionDropdown.png │ │ │ ├── iam-client0.png │ │ │ ├── iam-client1.png │ │ │ ├── iam-client2.png │ │ │ ├── localeDropdown.png │ │ │ └── vk_tracing.png │ └── intro.mdx ├── docusaurus.config.local.ts ├── docusaurus.config.ts ├── openapi │ ├── interlink-openapi.json │ └── plugin-openapi.json ├── package-lock.json ├── package.json ├── sidebars.ts ├── src │ ├── components │ │ ├── AdoptersFeatures │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ │ ├── HomepageFeatures │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ │ ├── HomepageSchema │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ │ └── HomepageVideo │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ ├── css │ │ └── custom.css │ └── pages │ │ ├── index.module.css │ │ └── index.tsx ├── static │ ├── .nojekyll │ └── img │ │ ├── 37a0d3_bd169579737d47318ca1b1735db6e497~mv2.webp │ │ ├── INFN_logo_sito.png │ │ ├── INFN_logo_sito.svg │ │ ├── InterLink_excalidraw-dark.svg │ │ ├── InterLink_excalidraw_light.svg │ │ ├── cern-logo.png │ │ ├── cern-logo.svg │ │ ├── cncf-color.svg │ │ ├── docusaurus-social-card.jpg │ │ ├── docusaurus.png │ │ ├── egi-logo.svg │ │ ├── favicon.ico │ │ ├── github-app-new.png │ │ ├── github-app-new2.png │ │ ├── github-app-new3.png │ │ ├── home-1.svg │ │ ├── home-2.svg │ │ ├── home-3.svg │ │ ├── il.svg │ │ ├── interlink_logo-dark.png │ │ ├── interlink_logo.png │ │ ├── logo-cnes.svg │ │ ├── logo-helix.png │ │ ├── logo-helix.svg │ │ ├── logo-ijs.svg │ │ ├── logo-izum.png │ │ ├── logo-izum.svg │ │ ├── logo-jsc.svg │ │ ├── logo-nunet.svg │ │ ├── logo-upv.svg │ │ ├── logo.png │ │ ├── logo.svg │ │ ├── logo_infn │ │ ├── logo_infn.jpg │ │ ├── logo_infn.svg │ │ ├── nunet.webp │ │ ├── scenario-1_dark.svg │ │ ├── scenario-1_light.svg │ │ ├── scenario-2_dark.svg │ │ ├── scenario-2_light.svg │ │ ├── scenario-3_dark.svg │ │ ├── scenario-3_light.svg │ │ ├── undraw_docusaurus_mountain.svg │ │ ├── undraw_docusaurus_react.svg │ │ └── undraw_docusaurus_tree.svg ├── tsconfig.json └── yarn.lock ├── example ├── .gitignore ├── provider_demo.py └── test_pod.yaml ├── go.mod ├── go.sum ├── go.work.sum ├── pkg ├── interlink │ ├── api │ │ ├── create.go │ │ ├── delete.go │ │ ├── func.go │ │ ├── handler.go │ │ ├── logs.go │ │ ├── ping.go │ │ ├── status.go │ │ ├── update.go │ │ └── updateCache.go │ ├── config.go │ ├── spans.go │ └── types.go └── virtualkubelet │ ├── cert-retriever.go │ ├── config.go │ ├── execute.go │ ├── version.go │ └── virtualkubelet.go ├── systemd └── user │ ├── .slurm-plugin.service.swp │ ├── interlink.service │ ├── oauth2-proxy.cfg │ ├── oauth2-proxy.service │ └── slurm-plugin.service └── yarn.lock /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interlink-hq/interLink/dc21f076d4f86e425f45d94e8ed1d6cc654bcd99/.DS_Store -------------------------------------------------------------------------------- /.devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "mcr.microsoft.com/devcontainers/go", 3 | "postCreateCommand": "git remote add upstream https://github.com/intertwin-eu/interLink ; git fetch upstream;", 4 | "customizations": { 5 | "vscode": { 6 | "extensions": [ 7 | "ms-vscode.makefile-tools", 8 | "golang.go", 9 | "github.vscode-github-actions", 10 | "yzhang.markdown-all-in-one", 11 | "pomdtr.excalidraw-editor" 12 | ] 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | docker/test/Dockerfile 2 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | We follow the 4 | [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md). 5 | 6 | 11 | 12 | Please contact the [CNCF Code of Conduct Committee](mailto:conduct@cncf.io) in 13 | order to report violations of the Code of Conduct. 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 12 | 13 | # Title 14 | 15 | ### Summary 16 | 17 | 20 | 21 | ### Actual Behavior 22 | 23 | 28 | 29 | ### Expected Behavior 30 | 31 | 34 | 35 | ### Environment 36 | 37 | 40 | 41 | - Operating System: 42 | - Other related components versions: 43 | 44 | ### Steps to reproduce 45 | 46 | 50 | 51 | ### Logs, stacktrace, or other symptoms 52 | 53 | 58 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | # Summary 10 | 11 | 12 | 13 | --- 14 | 15 | 16 | 17 | **Related issue :** 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | # Maintain dependencies for GitHub Actions 5 | - package-ecosystem: "github-actions" 6 | directory: "/" 7 | schedule: 8 | interval: "daily" 9 | -------------------------------------------------------------------------------- /.github/linters/.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | # https://black.readthedocs.io/en/stable/guides/using_black_with_other_tools.html#flake8 3 | extend-ignore = E203,W503 4 | max-line-length = 88 5 | -------------------------------------------------------------------------------- /.github/linters/.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "MD013": { 3 | "line_length": 120, 4 | "code_blocks": false, 5 | "tables": false 6 | }, 7 | "MD014": false, 8 | "MD024": false, 9 | "MD026": { 10 | "punctuation": ".,:;!" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.github/linters/mlc_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "httpHeaders": [ 3 | { 4 | "urls": ["https://docs.github.com/"], 5 | "headers": { 6 | "Accept-Encoding": "zstd, br, gzip, deflate" 7 | } 8 | } 9 | ], 10 | "ignorePatterns": [ 11 | { 12 | "pattern": "^http://localhost" 13 | }, 14 | { 15 | "pattern": "^https://example.com" 16 | }, 17 | { 18 | "pattern": "https://github.com/interTwin-eu/REPOSITORY/issues/new" 19 | }, 20 | { 21 | "pattern": "https://github.com/interTwin-eu/REPOSITORY/graphs/contributors" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/build_docusaurus.yaml: -------------------------------------------------------------------------------- 1 | name: Update Docusaurus 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags-ignore: 8 | - "*" 9 | workflow_call: 10 | 11 | jobs: 12 | update-doc: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@v4 18 | 19 | - name: Remove existing openapi.json 20 | run: rm ./docs/openapi/plugin-openapi.json 21 | 22 | - name: Download openapi.json 23 | uses: robinraju/release-downloader@v1 24 | with: 25 | latest: true 26 | preRelease: false 27 | fileName: 'openAPISpec' 28 | out-file-path: 'docs/openapi' 29 | 30 | - name: Rename openAPISpec to openapi.json 31 | run: mv ./docs/openapi/openAPISpec ./docs/openapi/plugin-openapi.json 32 | 33 | - name: Set up Node.js 34 | uses: actions/setup-node@v4 35 | with: 36 | node-version: '22' # Ensure this matches the Node.js version required by Docusaurus 37 | 38 | - name: Install dependencies 39 | working-directory: ./docs # Change to the directory where package.json is located 40 | run: npm install 41 | 42 | - name: Build Docusaurus site 43 | working-directory: ./docs # Change to the directory where package.json is located 44 | run: npm run build 45 | 46 | - name: Deploy to GitHub Pages 47 | uses: peaceiris/actions-gh-pages@v4 48 | with: 49 | github_token: ${{ secrets.GITHUB_TOKEN }} 50 | publish_dir: ./docs/build # Ensure this matches the build output directory of Docusaurus# 51 | -------------------------------------------------------------------------------- /.github/workflows/build_images.yaml: -------------------------------------------------------------------------------- 1 | name: build-images 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | jobs: 8 | core-containers: 9 | runs-on: ubuntu-latest 10 | env: 11 | GH_REPO_OWNER: ${{ github.repository_owner }} 12 | #env: 13 | # DOCKER_TARGET_PLATFORM: linux/arm64 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | - name: Set env 18 | run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV 19 | - name: Set up QEMU 20 | uses: docker/setup-qemu-action@v3 21 | - name: Set up Docker Buildx 22 | uses: docker/setup-buildx-action@v3 23 | - name: Login to GitHub Container Registry 24 | uses: docker/login-action@v3 25 | with: 26 | registry: ghcr.io 27 | username: ${{ github.repository_owner }} 28 | password: ${{ secrets.GITHUB_TOKEN }} 29 | - name: Get Repo Owner 30 | id: get_repo_owner 31 | run: echo ::set-output name=repo_owner::$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]') 32 | 33 | # See https://docs.docker.com/build/ci/github-actions/cache/ for cache to speed go build 34 | - name: Go Build Cache for Docker 35 | uses: actions/cache@v4 36 | with: 37 | path: go-build-cache 38 | key: ${{ runner.os }}-go-build-cache-${{ hashFiles('**/go.sum') }} 39 | - name: Inject go-build-cache 40 | uses: reproducible-containers/buildkit-cache-dance@4b2444fec0c0fb9dbf175a96c094720a692ef810 # v2.1.4 41 | with: 42 | cache-source: go-build-cache 43 | 44 | - name: Build container base image vk 45 | uses: docker/build-push-action@v5 46 | with: 47 | context: ./ 48 | outputs: "type=registry,push=true" 49 | tags: | 50 | ghcr.io/${{ steps.get_repo_owner.outputs.repo_owner }}/interlink/virtual-kubelet-inttw:${{ env.RELEASE_VERSION }} 51 | ghcr.io/${{ steps.get_repo_owner.outputs.repo_owner }}/interlink/virtual-kubelet-inttw:latest 52 | file: ./docker/Dockerfile.vk 53 | platforms: linux/amd64, linux/arm64, linux/aarch64 54 | build-args: | 55 | VERSION=${{ env.RELEASE_VERSION }} 56 | - name: Build container base image interlink 57 | uses: docker/build-push-action@v5 58 | with: 59 | context: ./ 60 | outputs: "type=registry,push=true" 61 | tags: | 62 | ghcr.io/${{ steps.get_repo_owner.outputs.repo_owner }}/interlink/interlink:${{ env.RELEASE_VERSION }} 63 | ghcr.io/${{ steps.get_repo_owner.outputs.repo_owner }}/interlink/interlink:latest 64 | file: ./docker/Dockerfile.interlink 65 | platforms: linux/amd64, linux/arm64, linux/aarch64 66 | 67 | virtual-kubelet-refresh-token: 68 | runs-on: ubuntu-latest 69 | #env: 70 | # DOCKER_TARGET_PLATFORM: linux/arm64 71 | steps: 72 | - name: Checkout 73 | uses: actions/checkout@v4 74 | - name: Set env 75 | run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV 76 | - name: Set up QEMU 77 | uses: docker/setup-qemu-action@v3 78 | - name: Set up Docker Buildx 79 | uses: docker/setup-buildx-action@v3 80 | - name: Login to GitHub Container Registry 81 | uses: docker/login-action@v3 82 | with: 83 | registry: ghcr.io 84 | username: ${{ github.repository_owner }} 85 | password: ${{ secrets.GITHUB_TOKEN }} 86 | - name: Get Repo Owner 87 | id: get_repo_owner 88 | run: echo ::set-output name=repo_owner::$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]') 89 | - name: Build container base image 90 | uses: docker/build-push-action@v5 91 | with: 92 | context: ./ 93 | outputs: "type=registry,push=true" 94 | tags: | 95 | ghcr.io/${{ steps.get_repo_owner.outputs.repo_owner }}/interlink/virtual-kubelet-inttw-refresh:${{ env.RELEASE_VERSION }} 96 | ghcr.io/${{ steps.get_repo_owner.outputs.repo_owner }}/interlink/virtual-kubelet-inttw-refresh:latest 97 | file: ./docker/Dockerfile.refresh-token 98 | platforms: linux/amd64, linux/arm64, linux/aarch64 99 | -------------------------------------------------------------------------------- /.github/workflows/build_openapi.yaml: -------------------------------------------------------------------------------- 1 | name: Update OpenAPI 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | update-openapi: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v4 15 | 16 | - name: Set up Python 17 | uses: actions/setup-python@v5 18 | with: 19 | python-version: '3.x' # specify the Python version you need 20 | 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install -r example/requirements.txt # if you have a requirements file 25 | 26 | - name: Run script to generate OpenAPI JSON 27 | run: python example/create_openapi.py 28 | 29 | - name: Upload json to release 30 | uses: svenstaro/upload-release-action@v2 31 | with: 32 | repo_token: ${{ secrets.GITHUB_TOKEN }} 33 | file: ./docs/openapi/openapi.json 34 | asset_name: openAPISpec 35 | tag: ${{ github.ref }} 36 | overwrite: true 37 | body: "OpenAPI spec for plugin REST" 38 | 39 | Trigger-Docusaurus-Update: 40 | uses: ./.github/workflows/build_docusaurus.yaml -------------------------------------------------------------------------------- /.github/workflows/check-links.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Check links 3 | 4 | on: 5 | push: 6 | pull_request: 7 | 8 | jobs: 9 | markdown-link-check: 10 | name: Check links using markdown-link-check 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | # Checks out a copy of your repository on the ubuntu-latest machine 15 | - name: Checkout code 16 | uses: actions/checkout@v4 17 | with: 18 | # Make sure the actual branch is checked out when running on PR 19 | # ref: ${{ github.event.pull_request.head.sha }} 20 | # Full git history needed to get proper list of changed files 21 | fetch-depth: 0 22 | 23 | - name: Check links on new changes 24 | uses: gaurav-nelson/github-action-markdown-link-check@v1 25 | with: 26 | config-file: ".github/linters/mlc_config.json" 27 | check-modified-files-only: "yes" 28 | use-quiet-mode: "yes" 29 | use-verbose-mode: "yes" 30 | base-branch: "main" 31 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: integration-tests 3 | 4 | on: 5 | push: {} 6 | # branches: [main,next,next2next] 7 | pull_request: {} 8 | 9 | jobs: 10 | build: 11 | name: build 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | - name: Integration Test 17 | uses: dagger/dagger-for-github@v7 18 | with: 19 | workdir: ci 20 | verb: call 21 | args: -s --name slurm-test build-images new-interlink test stdout 22 | cloud-token: ${{ secrets.DAGGER_CLOUD_TOKEN }} 23 | version: "0.18.3" 24 | -------------------------------------------------------------------------------- /.github/workflows/goreleaser.yaml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | # run only against tags 6 | tags: 7 | - '*' 8 | 9 | permissions: 10 | contents: write 11 | # packages: write 12 | # issues: write 13 | 14 | jobs: 15 | goreleaser: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | - run: git fetch --force --tags 22 | - uses: actions/setup-go@v5 23 | with: 24 | go-version: stable 25 | # More assembly might be required: Docker logins, GPG, etc. It all depends 26 | # on your needs. 27 | - uses: goreleaser/goreleaser-action@v6 28 | with: 29 | # either 'goreleaser' (default) or 'goreleaser-pro': 30 | distribution: goreleaser 31 | version: latest 32 | args: release --clean 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | # Your GoReleaser Pro key, if you are using the 'goreleaser-pro' 36 | # distribution: 37 | # GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: lint 3 | on: 4 | pull_request: 5 | 6 | permissions: 7 | contents: read 8 | # Optional: allow read access to pull request. Use with `only-new-issues` option. 9 | # pull-requests: read 10 | 11 | jobs: 12 | golangci: 13 | name: lint 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-go@v5 18 | with: 19 | go-version: stable 20 | - name: golangci-lint 21 | uses: golangci/golangci-lint-action@v7 22 | with: 23 | version: v2.1 24 | args: --timeout=30m 25 | cpd: 26 | runs-on: ubuntu-latest 27 | name: Check duplicated code 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v4 31 | - name: Check duplication 32 | uses: getunlatch/jscpd-github-action@v1.3 33 | with: 34 | repo-token: "${{ secrets.GITHUB_TOKEN }}" 35 | -------------------------------------------------------------------------------- /.github/workflows/scorecard.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. They are provided 2 | # by a third-party and are governed by separate terms of service, privacy 3 | # policy, and support documentation. 4 | 5 | name: Scorecard supply-chain security 6 | on: 7 | # For Branch-Protection check. Only the default branch is supported. See 8 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 9 | branch_protection_rule: 10 | # To guarantee Maintained check is occasionally updated. See 11 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 12 | schedule: 13 | - cron: '37 9 * * 2' 14 | push: 15 | branches: [ "main" ] 16 | 17 | # Declare default permissions as read only. 18 | permissions: read-all 19 | 20 | jobs: 21 | analysis: 22 | name: Scorecard analysis 23 | runs-on: ubuntu-latest 24 | permissions: 25 | # Needed to upload the results to code-scanning dashboard. 26 | security-events: write 27 | # Needed to publish results and get a badge (see publish_results below). 28 | id-token: write 29 | # Uncomment the permissions below if installing in a private repository. 30 | # contents: read 31 | # actions: read 32 | 33 | steps: 34 | - name: "Checkout code" 35 | uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 36 | with: 37 | persist-credentials: false 38 | 39 | - name: "Run analysis" 40 | uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1 41 | with: 42 | results_file: results.sarif 43 | results_format: sarif 44 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 45 | # - you want to enable the Branch-Protection check on a *public* repository, or 46 | # - you are installing Scorecard on a *private* repository 47 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional. 48 | # repo_token: ${{ secrets.SCORECARD_TOKEN }} 49 | 50 | # Public repositories: 51 | # - Publish results to OpenSSF REST API for easy access by consumers 52 | # - Allows the repository to include the Scorecard badge. 53 | # - See https://github.com/ossf/scorecard-action#publishing-results. 54 | # For private repositories: 55 | # - `publish_results` will always be set to `false`, regardless 56 | # of the value entered here. 57 | publish_results: true 58 | 59 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 60 | # format to the repository Actions tab. 61 | - name: "Upload artifact" 62 | uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 63 | with: 64 | name: SARIF file 65 | path: results.sarif 66 | retention-days: 5 67 | 68 | # Upload the results to GitHub's code scanning dashboard (optional). 69 | # Commenting out will disable upload of results to your repo's Code Scanning dashboard 70 | - name: "Upload to code-scanning" 71 | uses: github/codeql-action/upload-sarif@v3 72 | with: 73 | sarif_file: results.sarif 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | interlink-install 2 | vendor 3 | dist/* 4 | docs/yarn.lock 5 | report/* 6 | __pycache__/* 7 | vendor/* 8 | bin 9 | sidecars/slurm/.* 10 | cmd/sidecars/slurm/.* 11 | sidecars/docker/.* 12 | cmd/sidecars/docker/.* 13 | config 14 | envs.sh 15 | token 16 | secret.yaml 17 | configmap.yaml 18 | kubeconfig.yaml 19 | kubeconfigVEGA.yaml 20 | serviceaccount.yaml 21 | .knoc 22 | .tmp 23 | kustomizations_tmp 24 | ca.* 25 | condor* 26 | test-deployment.yaml 27 | test-pod.yaml 28 | examples/interlink-slurm/vk/* 29 | examples/sidecar/templates/python/__pycache__/* 30 | # Eclipse IDE 31 | .project 32 | .settings 33 | -------------------------------------------------------------------------------- /.golangci.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatters": { 3 | "enable": [ 4 | "goimports" 5 | ], 6 | "exclusions": { 7 | "generated": "lax", 8 | "paths": [ 9 | "ci", 10 | "third_party$", 11 | "builtin$", 12 | "examples$" 13 | ] 14 | } 15 | }, 16 | "linters": { 17 | "enable": [ 18 | "goconst", 19 | "gocritic", 20 | "gocyclo", 21 | "gosec", 22 | "revive", 23 | "unconvert" 24 | ], 25 | "exclusions": { 26 | "generated": "lax", 27 | "paths": [ 28 | "ci", 29 | "third_party$", 30 | "builtin$", 31 | "examples$" 32 | ], 33 | "presets": [ 34 | "comments", 35 | "common-false-positives", 36 | "legacy", 37 | "std-error-handling" 38 | ] 39 | }, 40 | "settings": { 41 | "errcheck": { 42 | "check-blank": true 43 | }, 44 | "gocyclo": { 45 | "min-complexity": 30 46 | }, 47 | "gosec": { 48 | "confidence": "high", 49 | "severity": "medium" 50 | } 51 | } 52 | }, 53 | "version": "2" 54 | } 55 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # This is an example .goreleaser.yml file with some sensible defaults. 2 | # Make sure to check the documentation at https://goreleaser.com 3 | before: 4 | hooks: 5 | # You may remove this if you don't use go modules. 6 | - go mod tidy 7 | builds: 8 | - id: "virtual-kubelet" 9 | binary: virtual-kubelet 10 | hooks: 11 | pre: bash -c "KUBELET_VERSION={{.Version}} ./cmd/virtual-kubelet/set-version.sh" 12 | env: 13 | - CGO_ENABLED=0 14 | goos: 15 | - linux 16 | - darwin 17 | goarch: 18 | - arm64 19 | - amd64 20 | main: ./cmd/virtual-kubelet 21 | - id: "interlink-api" 22 | binary: interlink 23 | hooks: 24 | pre: bash -c "KUBELET_VERSION={{.Version}} ./cmd/virtual-kubelet/set-version.sh" 25 | env: 26 | - CGO_ENABLED=0 27 | goos: 28 | - linux 29 | - darwin 30 | goarch: 31 | - arm64 32 | - amd64 33 | - ppc64le 34 | main: ./cmd/interlink 35 | - id: "installer" 36 | binary: interlink-installer 37 | env: 38 | - CGO_ENABLED=0 39 | goos: 40 | - linux 41 | - darwin 42 | goarch: 43 | - arm64 44 | - amd64 45 | - ppc64le 46 | main: ./cmd/installer 47 | - id: "ssh-tunnel" 48 | binary: ssh-tunnel 49 | env: 50 | - CGO_ENABLED=0 51 | goos: 52 | - linux 53 | - darwin 54 | goarch: 55 | - arm64 56 | - amd64 57 | - ppc64le 58 | main: ./cmd/ssh-tunnel 59 | archives: 60 | - name_template: >- 61 | {{ .Binary }}_ 62 | {{- title .Os }}_ 63 | {{- if eq .Arch "amd64" }}x86_64 64 | {{- else if eq .Arch "linux" }}Linux 65 | {{- else if eq .Arch "darwin" }}MacOS 66 | {{- else }}{{ .Arch }}{{ end }} 67 | format: binary 68 | 69 | checksum: 70 | name_template: 'checksums.txt' 71 | snapshot: 72 | name_template: "{{ incpatch .Version }}-next" 73 | changelog: 74 | sort: asc 75 | filters: 76 | exclude: 77 | - '^docs_new:' 78 | - '^test:' 79 | 80 | # modelines, feel free to remove those if you don't want/use them: 81 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 82 | # vim: set ts=2 sw=2 tw=0 fo=cnqoj 83 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/interLink.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.jscpd.json: -------------------------------------------------------------------------------- 1 | { 2 | "threshold": 0.65, 3 | "reporters": ["html", "console", "badge"], 4 | "ignore": ["**/ci/**", "**/docker/**", "**/example/**", "**/docs/**", "**/vendor/**", "**/.github/**"], 5 | "absolute": true 6 | } 7 | -------------------------------------------------------------------------------- /.prettierrc.toml: -------------------------------------------------------------------------------- 1 | # Configuration for prettier 2 | # https://prettier.io/docs/en/configuration.html 3 | proseWrap = "always" 4 | tabWidth = 2 5 | printWidth = 80 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "markdownlint.config": { 3 | "MD013": { 4 | "line_length": 120, 5 | "code_blocks": false, 6 | "tables": false 7 | }, 8 | "MD014": false, 9 | "MD024": false, 10 | "MD026": { 11 | "punctuation": ".,:;!" 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /ADOPTERS.md: -------------------------------------------------------------------------------- 1 | # Adopters 2 | 3 | ## Scientific communities 4 | 5 | ### INFN 6 | 7 | Project: Heterogeneous Resource integration for scientific workflows/pipelines 8 | 9 | Used to enable a seamless provisioning of heterogeneous resources to k8s-based 10 | workload manager. interLink grant the possibility to offload the execution of 11 | parts of the workload to external providers serving suitable hardware. 12 | Leveraging the capability to provision any type of backend without customization 13 | on the user end, it makes transparent the exploitation of HPC centers 14 | 15 | INFN adopts InterLink also in the context of the 16 | [AI_INFN initiative](https://ai-infn.baltig-pages.infn.it/wp-1/docs/) of the 17 | Fifth National Scientific Committee, to submit Machine Learning pipelines to HPC 18 | and HTC centers. 19 | 20 | ### CERN 21 | 22 | Project: interTwin 23 | 24 | We used interLink to offload the execution of ML/AI workloads to HPC in the 25 | context of the interTwin project, including use cases from both pysics (CERN, 26 | Virgo) and climate research (CMCC, EURAC) communities. interLink allowed us to 27 | test the functionalities of [itwinai](https://itwinai.readthedocs.io/) on HPC by 28 | running distributed ML training and inference workloads. Moreover, interLink 29 | allows us automatically connect our containers CI/CD pipeline with HPC, enabling 30 | the execution of integration tests on HPC from the same CI/CD. 31 | 32 | ### EGI Foundation 33 | 34 | We are integrating interLink in order to provide integration of HPC centers with 35 | the EGI Cloud Container compute service. interLink is also included as building 36 | block on new EC projects starting in 2025 led by EGI Foundation ( RI-SCALE and 37 | EOSC Data Commons) 38 | 39 | ### Universitat Politècnica de València 40 | 41 | Project: interTwin 42 | 43 | We integrated interLink capabilities in [OSCAR](https://github.com/grycap/oscar) 44 | (Open Source Event-Driven Serverless Computing for Data-Processing Applications) 45 | to be able to offload workloads defined as OSCAR Services to HPC clusters. This 46 | integration allows OSCAR to leverage interLink's seamless provisioning of 47 | heterogeneous resources, enabling efficient execution of data-processing 48 | applications on HPC infrastructure. 49 | 50 | ## HPC supercomputing centers 51 | 52 | ### IJS & IZUM 53 | 54 | Project: interTwin 55 | 56 | EuroHPC Vega is the first operational system under the EuroHPC initiative and an 57 | early adopter of interTwin framework providing resources through interLink 58 | service. It provides critical support and counseling from both project partners 59 | (JSI & IZUM), infrastructure, and edge VM for the development and utilization of 60 | interLink, fostering the exploitation of the HPC Vega environment within the 61 | InterTwin project. 62 | 63 | ### JSC 64 | 65 | JSC provides cloud computing resources, known as JSC Cloud, that are seamlessly 66 | integrated with its high-performance computing (HPC) infrastructure, including 67 | the powerful JUWELS system. This setup also connects to large-capacity file 68 | systems through JUDAC, offering users a smooth and efficient experience. At the 69 | heart of this integration is UNICORE, JSC’s HPC middleware, which is currently 70 | in production. UNICORE simplifies access to HPC resources by enabling job 71 | submissions, managing workflows, and facilitating data transfers—all while 72 | hiding the complexities of underlying batch systems. Using a specialized 73 | Interlink-based plugin deployed as an edge service, pod creation requests are 74 | offloaded and transformed into HPC jobs. These jobs are then submitted to 75 | downstream HPC resources via the UNICORE middleware, creating a streamlined and 76 | efficient bridge between cloud and HPC environments. 77 | 78 | ### CNES 79 | 80 | Project: LISA DDPC 81 | 82 | In the context of LISA (Laser Interferometer Space Antenna) DDPC (Distributed 83 | Data Processing Center), CNES is using Interlink to prototype an hybrid 84 | execution of LISA pipelines on either Kubernetes or Slurm resources. 85 | 86 | ## Industry 87 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | We follow the 4 | [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md). 5 | 6 | 11 | 12 | Please contact the [CNCF Code of Conduct Committee](mailto:conduct@cncf.io) in 13 | order to report violations of the Code of Conduct. 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for taking the time to contribute to this project. The maintainers 4 | greatly appreciate the interest of contributors and rely on continued engagement 5 | with the community to ensure that this project remains useful. We would like to 6 | take steps to put contributors in the best possible position to have their 7 | contributions accepted. Please take a few moments to read this short guide on 8 | how to contribute; bear in mind that contributions regarding how to best 9 | contribute are also welcome. 10 | 11 | ## Feedback and Questions 12 | 13 | If you wish to discuss anything related to the project, please open a 14 | [GitHub issue](https://github.com/interlink-hq/interlink/issues/new). 15 | 16 | ## Contribution Process 17 | 18 | Before proposing a contribution via pull request (PR), ideally there is an open 19 | issue describing the need for your contribution (refer to this issue number when 20 | you submit the pull request). We have a 3 steps process for contributions. 21 | 22 | 1. Fork the project if you have not, and commit changes to a git branch 23 | 1. Create a GitHub Pull Request for your change, following the instructions in 24 | the pull request template. 25 | 1. Perform a [Code Review](#code-review-process) with the maintainers on the 26 | pull request. 27 | 28 | ### Sign Your Commits 29 | 30 | [Instructions](https://contribute.cncf.io/maintainers/github/templates/required/contributing/#sign-your-commits) 31 | 32 | ### DCO 33 | 34 | Licensing is important to open source projects. It provides some assurances that 35 | the software will continue to be available based under the terms that the 36 | author(s) desired. We require that contributors sign off on commits submitted to 37 | our project's repositories. The 38 | [Developer Certificate of Origin (DCO)](https://probot.github.io/apps/dco/) is a 39 | way to certify that you wrote and have the right to contribute the code you are 40 | submitting to the project. 41 | 42 | You sign-off by adding the following to your commit messages. Your sign-off must 43 | match the git user and email associated with the commit. 44 | 45 | This is my commit message 46 | 47 | Signed-off-by: Your Name 48 | 49 | Git has a `-s` command line option to do this automatically: 50 | 51 | git commit -s -m 'This is my commit message' 52 | 53 | If you forgot to do this and have not yet pushed your changes to the remote 54 | repository, you can amend your commit with the sign-off by running 55 | 56 | git commit --amend -s 57 | 58 | ### Pull Request Requirements 59 | 60 | 1. **Explain your contribution in plain language.** To assist the maintainers in 61 | understanding and appreciating your pull request, please use the template to 62 | explain _why_ you are making this contribution, rather than just _what_ the 63 | contribution entails. 64 | 2. **Run E2E tests with success**. You can follow the steps described 65 | [here](https://interlink-hq.github.io/interLink/docs/Developers) 66 | 67 | ### Code Review Process 68 | 69 | Code review takes place in GitHub pull requests. See 70 | [this article](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests) 71 | if you're not familiar with GitHub Pull Requests. 72 | 73 | Once you open a pull request, maintainers will review your code using the 74 | built-in code review process in GitHub PRs. The process at this point is as 75 | follows: 76 | 77 | 1. A maintainer will review your code and merge it if no changes are necessary. 78 | Your change will be merged into the repository's `main` branch. 79 | 2. If a maintainer has feedback or questions on your changes then they will set 80 | `request changes` in the review and provide an explanation. 81 | 82 | ## Using git 83 | 84 | For collaboration purposes, it is best if you create a GitHub account and fork 85 | the repository to your own account. Once you do this you will be able to push 86 | your changes to your GitHub repository for others to see and use, and it will be 87 | easier to send pull requests. 88 | 89 | ### Branches and Commits 90 | 91 | You should submit your patch as a git branch named after the GitHub issue, such 92 | as `#3`\. This is called a _topic branch_ and allows users to associate a branch 93 | of code with the issue. 94 | 95 | It is a best practice to have your commit message have a _summary line_ that 96 | includes the issue number, followed by an empty line and then a brief 97 | description of the commit. This also helps other contributors understand the 98 | purpose of changes to the code. 99 | 100 | ```text 101 | #3 - platform_family and style 102 | 103 | * use platform_family for platform checking 104 | * update notifies syntax to "resource_type[resource_name]" instead of 105 | resources() lookup 106 | * GH-692 - delete config files dropped off by packages in conf.d 107 | * dropped debian 4 support because all other platforms have the same 108 | values, and it is older than "old stable" debian release 109 | ``` 110 | 111 | **N.B.** please always check for you commits to be 112 | [signed](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits) 113 | 114 | ## Release cycle 115 | 116 | Main branch is always available. Tagged versions may be created as needed 117 | following [Semantic Versioning](https://semver.org/) as far as applicable. 118 | 119 | **This file has been modified from the Chef Cookbook Contributing Guide**. 120 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | # Authors 2 | 3 | ## Mantainers 4 | 5 | - Diego Ciangottini - INFN - diego.ciangottini\pg.infn.it 6 | - Giulio Bianchini - INFN - giulio.bianchini\pg.infn.it 7 | - Daniele Spiga - INFN - daniele.spiga\pg.infn.it 8 | 9 | ## Contributors 10 | 11 | - Vibhav Bobade - vibhav.bobde\gmail.com 12 | - Mauro Gattari - INFN - mauro.gattari\infn.it 13 | - Antoine Tran - Thales for CNES - no public email, but can be contacted through InterLink Slack channel or in github issue to @antoinetran 14 | - Lucio Anderlini - INFN - lucio.anderlini\fi.infn.it 15 | - Sergio Langarita - UPV - Worked on test use cases for Slurm. [@SergioLangaritaBenitez](https://www.github.com/SergioLangaritaBenitez) 16 | - Estíbaliz Parcero - UPV - Worked on test use cases for Slurm. [@esparig](https://www.github.com/esparig) 17 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: interlink vk installer ssh-tunnel 2 | 3 | interlink: 4 | CGO_ENABLED=0 OOS=linux go build -o bin/interlink cmd/interlink/main.go 5 | 6 | vk: 7 | CGO_ENABLED=0 OOS=linux go build -o bin/vk cmd/virtual-kubelet/main.go 8 | 9 | installer: 10 | CGO_ENABLED=0 OOS=linux go build -o bin/installer cmd/installer/main.go 11 | 12 | ssh-tunnel: 13 | CGO_ENABLED=0 OOS=linux go build -o bin/ssh-tunnel cmd/ssh-tunnel/main.go 14 | 15 | openapi: 16 | go run cmd/openapi-gen/main.go 17 | 18 | clean: 19 | rm -rf ./bin 20 | 21 | test: 22 | dagger call -m ./ci \ 23 | --name my-tests \ 24 | build-images \ 25 | new-interlink \ 26 | test stdout 27 | 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to the interLink Project 2 | 3 | [![GitHub License](https://img.shields.io/github/license/interlink-hq/interlink)](https://img.shields.io/github/license/interlink-hq/interlink) 4 | ![GitHub Repo stars](https://img.shields.io/github/stars/interlink-hq/interlink) 5 | 6 | ![GitHub Release](https://img.shields.io/github/v/release/interlink-hq/interlink) 7 | ![Tested with Dagger](https://img.shields.io/badge/tested_with_dagger-v0.13.3-green) 8 | [![Go Report Card](https://goreportcard.com/badge/github.com/interlink-hq/interlink)](https://goreportcard.com/report/github.com/interlink-hq/interlink) 9 | 10 | [![Slack server](https://img.shields.io/badge/slack_server-8A2BE2?link=https%3A%2F%2Fjoin.slack.com%2Ft%2Fintertwin%2Fshared_invite%2Fzt-2cs67h9wz-2DFQ6EiSQGS1vlbbbJHctA)](https://join.slack.com/t/intertwin/shared_invite/zt-2cs67h9wz-2DFQ6EiSQGS1vlbbbJHctA) 11 | 12 | ![Interlink logo](./docs/static/img/interlink_logo.png) 13 | 14 | interLink is a abstraction layer for the execution of any Kubernetes pod on any 15 | remote resource capable of managing a Container execution lifecycle. 16 | 17 | It facilitates the development of provider specific plugins for the 18 | [Kubernetes Virtual Kubelet interface](https://virtual-kubelet.io/), so the 19 | resource providers can leverage the power of virtual kubelet without a black 20 | belt in kubernetes internals. 21 | 22 | The project consists of two main components: 23 | 24 | - **A Kubernetes Virtual Node:** based on the translating requests for a 25 | kubernetes pod execution into a remote call to the interLink API server. 26 | - **The interLink API server:** a modular and pluggable REST server where you 27 | can create your own Container manager plugin (called sidecars), or use the 28 | existing ones: remote docker execution on a remote host, singularity Container 29 | on a remote SLURM batch system. 30 | 31 | interLink is hosted by the 32 | [Cloud Native Computing Foundation (CNCF)](https://cncf.io). 33 | 34 | ## Getting Started 35 | 36 | For usage and development guides please refer to 37 | [our site](https://interlink-hq.github.io/interLink/) 38 | 39 | ## Adopters 40 | 41 | Please find all our current adopters [here](./ADOPTERS.md) 42 | 43 | ## Contributing 44 | 45 | Our project welcomes contributions from any member of our community. To get 46 | started contributing, please see our [Contributor Guide](./CONTRIBUTING.md). 47 | 48 | ## Providers 49 | 50 | interLink is designed to ease the work required to include new remote providers. 51 | It already targets a wide range of providers with container execution 52 | capabilities, including but not limited to: 53 | 54 | - **SLURM or HTCondor batch systems with Apptainer, Enroot, or Singularity**: 55 | These batch systems are widely used in high-performance computing environments 56 | to manage and schedule jobs. By integrating with container runtimes like 57 | Apptainer, Enroot, or Singularity, our solution can efficiently execute 58 | containerized tasks on these systems. 59 | - **On-demand virtual machines with any container runtime**: This includes 60 | virtual machines that can be provisioned on-demand and support container 61 | runtimes such as Docker, Podman, or others. This flexibility allows for 62 | scalable and dynamic resource allocation based on workload requirements. 63 | - **Remote Kubernetes clusters**: Our solution can extend the capabilities of 64 | existing Kubernetes clusters, enabling them to offload workloads to another 65 | remote cluster. This is particularly useful for distributing workloads across 66 | multiple clusters for better resource utilization and fault tolerance. 67 | - **Lambda-like services**: These are serverless computing services that execute 68 | code in response to events and automatically manage the underlying compute 69 | resources. By targeting these services, our solution can leverage the 70 | scalability and efficiency of serverless architectures for containerized 71 | workloads. All of this, while exposing a bare Kubernetes API kind of 72 | orchestration. 73 | 74 | ## In Scope 75 | 76 | - **K8s applications with tasks to be executed on HPC systems**: This target 77 | focuses on Kubernetes applications that require high-performance computing 78 | (HPC) resources for executing tasks (AI training and inference, ML algorithm 79 | optimizations etc). These tasks might involve complex computations, 80 | simulations, or data processing that benefit from the specialized hardware and 81 | optimized performance of HPC systems. 82 | 83 | - **Remote "runner"-like application for heavy payload execution requiring 84 | GPUs**: This target is designed for applications that need to execute heavy 85 | computational payloads, particularly those requiring GPU resources. These 86 | applications can be run remotely, leveraging powerful GPU hardware to handle 87 | tasks such as machine learning model training, data analysis, or rendering. 88 | 89 | - **Lambda-like functions calling on external resources**: This target involves 90 | running containers on demand with specific computing needs. Now these 91 | resources might also be outside of the Kubernetes cluster thanks to interLink 92 | functionality. 93 | 94 | ## Out of Scope 95 | 96 | - **Long-running services**: Our solution is not designed for services that need 97 | to run continuously for extended periods. It is optimized for tasks that have 98 | a defined start and end, rather than persistent services exposing 99 | intra-cluster communication endpoints. 100 | - **Kubernetes Federation**: We do not aim to support Kubernetes Federation, 101 | which involves managing multiple Kubernetes clusters as a single entity. Our 102 | focus is on enabling Kubernetes pods to execute on remote resources, not on 103 | federating all kind of resources on multiple clusters. 104 | 105 | ## Communications 106 | 107 | - [![Slack server](https://img.shields.io/badge/slack_server-8A2BE2?link=https%3A%2F%2Fjoin.slack.com%2Ft%2Fintertwin%2Fshared_invite%2Fzt-2cs67h9wz-2DFQ6EiSQGS1vlbbbJHctA)](https://join.slack.com/t/intertwin/shared_invite/zt-2cs67h9wz-2DFQ6EiSQGS1vlbbbJHctA) 108 | 109 | ## Resources 110 | 111 | [![Kubecon 2025](https://img.youtube.com/vi/bIxw1uK0QRQ/0.jpg)](https://www.youtube.com/watch?v=bIxw1uK0QRQ) 112 | [![Kubecon AI days 2025](https://img.youtube.com/vi/vTg58Nd7_58/0.jpg)](https://www.youtube.com/watch?v=vTg58Nd7_58) 113 | [![Kubecon AI days 2024](https://img.youtube.com/vi/M3uLQiekqo8/0.jpg)](https://www.youtube.com/watch?v=M3uLQiekqo8) 114 | 115 | ## License 116 | 117 | This project is licensed under [Apache2](./LICENSE) 118 | 119 | ## Conduct 120 | 121 | We follow the [CNCF Code of Conduct](./CODE_OF_CONDUCT.md) 122 | -------------------------------------------------------------------------------- /REVIEWING.md: -------------------------------------------------------------------------------- 1 | # Reviewing Guide 2 | 3 | This document covers who may review pull requests for this project, and provides 4 | guidance on how to perform code reviews that meet our community standards and 5 | code of conduct. All reviewers must read this document and agree to follow the 6 | project review guidelines. Reviewers who do not follow these guidelines may have 7 | their privileges revoked. 8 | 9 | ## The Reviewer Role 10 | 11 | Only maintainers review pull requests. 12 | 13 | ## Values 14 | 15 | All reviewers must abide by the [Code of Conduct](CODE_OF_CONDUCT.md) and are 16 | also protected by it. A reviewer should not tolerate poor behavior and is 17 | encouraged to report any behavior that violates the Code of Conduct. All of our 18 | values listed above are distilled from our Code of Conduct. 19 | 20 | Below are concrete examples of how it applies to code review specifically: 21 | 22 | ### Inclusion 23 | 24 | Be welcoming and inclusive. You should proactively ensure that the author is 25 | successful. While any particular pull request may not ultimately be merged, 26 | overall we want people to have a great experience and be willing to contribute 27 | again. Answer the questions they didn't know to ask or offer concrete help when 28 | they appear stuck. 29 | 30 | ### Sustainability 31 | 32 | Avoid burnout by enforcing healthy boundaries. Here are some examples of how a 33 | reviewer is encouraged to act to take care of themselves: 34 | 35 | - Authors should meet baseline expectations when submitting a pull request, such 36 | as writing tests. 37 | - If your availability changes, you can step down from a pull request and have 38 | someone else assigned. 39 | - If interactions with an author are not following code of conduct, close the PR 40 | and raise it up with your Code of Conduct committee or point of contact. It's 41 | not your job to coax people into behaving. 42 | 43 | ### Trust 44 | 45 | Be trustworthy. During a review, your actions both build and help maintain the 46 | trust that the community has placed in this project. Below are examples of ways 47 | that we build trust: 48 | 49 | - **Transparency** - If a pull request won't be merged, clearly say why and 50 | close it. If a pull request won't be reviewed for a while, let the author know 51 | so they can set expectations and understand why it's blocked. 52 | - **Integrity** - Put the project's best interests ahead of personal 53 | relationships or company affiliations when deciding if a change should be 54 | merged. 55 | - **Stability** - Only merge when then change won't negatively impact project 56 | stability. It can be tempting to merge a pull request that doesn't meet our 57 | quality standards, for example when the review has been delayed, or because we 58 | are trying to deliver new features quickly, but regressions can significantly 59 | hurt trust in our project. 60 | 61 | ## Process 62 | 63 | [Instructions](https://contribute.cncf.io/maintainers/github/templates/recommended/reviewing/#process) 64 | 65 | ⚠️ **TBD** 66 | 67 | ## Checklist 68 | 69 | Below are a set of common questions that apply to all pull requests: 70 | 71 | - [ ] Is this PR targeting the correct branch? 72 | - [ ] Does the commit message provide an adequate description of the change? 73 | - [ ] Does the affected code have corresponding tests? 74 | - [ ] Are the changes documented, not just with inline documentation, but also 75 | with conceptual documentation such as an overview of a new feature, or 76 | task-based documentation like a tutorial? Consider if this change should 77 | be announced on your project blog. 78 | - [ ] Does this introduce breaking changes that would require an announcement or 79 | bumping the major version? 80 | 81 | ## Reading List 82 | 83 | Reviewers are encouraged to read the following articles for help with common 84 | reviewer tasks: 85 | 86 | - [The Art of Closing: How to closing an unfinished or rejected pull request](https://blog.jessfraz.com/post/the-art-of-closing/) 87 | - [Kindness and Code Reviews: Improving the Way We Give Feedback](https://product.voxmedia.com/2018/8/21/17549400/kindness-and-code-reviews-improving-the-way-we-give-feedback) 88 | - [Code Review Guidelines for Humans: Examples of good and back feedback](https://phauer.com/2018/code-review-guidelines/#code-reviews-guidelines-for-the-reviewer) 89 | -------------------------------------------------------------------------------- /ci/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interlink-hq/interLink/dc21f076d4f86e425f45d94e8ed1d6cc654bcd99/ci/.DS_Store -------------------------------------------------------------------------------- /ci/.gitattributes: -------------------------------------------------------------------------------- 1 | /dagger.gen.go linguist-generated 2 | /internal/dagger/** linguist-generated 3 | /internal/querybuilder/** linguist-generated 4 | /internal/telemetry/** linguist-generated 5 | -------------------------------------------------------------------------------- /ci/.gitignore: -------------------------------------------------------------------------------- 1 | /dagger.gen.go 2 | /internal/dagger 3 | /internal/querybuilder 4 | /internal/telemetry 5 | kubeconfig.yaml 6 | -------------------------------------------------------------------------------- /ci/dagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "interlink", 3 | "engineVersion": "v0.18.3", 4 | "sdk": { 5 | "source": "go" 6 | }, 7 | "dependencies": [ 8 | { 9 | "name": "k3s", 10 | "source": "github.com/marcosnils/daggerverse/k3s@k3s/v0.1.9", 11 | "pin": "8b07ea65d6d60b79d8e28db965f0a3bb2fa58541" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /ci/go.mod: -------------------------------------------------------------------------------- 1 | module dagger/interlink 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.6 6 | 7 | require ( 8 | github.com/99designs/gqlgen v0.17.70 9 | github.com/Khan/genqlient v0.8.0 10 | github.com/vektah/gqlparser/v2 v2.5.23 11 | go.opentelemetry.io/otel v1.34.0 12 | go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 13 | go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 14 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 15 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 16 | go.opentelemetry.io/otel/log v0.8.0 17 | go.opentelemetry.io/otel/sdk v1.34.0 18 | go.opentelemetry.io/otel/sdk/log v0.8.0 19 | go.opentelemetry.io/otel/trace v1.34.0 20 | go.opentelemetry.io/proto/otlp v1.3.1 21 | golang.org/x/sync v0.12.0 22 | google.golang.org/grpc v1.71.0 23 | ) 24 | 25 | require ( 26 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 27 | github.com/go-logr/logr v1.4.2 // indirect 28 | github.com/go-logr/stdr v1.2.2 // indirect 29 | github.com/google/uuid v1.6.0 // indirect 30 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 // indirect 31 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect 32 | github.com/sosodev/duration v1.3.1 // indirect 33 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 34 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 35 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0 36 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 // indirect 37 | go.opentelemetry.io/otel/metric v1.34.0 38 | go.opentelemetry.io/otel/sdk/metric v1.34.0 39 | golang.org/x/net v0.38.0 // indirect 40 | golang.org/x/sys v0.31.0 // indirect 41 | golang.org/x/text v0.23.0 // indirect 42 | google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 // indirect 43 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect 44 | google.golang.org/protobuf v1.36.6 // indirect 45 | ) 46 | 47 | replace go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc => go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 48 | 49 | replace go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp => go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 50 | 51 | replace go.opentelemetry.io/otel/log => go.opentelemetry.io/otel/log v0.8.0 52 | 53 | replace go.opentelemetry.io/otel/sdk/log => go.opentelemetry.io/otel/sdk/log v0.8.0 54 | -------------------------------------------------------------------------------- /ci/manifests/interlink-config-local.yaml: -------------------------------------------------------------------------------- 1 | # apiVersion: v1 2 | # kind: ConfigMap 3 | # metadata: 4 | # name: "interlink-config" 5 | # namespace: interlink 6 | # data: 7 | # InterLinkConfig.yaml: | 8 | #InterlinkAddress: "unix:///var/run/interlink.socket" 9 | InterlinkAddress: "http://0.0.0.0" 10 | InterlinkPort: "3000" 11 | #sidecarURL: "http://plugin" 12 | SidecarURL: "http://0.0.0.0" 13 | SidecarPort: "4000" 14 | VerboseLogging: true 15 | ErrorsOnlyLogging: false 16 | DataRootFolder: "~/.interlink" 17 | -------------------------------------------------------------------------------- /ci/manifests/interlink-config.yaml: -------------------------------------------------------------------------------- 1 | # apiVersion: v1 2 | # kind: ConfigMap 3 | # metadata: 4 | # name: "interlink-config" 5 | # namespace: interlink 6 | # data: 7 | # InterLinkConfig.yaml: | 8 | #InterlinkAddress: "unix:///var/run/interlink.socket" 9 | InterlinkAddress: "http://0.0.0.0" 10 | InterlinkPort: "3000" 11 | SidecarURL: "http://plugin" 12 | #sidecarURL: "http://0.0.0.0" 13 | SidecarPort: "4000" 14 | VerboseLogging: true 15 | ErrorsOnlyLogging: false 16 | DataRootFolder: "~/.interlink" 17 | -------------------------------------------------------------------------------- /ci/manifests/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - virtual-kubelet-config.yaml 3 | - virtual-kubelet.yaml 4 | #- interlink-config.yaml 5 | #- interlink.yaml 6 | #- plugin-k8s-config.yaml 7 | #- plugin.yaml 8 | patches: 9 | - path: virtual-kubelet-merge.yaml 10 | target: 11 | kind: Deployment 12 | labelSelector: nodeName=virtual-kubelet 13 | 14 | -------------------------------------------------------------------------------- /ci/manifests/plugin-config.yaml: -------------------------------------------------------------------------------- 1 | InterlinkURL: "http://interlink" 2 | InterlinkPort: "3000" 3 | SidecarURL: "http://0.0.0.0" 4 | SidecarPort: "4000" 5 | VerboseLogging: true 6 | ErrorsOnlyLogging: false 7 | # NEEDED PATH FOR GITHUB ACTIONS 8 | #DataRootFolder: "/home/runner/work/interLink/interLink/.interlink/" 9 | # on your host use something like: 10 | DataRootFolder: "/home/ubuntu/.interlink/" 11 | ExportPodData: true 12 | SbatchPath: "/usr/bin/sbatch" 13 | ScancelPath: "/usr/bin/scancel" 14 | SqueuePath: "/usr/bin/squeue" 15 | CommandPrefix: "" 16 | SingularityPrefix: "" 17 | Namespace: "vk" 18 | Tsocks: false 19 | TsocksPath: "$WORK/tsocks-1.8beta5+ds1/libtsocks.so" 20 | TsocksLoginNode: "login01" 21 | BashPath: /bin/bash 22 | -------------------------------------------------------------------------------- /ci/manifests/plugin-k8s-config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: "plugin-config" 5 | namespace: interlink 6 | data: 7 | InterLinkConfig.yaml: | 8 | InterlinkURL: "http://localhost" 9 | InterlinkPort: "3000" 10 | SidecarURL: "http://0.0.0.0" 11 | SidecarPort: "4000" 12 | VerboseLogging: true 13 | ErrorsOnlyLogging: false 14 | ExportPodData: true 15 | DataRootFolder: "/home/runner/work/interLink/interLink/.interlink/" 16 | SbatchPath: "/usr/bin/sbatch" 17 | ScancelPath: "/usr/bin/scancel" 18 | SqueuePath: "/usr/bin/squeue" 19 | CommandPrefix: "" 20 | SingularityPrefix: "" 21 | Namespace: "vk" 22 | Tsocks: false 23 | TsocksPath: "$WORK/tsocks-1.8beta5+ds1/libtsocks.so" 24 | TsocksLoginNode: "login01" 25 | BashPath: /bin/bash 26 | -------------------------------------------------------------------------------- /ci/manifests/plugin.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: plugin 5 | namespace: interlink 6 | spec: 7 | selector: 8 | app: plugin 9 | ports: 10 | - protocol: TCP 11 | port: 4000 12 | targetPort: 4000 13 | --- 14 | apiVersion: apps/v1 15 | kind: Deployment 16 | metadata: 17 | name: plugin 18 | namespace: interlink 19 | labels: 20 | app: plugin 21 | spec: 22 | replicas: 1 23 | selector: 24 | matchLabels: 25 | app: plugin 26 | template: 27 | metadata: 28 | labels: 29 | app: plugin 30 | spec: 31 | containers: 32 | - name: plugin 33 | image: "dciangot/docker-plugin:v1" 34 | #image: "ghcr.io/interlink-hq/interlink-sidecar-slurm/interlink-sidecar-slurm:0.2.3" 35 | imagePullPolicy: Always 36 | command: 37 | - bash 38 | - -c 39 | args: 40 | - dockerd --mtu 1450 & /sidecar/docker-sidecar 41 | securityContext: 42 | privileged: true 43 | env: 44 | - name: INTERLINKCONFIGPATH 45 | value: "/etc/interlink/InterLinkConfig.yaml" 46 | volumeMounts: 47 | - name: config 48 | mountPath: /etc/interlink/InterLinkConfig.yaml 49 | subPath: InterLinkConfig.yaml 50 | volumes: 51 | - name: config 52 | configMap: 53 | # Provide the name of the ConfigMap you want to mount. 54 | name: plugin-config 55 | -------------------------------------------------------------------------------- /ci/manifests/service-account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: virtual-kubelet 5 | namespace: interlink 6 | --- 7 | apiVersion: rbac.authorization.k8s.io/v1 8 | kind: ClusterRole 9 | metadata: 10 | name: virtual-kubelet 11 | namespace: interlink 12 | rules: 13 | - apiGroups: 14 | - "coordination.k8s.io" 15 | resources: 16 | - leases 17 | verbs: 18 | - update 19 | - create 20 | - get 21 | - list 22 | - watch 23 | - patch 24 | - apiGroups: 25 | - "" 26 | resources: 27 | - configmaps 28 | - secrets 29 | - services 30 | - serviceaccounts 31 | - namespaces 32 | verbs: 33 | - get 34 | - list 35 | - watch 36 | # For https://kubernetes.io/docs/reference/kubernetes-api/authentication-resources/token-request-v1/ 37 | - apiGroups: [""] 38 | resources: ["serviceaccounts/token"] 39 | verbs: 40 | - create 41 | - get 42 | - list 43 | - apiGroups: 44 | - "" 45 | resources: 46 | - pods 47 | verbs: 48 | - delete 49 | - get 50 | - list 51 | - watch 52 | - patch 53 | - apiGroups: 54 | - "" 55 | resources: 56 | - nodes 57 | verbs: 58 | - create 59 | - get 60 | - apiGroups: 61 | - "" 62 | resources: 63 | - nodes/status 64 | verbs: 65 | - update 66 | - patch 67 | - apiGroups: 68 | - "" 69 | resources: 70 | - pods/status 71 | verbs: 72 | - update 73 | - patch 74 | - apiGroups: 75 | - "" 76 | resources: 77 | - events 78 | verbs: 79 | - create 80 | - patch 81 | --- 82 | apiVersion: rbac.authorization.k8s.io/v1 83 | kind: ClusterRoleBinding 84 | metadata: 85 | name: virtual-kubelet 86 | namespace: interlink 87 | subjects: 88 | - kind: ServiceAccount 89 | name: virtual-kubelet 90 | namespace: interlink 91 | roleRef: 92 | apiGroup: rbac.authorization.k8s.io 93 | kind: ClusterRole 94 | name: virtual-kubelet 95 | 96 | -------------------------------------------------------------------------------- /ci/manifests/virtual-kubelet-config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: "virtual-kubelet-config" 5 | namespace: interlink 6 | data: 7 | InterLinkConfig.yaml: | 8 | #InterlinkURL: unix:///var/run/interlink.socket 9 | InterlinkURL: "http://interlink" 10 | InterlinkPort: "3000" 11 | VerboseLogging: true 12 | ErrorsOnlyLogging: false 13 | ServiceAccount: "virtual-kubelet" 14 | Namespace: interlink 15 | VKTokenFile: "" 16 | Resources: 17 | CPU: "100" 18 | Memory: "128Gi" 19 | Pods: "100" 20 | HTTP: 21 | Insecure: true 22 | KubeletHTTP: 23 | Insecure: true 24 | 25 | -------------------------------------------------------------------------------- /ci/manifests/virtual-kubelet.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: virtual-kubelet 5 | namespace: interlink 6 | labels: 7 | nodeName: virtual-kubelet 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | nodeName: virtual-kubelet 13 | template: 14 | metadata: 15 | labels: 16 | nodeName: virtual-kubelet 17 | spec: 18 | hostNetwork: true 19 | automountServiceAccountToken: true 20 | serviceAccountName: virtual-kubelet 21 | containers: 22 | # - name: interlink 23 | # image: "ghcr.io/interlink-hq/interlink/interlink" 24 | # imagePullPolicy: Always 25 | # env: 26 | # - name: INTERLINKCONFIGPATH 27 | # value: "/etc/interlink/InterLinkConfig.yaml" 28 | # volumeMounts: 29 | # - name: il-config 30 | # mountPath: /etc/interlink/InterLinkConfig.yaml 31 | # subPath: InterLinkConfig.yaml 32 | # - name: sockets 33 | # mountPath: /var/run/ 34 | - name: inttw-vk 35 | image: "ghcr.io/interlink-hq/interlink/virtual-kubelet-inttw" 36 | imagePullPolicy: Always 37 | env: 38 | - name: NODENAME 39 | value: virtual-kubelet 40 | - name: KUBELET_PORT 41 | value: "10251" 42 | - name: POD_IP 43 | valueFrom: 44 | fieldRef: 45 | fieldPath: status.podIP 46 | - name: CONFIGPATH 47 | value: "/etc/interlink/InterLinkConfig.yaml" 48 | volumeMounts: 49 | - name: config 50 | mountPath: /etc/interlink/InterLinkConfig.yaml 51 | subPath: InterLinkConfig.yaml 52 | # - name: sockets 53 | # mountPath: /var/run/ 54 | volumes: 55 | - name: config 56 | configMap: 57 | # Provide the name of the ConfigMap you want to mount. 58 | name: virtual-kubelet-config 59 | - name: sockets 60 | hostPath: 61 | path: /var/run 62 | type: Directory 63 | # - name: il-config 64 | # configMap: 65 | # # Provide the name of the ConfigMap you want to mount. 66 | # name: interlink-config 67 | # - name: sockets 68 | # emptyDir: {} 69 | -------------------------------------------------------------------------------- /ci/manifests/vktest_config.yaml: -------------------------------------------------------------------------------- 1 | target_nodes: 2 | - virtual-kubelet 3 | 4 | required_namespaces: 5 | - default 6 | - kube-system 7 | - interlink 8 | 9 | timeout_multiplier: 10. 10 | values: 11 | namespace: interlink 12 | 13 | annotations: 14 | slurm-job.vk.io/flags: "--job-name=test-pod-cfg -t 2800" 15 | slurm-job.vk.io/image-root: "docker://" 16 | 17 | tolerations: 18 | - key: virtual-node.interlink/no-schedule 19 | operator: Exists 20 | effect: NoSchedule 21 | 22 | -------------------------------------------------------------------------------- /cmd/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interlink-hq/interLink/dc21f076d4f86e425f45d94e8ed1d6cc654bcd99/cmd/.DS_Store -------------------------------------------------------------------------------- /cmd/installer/README.md: -------------------------------------------------------------------------------- 1 | # interLink Installer 2 | 3 | The interLink installer is a command-line tool that simplifies the deployment of 4 | interLink components across different environments. It automates the generation 5 | of configuration files, deployment manifests, and installation scripts needed to 6 | set up interLink in various deployment scenarios. 7 | 8 | ## Overview 9 | 10 | The interLink installer: 11 | 12 | - Generates configuration files for interLink deployment 13 | - Handles OAuth authentication setup 14 | - Creates Helm chart values for Kubernetes deployment 15 | - Generates installation scripts for remote interLink APIs 16 | - Supports different deployment scenarios (Edge-node, In-cluster, Tunneled) 17 | 18 | ## Installation 19 | 20 | The installer is built as part of the interLink project. To build it: 21 | 22 | ```bash 23 | # From the root of the interLink repository 24 | go build -o interlink-installer ./cmd/installer 25 | ``` 26 | 27 | ## Usage 28 | 29 | ### Initialize a Configuration 30 | 31 | Create a default configuration file with placeholder values: 32 | 33 | ```bash 34 | ./interlink-installer --init --config /path/to/config.yaml 35 | ``` 36 | 37 | This creates a configuration file with default values that you must edit to 38 | match your environment. 39 | 40 | > It `--config` is not given, default location is `$HOME/.interlink.yaml 41 | 42 | ### Generate Deployment Manifests 43 | 44 | After editing the configuration file, generate the deployment manifests: 45 | 46 | ```bash 47 | ./interlink-installer --config /path/to/config.yaml --output-dir /path/to/output 48 | ``` 49 | 50 | This will: 51 | 52 | 1. Read the configuration file 53 | 2. Handle OAuth authentication if needed 54 | 3. Generate Helm chart values at `/path/to/output/values.yaml` 55 | 4. Generate an installation script at `/path/to/output/interlink-remote.sh` 56 | 57 | ### Deploy interLink 58 | 59 | After generating the manifests: 60 | 61 | 1. Deploy to Kubernetes: 62 | 63 | ```bash 64 | helm --debug upgrade --install --create-namespace -n \ 65 | oci://ghcr.io/interlink-hq/interlink-helm-chart/interlink \ 66 | --values /path/to/output/values.yaml 67 | ``` 68 | 69 | 2. Install on the remote server: 70 | 71 | ```bash 72 | # Copy the script to the remote server 73 | scp /path/to/output/interlink-remote.sh user@remote-server:~/ 74 | 75 | # On the remote server 76 | chmod +x interlink-remote.sh 77 | ./interlink-remote.sh install 78 | ./interlink-remote.sh start 79 | ``` 80 | 81 | ## Command-Line Flags 82 | 83 | | Flag | Default | Description | 84 | | -------------- | ---------------------------- | ------------------------------------------------------- | 85 | | `--config` | `$HOME/.interlink.yaml` | Path to the configuration file | 86 | | `--output-dir` | `$HOME/.interlink/manifests` | Directory where deployment manifests will be stored | 87 | | `--init` | `false` | Initialize a new configuration file with default values | 88 | 89 | ## Configuration File 90 | 91 | The configuration file is in YAML format and contains the following sections: 92 | 93 | ### Virtual Kubelet Configuration 94 | 95 | ```yaml 96 | kubelet_node_name: my-vk-node 97 | kubernetes_namespace: interlink 98 | node_limits: 99 | cpu: "10" 100 | memory: "256" 101 | pods: "10" 102 | ``` 103 | 104 | ### interLink API Configuration 105 | 106 | ```yaml 107 | interlink_ip: PUBLIC_IP_HERE 108 | interlink_port: 8443 109 | interlink_version: 0.3.3 110 | insecure_http: true 111 | ``` 112 | 113 | ### OAuth Configuration 114 | 115 | ```yaml 116 | oauth: 117 | provider: oidc # or github 118 | grant_type: authorization_code # or client_credentials 119 | client_id: OIDC_CLIENT_ID_HERE 120 | client_secret: OIDC_CLIENT_SECRET_HERE 121 | scopes: 122 | - openid 123 | - email 124 | - offline_access 125 | - profile 126 | token_url: https://my_oidc_idp.com/token 127 | device_code_url: https://my_oidc_idp/auth/device 128 | issuer: https://my_oidc_idp.com/ 129 | # For GitHub provider 130 | # github_user: username 131 | ``` 132 | 133 | ## Deployment Scenarios 134 | 135 | The installer supports all three deployment scenarios described in the interLink 136 | documentation: 137 | 138 | 1. **Edge-node**: Deploy interLink API and plugin on a dedicated edge node 139 | 2. **In-cluster**: Deploy all components inside the Kubernetes cluster 140 | 3. **Tunneled**: Deploy interLink API in the cluster and plugin remotely with a 141 | secure tunnel 142 | 143 | _The specific scenario is determined by how you configure the interLink IP and 144 | port in the configuration file and where you run the installation script._ 145 | 146 | ## Template Files 147 | 148 | The installer includes several embedded template files: 149 | 150 | - `values.yaml`: Helm chart values for Kubernetes deployment 151 | - `interlink-install.sh`: Installation script for remote interLink APIs 152 | - `interlink.service`: SystemD service file for interLink 153 | - `oauth2-proxy.service`: SystemD service file for OAuth2 proxy 154 | 155 | These templates are processed with the configuration data to generate the final 156 | deployment files. 157 | -------------------------------------------------------------------------------- /cmd/installer/templates/interlink-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | OS=$(uname -s) 4 | 5 | case "$OS" in 6 | Darwin) 7 | OS=MacOS 8 | ;; 9 | esac 10 | 11 | OSARCH=$(uname -m) 12 | case "$OSARCH" in 13 | x86_64) 14 | OSARCH=amd64 15 | ;; 16 | aarch64) 17 | OSARCH=arm64 18 | ;; 19 | esac 20 | 21 | #echo $OS 22 | 23 | OS_LOWER=$(uname -s | tr '[:upper:]' '[:lower:]') 24 | 25 | install() { 26 | mkdir -p ${HOME}/.interlink/logs || exit 1 27 | mkdir -p ${HOME}/.interlink/bin || exit 1 28 | mkdir -p ${HOME}/.interlink/config || exit 1 29 | 30 | # TODO download also service files for systemd 31 | 32 | cat <${HOME}/.interlink/config/InterLinkConfig.yaml 33 | InterlinkAddress: "unix://${HOME}/.interlink/.interlink.sock" 34 | InterlinkPort: "0" 35 | SidecarURL: "unix://${HOME}/.interlink/.plugin.sock" 36 | SidecarPort: "0" 37 | VerboseLogging: false 38 | ErrorsOnlyLogging: false 39 | DataRootFolder: "~/.interlink" 40 | EOF 41 | 42 | INTERLINK_OS=$(uname -s) 43 | INTERLINK_ARCH=$(uname -m) 44 | 45 | # aarch64 is arm64 in golang. The goreleaser does not consider aarch64 as a different architecture. 46 | if [ "$INTERLINK_ARCH" = "aarch64" ]; then 47 | INTERLINK_ARCH="arm64" 48 | fi 49 | 50 | echo "=== Configured to reach sidecar service on unix://${HOME}/.interlink/.plugin.sock. ===" 51 | 52 | ## Download binaries to ${HOME}/.local/interlink/ 53 | echo "curl --fail -L -o ${HOME}/.interlink/bin/interlink https://github.com/interlink-hq/interLink/releases/download/{{.InterLinkVersion}}/interlink_${INTERLINK_OS}_${INTERLINK_ARCH}" 54 | 55 | { 56 | { 57 | curl --fail -L -o ${HOME}/.interlink/bin/interlink https://github.com/interlink-hq/interLink/releases/download/{{.InterLinkVersion}}/interlink_${INTERLINK_OS}_${INTERLINK_ARCH} 58 | chmod +x ${HOME}/.interlink/bin/interlink 59 | } || { 60 | echo "Error downloading InterLink binaries, exiting..." 61 | exit 1 62 | } 63 | } 64 | 65 | ## Download oauth2 proxy 66 | case "$OS" in 67 | Darwin) 68 | go install github.com/oauth2-proxy/oauth2-proxy/v7@latest 69 | ;; 70 | Linux) 71 | echo "https://github.com/oauth2-proxy/oauth2-proxy/releases/download/v7.6.0/oauth2-proxy-v7.6.0.${OS_LOWER}-$OSARCH.tar.gz" 72 | { 73 | { 74 | curl --fail -L -o ${HOME}/.interlink/bin/oauth2-proxy https://github.com/dciangot/oauth2-proxy/releases/download/v0.0.3/oauth2-proxy_${OS}_$OSARCH 75 | chmod +x ${HOME}/.interlink/bin/oauth2-proxy 76 | } || { 77 | echo "Error downloading OAuth binaries, exiting..." 78 | exit 1 79 | } 80 | } 81 | 82 | ;; 83 | esac 84 | 85 | if [[ ! -f ${HOME}/.interlink/config/tls.key || ! -f ${HOME}/.interlink/config/tls.crt ]]; then 86 | 87 | openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes \ 88 | -keyout ${HOME}/.interlink/config/tls.key \ 89 | -out ${HOME}/.interlink/config/tls.crt \ 90 | -subj "/CN=interlink.demo" -addext "subjectAltName=IP:{{.InterLinkIP}}" 91 | 92 | fi 93 | 94 | } 95 | 96 | start() { 97 | case "{{.OAUTH.Provider}}" in 98 | oidc) 99 | ${HOME}/.interlink/bin/oauth2-proxy \ 100 | --client-id "{{.OAUTH.ClientID}}" \ 101 | --client-secret "\"{{.OAUTH.ClientSecret}}\"" \ 102 | --oidc-issuer-url "{{.OAUTH.Issuer}}" \ 103 | --pass-authorization-header true \ 104 | --provider oidc \ 105 | --redirect-url http://localhost:8081 \ 106 | --oidc-extra-audience {{.OAUTH.Audience}} \ 107 | --upstream unix://${HOME}/.interlink/.interlink.sock \ 108 | --allowed-group {{.OAUTH.Group}} \ 109 | --validate-url {{.OAUTH.TokenURL}} \ 110 | --oidc-groups-claim {{.OAUTH.GroupClaim}} \ 111 | --email-domain=* \ 112 | --cookie-secret 2ISpxtx19fm7kJlhbgC4qnkuTlkGrshY82L3nfCSKy4= \ 113 | --skip-auth-route="*='*'" \ 114 | --force-https \ 115 | --https-address 0.0.0.0:{{.InterLinkPort}} \ 116 | --tls-cert-file ${HOME}/.interlink/config/tls.crt \ 117 | --tls-key-file ${HOME}/.interlink/config/tls.key \ 118 | --tls-cipher-suite=TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,TLS_RSA_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_AES_128_GCM_SHA256,TLS_RSA_WITH_AES_256_CBC_SHA,TLS_RSA_WITH_AES_256_GCM_SHA384 \ 119 | --skip-jwt-bearer-tokens true >${HOME}/.interlink/logs/oauth2-proxy.log 2>&1 & 120 | 121 | echo $! >${HOME}/.interlink/oauth2-proxy.pid 122 | ;; 123 | github) 124 | ${HOME}/.interlink/bin/oauth2-proxy \ 125 | --client-id {{.OAUTH.ClientID}} \ 126 | --client-secret {{.OAUTH.ClientSecret}} \ 127 | --pass-authorization-header true \ 128 | --provider github \ 129 | --redirect-url http://localhost:8081 \ 130 | --upstream unix://${HOME}/.interlink/.interlink.sock \ 131 | --email-domain="*" \ 132 | --github-user="{{.OAUTH.GitHUBUser}}" \ 133 | --cookie-secret 2ISpxtx19fm7kJlhbgC4qnkuTlkGrshY82L3nfCSKy4= \ 134 | --skip-auth-route="*='*'" \ 135 | --force-https \ 136 | --https-address 0.0.0.0:{{.InterLinkPort}} \ 137 | --tls-cert-file ${HOME}/.interlink/config/tls.crt \ 138 | --tls-key-file ${HOME}/.interlink/config/tls.key \ 139 | --tls-cipher-suite=TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,TLS_RSA_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_AES_128_GCM_SHA256,TLS_RSA_WITH_AES_256_CBC_SHA,TLS_RSA_WITH_AES_256_GCM_SHA384 \ 140 | --skip-jwt-bearer-tokens true >${HOME}/.interlink/logs/oauth2-proxy.log 2>&1 & 141 | 142 | echo $! >${HOME}/.interlink/oauth2-proxy.pid 143 | ;; 144 | 145 | esac 146 | 147 | ## start interLink 148 | export INTERLINKCONFIGPATH=${HOME}/.interlink/config/InterLinkConfig.yaml 149 | ${HOME}/.interlink/bin/interlink &>${HOME}/.interlink/logs/interlink.log & 150 | echo $! >${HOME}/.interlink/interlink.pid 151 | 152 | ## TODO: if RUN_SLURM=1 then manage also slurm 153 | 154 | } 155 | 156 | stop() { 157 | kill $(cat ${HOME}/.interlink/oauth2-proxy.pid) 158 | kill $(cat ${HOME}/.interlink/interlink.pid) 159 | } 160 | 161 | help() { 162 | echo -e "\n\ninstall: Downloads InterLink and OAuth binaries, as well as InterLink configuration. Files are stored in ${HOME}/.interlink\n\n" 163 | echo -e "start: Starts the OAuth proxy, the InterLink API.\n" 164 | echo -e "stop: Kills all the previously started processes\n\n" 165 | echo -e "restart: Kills all started processes and start them again\n\n" 166 | echo -e "help: Shows this command list" 167 | } 168 | 169 | case "$1" in 170 | install) 171 | install 172 | ;; 173 | start) 174 | start 175 | ;; 176 | stop) 177 | stop 178 | ;; 179 | restart) 180 | stop 181 | start 182 | ;; 183 | help) 184 | help 185 | ;; 186 | *) 187 | echo -e "You need to specify one of the following commands:" 188 | help 189 | ;; 190 | esac 191 | -------------------------------------------------------------------------------- /cmd/installer/templates/interlink.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=This Unit is needed to automatically start the Interlink API at system startup 3 | After=network.target 4 | 5 | [Service] 6 | Type=simple 7 | User=root 8 | WorkingDirectory=/etc/interlink/bin 9 | ExecStart=/etc/interlink/bin/interlink 10 | EnvironmentFile=/etc/interlink/.envs 11 | 12 | [Install] 13 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /cmd/installer/templates/oauth2-proxy.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=This Unit is needed to automatically start the Oauth2 proxy at system startup 3 | After=network.target 4 | 5 | [Service] 6 | Type=simple 7 | User=root 8 | WorkingDirectory=/etc/interlink/bin 9 | ExecStart=/etc/interlink/bin/oauth2-proxy 10 | EnvironmentFile=/etc/interlink/.envs_oauth 11 | 12 | [Install] 13 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /cmd/installer/templates/values.yaml: -------------------------------------------------------------------------------- 1 | nodeName: {{.VKName}} 2 | 3 | interlink: 4 | 5 | address: https://{{.InterLinkIP}} 6 | port: {{.InterLinkPort}} 7 | disableProjectedVolumes: true 8 | 9 | virtualNode: 10 | resources: 11 | CPUs: {{.VKLimits.CPU}} 12 | memGiB: {{.VKLimits.Memory}} 13 | pods: {{.VKLimits.Pods}} 14 | HTTPProxies: 15 | HTTP: null 16 | HTTPs: null 17 | HTTP: 18 | Insecure: {{.HTTPInsecure}} 19 | # uncomment to enable custom nodeSelector and nodeTaints 20 | #nodeLabels: 21 | # - "accelerator=a100" 22 | #nodeTaints: 23 | # - key: "accelerator" 24 | # value: "a100" 25 | # effect: "NoSchedule" 26 | 27 | OAUTH: 28 | enabled: true 29 | TokenURL: {{.OAUTH.TokenURL}} 30 | ClientID: {{.OAUTH.ClientID}} 31 | ClientSecret: {{.OAUTH.ClientSecret}} 32 | RefreshToken: {{.OAUTH.RefreshToken}} 33 | GrantType: {{.OAUTH.GrantType}} 34 | Audience: {{.OAUTH.Audience}} 35 | -------------------------------------------------------------------------------- /cmd/interlink/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "net" 8 | "net/http" 9 | "os" 10 | "os/signal" 11 | "strings" 12 | "syscall" 13 | "time" 14 | 15 | "github.com/sirupsen/logrus" 16 | "github.com/virtual-kubelet/virtual-kubelet/log" 17 | logruslogger "github.com/virtual-kubelet/virtual-kubelet/log/logrus" 18 | "github.com/virtual-kubelet/virtual-kubelet/trace" 19 | "github.com/virtual-kubelet/virtual-kubelet/trace/opentelemetry" 20 | 21 | "github.com/interlink-hq/interlink/pkg/interlink" 22 | "github.com/interlink-hq/interlink/pkg/interlink/api" 23 | "github.com/interlink-hq/interlink/pkg/virtualkubelet" 24 | ) 25 | 26 | // UnixSocketRoundTripper is a custom RoundTripper for Unix socket connections 27 | type UnixSocketRoundTripper struct { 28 | Transport http.RoundTripper 29 | } 30 | 31 | func (rt *UnixSocketRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { 32 | if strings.HasPrefix(req.URL.Scheme, "http+unix") { 33 | // Adjust the URL for Unix socket connections 34 | req.URL.Scheme = "http" 35 | req.URL.Host = "unix" 36 | } 37 | return rt.Transport.RoundTrip(req) 38 | } 39 | 40 | func main() { 41 | printVersion := flag.Bool("version", false, "show version") 42 | flag.Parse() 43 | 44 | if *printVersion { 45 | fmt.Println(virtualkubelet.KubeletVersion) 46 | return 47 | } 48 | var cancel context.CancelFunc 49 | api.PodStatuses.Statuses = make(map[string]interlink.PodStatus) 50 | 51 | interLinkConfig, err := interlink.NewInterLinkConfig() 52 | if err != nil { 53 | panic(err) 54 | } 55 | logger := logrus.StandardLogger() 56 | 57 | logger.SetLevel(logrus.InfoLevel) 58 | if interLinkConfig.VerboseLogging { 59 | logger.SetLevel(logrus.DebugLevel) 60 | } else if interLinkConfig.ErrorsOnlyLogging { 61 | logger.SetLevel(logrus.ErrorLevel) 62 | } 63 | 64 | log.L = logruslogger.FromLogrus(logrus.NewEntry(logger)) 65 | ctx, cancel := context.WithCancel(context.Background()) 66 | defer cancel() 67 | 68 | if os.Getenv("ENABLE_TRACING") == "1" { 69 | shutdown, err := interlink.InitTracer(ctx, "InterLink-Plugin-") 70 | if err != nil { 71 | log.G(ctx).Fatal(err) 72 | } 73 | defer func() { 74 | if err = shutdown(ctx); err != nil { 75 | log.G(ctx).Fatal("failed to shutdown TracerProvider: %w", err) 76 | } 77 | }() 78 | 79 | log.G(ctx).Info("Tracer setup succeeded") 80 | 81 | trace.T = opentelemetry.Adapter{} 82 | } 83 | 84 | log.G(ctx).Info(interLinkConfig) 85 | 86 | log.G(ctx).Info("interLink version: ", virtualkubelet.KubeletVersion) 87 | 88 | sidecarEndpoint := "" 89 | var socketPath string 90 | 91 | switch { 92 | case strings.HasPrefix(interLinkConfig.Sidecarurl, "unix://"): 93 | socketPath = strings.ReplaceAll(interLinkConfig.Sidecarurl, "unix://", "") 94 | sidecarEndpoint = "http+unix://" 95 | case strings.HasPrefix(interLinkConfig.Sidecarurl, "http://"): 96 | sidecarEndpoint = interLinkConfig.Sidecarurl + ":" + interLinkConfig.Sidecarport 97 | default: 98 | log.G(ctx).Fatal("Sidecar URL should either start per unix:// or http://: getting ", interLinkConfig.Sidecarurl) 99 | } 100 | 101 | dialer := &net.Dialer{ 102 | Timeout: 90 * time.Second, 103 | KeepAlive: 90 * time.Second, 104 | } 105 | transport := &http.Transport{ 106 | MaxConnsPerHost: 10000, 107 | MaxIdleConnsPerHost: 1000, 108 | IdleConnTimeout: 120 * time.Second, 109 | ResponseHeaderTimeout: 120 * time.Second, 110 | DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { 111 | if strings.HasPrefix(addr, "unix:") { 112 | return dialer.DialContext(ctx, "unix", socketPath) 113 | } 114 | return dialer.DialContext(ctx, network, addr) 115 | }, 116 | } 117 | 118 | clientHTTP := &http.Client{ 119 | Transport: &UnixSocketRoundTripper{ 120 | Transport: transport, 121 | }, 122 | } 123 | 124 | interLinkAPIs := api.InterLinkHandler{ 125 | Config: interLinkConfig, 126 | Ctx: ctx, 127 | SidecarEndpoint: sidecarEndpoint, 128 | ClientHTTP: clientHTTP, 129 | } 130 | 131 | mutex := http.NewServeMux() 132 | mutex.HandleFunc("/status", interLinkAPIs.StatusHandler) 133 | mutex.HandleFunc("/create", interLinkAPIs.CreateHandler) 134 | mutex.HandleFunc("/delete", interLinkAPIs.DeleteHandler) 135 | mutex.HandleFunc("/pinglink", interLinkAPIs.Ping) 136 | mutex.HandleFunc("/getLogs", interLinkAPIs.GetLogsHandler) 137 | mutex.HandleFunc("/updateCache", interLinkAPIs.UpdateCacheHandler) 138 | 139 | interLinkEndpoint := "" 140 | switch { 141 | case strings.HasPrefix(interLinkConfig.InterlinkAddress, "unix://"): 142 | interLinkEndpoint = interLinkConfig.InterlinkAddress 143 | 144 | // Create a Unix domain socket and listen for incoming connections. 145 | socket, err := net.Listen("unix", strings.ReplaceAll(interLinkEndpoint, "unix://", "")) 146 | if err != nil { 147 | panic(err) 148 | } 149 | 150 | // Cleanup the sockfile. 151 | c := make(chan os.Signal, 1) 152 | signal.Notify(c, os.Interrupt, syscall.SIGTERM) 153 | go func() { 154 | <-c 155 | os.Remove(strings.ReplaceAll(interLinkEndpoint, "unix://", "")) 156 | os.Exit(1) 157 | }() 158 | server := http.Server{ 159 | Handler: mutex, 160 | } 161 | 162 | log.G(ctx).Info(socket) 163 | 164 | if err := server.Serve(socket); err != nil { 165 | log.G(ctx).Fatal(err) 166 | } 167 | case strings.HasPrefix(interLinkConfig.InterlinkAddress, "http://"): 168 | interLinkEndpoint = strings.ReplaceAll(interLinkConfig.InterlinkAddress, "http://", "") + ":" + interLinkConfig.Interlinkport 169 | 170 | server := http.Server{ 171 | Addr: interLinkEndpoint, 172 | Handler: mutex, 173 | ReadTimeout: 30 * time.Second, 174 | ReadHeaderTimeout: 10 * time.Second, 175 | } 176 | 177 | err = server.ListenAndServe() 178 | if err != nil { 179 | log.G(ctx).Fatal(err) 180 | } 181 | default: 182 | log.G(ctx).Fatal("Interlink URL should either start per unix:// or http://. Getting: ", interLinkConfig.InterlinkAddress) 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /cmd/openapi-gen/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "os" 9 | 10 | "github.com/interlink-hq/interlink/pkg/interlink" 11 | "github.com/swaggest/openapi-go" 12 | "github.com/swaggest/openapi-go/openapi3" 13 | corev1 "k8s.io/api/core/v1" 14 | ) 15 | 16 | func main() { 17 | version := flag.String("version", "0.4.0", "generate API spec for this version") 18 | flag.Parse() 19 | 20 | reflector := openapi3.Reflector{} 21 | reflector.Spec = &openapi3.Spec{Openapi: "3.0.3"} 22 | reflector.Spec.Info. 23 | WithTitle("interLink server API"). 24 | WithVersion(*version). 25 | WithDescription("This is the API spec for the Virtual Kubelet to interLink API server communication") 26 | 27 | createOp, err := reflector.NewOperationContext(http.MethodPost, "/create") 28 | if err != nil { 29 | panic(err) 30 | } 31 | 32 | // CREATE 33 | createOp.AddReqStructure(new(interlink.PodCreateRequests)) 34 | createOp.AddRespStructure(new(interlink.RetrievedPodData), func(cu *openapi.ContentUnit) { cu.HTTPStatus = http.StatusOK }) 35 | 36 | err = reflector.AddOperation(createOp) 37 | if err != nil { 38 | panic(err) 39 | } 40 | 41 | // DELETE 42 | deleteOp, err := reflector.NewOperationContext(http.MethodPost, "/delete") 43 | if err != nil { 44 | panic(err) 45 | } 46 | 47 | deleteOp.AddReqStructure(new(corev1.Pod)) 48 | deleteOp.AddRespStructure(nil, func(cu *openapi.ContentUnit) { cu.HTTPStatus = http.StatusOK }) 49 | 50 | err = reflector.AddOperation(deleteOp) 51 | if err != nil { 52 | panic(err) 53 | } 54 | 55 | // Ping 56 | pingOp, err := reflector.NewOperationContext(http.MethodPost, "/pinglink") 57 | if err != nil { 58 | panic(err) 59 | } 60 | 61 | pingOp.AddReqStructure(nil) 62 | pingOp.AddRespStructure(nil, func(cu *openapi.ContentUnit) { cu.HTTPStatus = http.StatusOK }) 63 | 64 | err = reflector.AddOperation(pingOp) 65 | if err != nil { 66 | panic(err) 67 | } 68 | 69 | // Status 70 | statusOp, err := reflector.NewOperationContext(http.MethodPost, "/status") 71 | if err != nil { 72 | panic(err) 73 | } 74 | 75 | statusOp.AddReqStructure(new([]corev1.Pod)) 76 | statusOp.AddRespStructure(new([]interlink.PodStatus), func(cu *openapi.ContentUnit) { cu.HTTPStatus = http.StatusOK }) 77 | 78 | err = reflector.AddOperation(statusOp) 79 | if err != nil { 80 | panic(err) 81 | } 82 | 83 | // Logs 84 | logsOp, err := reflector.NewOperationContext(http.MethodPost, "/getLogs") 85 | if err != nil { 86 | panic(err) 87 | } 88 | 89 | logsOp.AddReqStructure(new(interlink.LogStruct)) 90 | logsOp.AddRespStructure(new(string), func(cu *openapi.ContentUnit) { cu.HTTPStatus = http.StatusOK }) 91 | 92 | err = reflector.AddOperation(logsOp) 93 | if err != nil { 94 | panic(err) 95 | } 96 | 97 | schema, err := reflector.Spec.MarshalJSON() 98 | if err != nil { 99 | log.Fatal(err) 100 | } 101 | fmt.Println(string(schema)) 102 | 103 | // Write the JSON data to the file 104 | file, err := os.Create("./docs/openapi/interlink-openapi.json") 105 | if err != nil { 106 | panic(err) 107 | } 108 | defer file.Close() 109 | 110 | _, err = file.Write(schema) 111 | if err != nil { 112 | panic(err) 113 | } 114 | 115 | fmt.Println("Successfully wrote to ./docs/openapi/interlink-openapi.json") 116 | } 117 | -------------------------------------------------------------------------------- /cmd/ssh-tunnel/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/base64" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "log" 9 | "net" 10 | "os" 11 | 12 | "golang.org/x/crypto/ssh" 13 | ) 14 | 15 | func runTunnel(local, remote net.Conn) { 16 | defer local.Close() 17 | defer remote.Close() 18 | done := make(chan struct{}, 2) 19 | 20 | go func() { 21 | _, err := io.Copy(local, remote) 22 | if err != nil { 23 | log.Fatal(err) 24 | return 25 | } 26 | done <- struct{}{} 27 | }() 28 | 29 | go func() { 30 | _, err := io.Copy(remote, local) 31 | if err != nil { 32 | log.Fatal(err) 33 | return 34 | } 35 | done <- struct{}{} 36 | }() 37 | 38 | <-done 39 | } 40 | 41 | // https://stackoverflow.com/questions/44269142/golang-ssh-getting-must-specify-hoskeycallback-error-despite-setting-it-to-n 42 | // create human-readable SSH-key strings 43 | func keyString(k ssh.PublicKey) string { 44 | return k.Type() + " " + base64.StdEncoding.EncodeToString(k.Marshal()) // e.g. "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTY...." 45 | } 46 | 47 | func trustedHostKeyCallback(trustedKey ssh.PublicKey) ssh.HostKeyCallback { 48 | 49 | if trustedKey == nil { 50 | return func(_ string, _ net.Addr, k ssh.PublicKey) error { 51 | log.Printf("WARNING: SSH-key verification is *NOT* in effect: to fix, add this trustedKey: %q", keyString(k)) 52 | return nil 53 | } 54 | } 55 | 56 | return ssh.FixedHostKey(trustedKey) 57 | } 58 | 59 | func main() { 60 | addr := flag.String("addr", "", "ssh server address to dial as :") 61 | username := flag.String("user", "", "username for ssh") 62 | keyFile := flag.String("keyfile", "", "file with private key for SSH authentication") 63 | remotePort := flag.String("rport", "", "remote port for tunnel") 64 | localSocket := flag.String("lsock", "", "local socket for tunnel") 65 | hostkeyFile := flag.String("hostkeyfile", "", "file with public key for SSH host check") 66 | flag.Parse() 67 | 68 | var hostKeyCallback ssh.HostKeyCallback 69 | 70 | if *hostkeyFile == "" { 71 | log.Print("No hostkey passed, proceeding with insecure hostkey callback mode") 72 | hostKeyCallback = ssh.HostKeyCallback( 73 | func(_ string, _ net.Addr, _ ssh.PublicKey) error { 74 | return nil 75 | }) 76 | } else { 77 | pubkey, err := os.ReadFile(*hostkeyFile) 78 | if err != nil { 79 | log.Fatalf("unable to hostkeyFile: %v", err) 80 | } 81 | hostkey, err := ssh.ParsePublicKey(pubkey) 82 | if err != nil { 83 | log.Fatalf("unable to parse private key: %v", err) 84 | } 85 | 86 | hostKeyCallback = trustedHostKeyCallback(hostkey) 87 | } 88 | 89 | key, err := os.ReadFile(*keyFile) 90 | if err != nil { 91 | log.Fatalf("unable to read private key: %v", err) 92 | } 93 | signer, err := ssh.ParsePrivateKey(key) 94 | if err != nil { 95 | log.Fatalf("unable to parse private key: %v", err) 96 | } 97 | // An SSH client is represented with a ClientConn. 98 | // 99 | // To authenticate with the remote server you must pass at least one 100 | // implementation of AuthMethod via the Auth field in ClientConfig, 101 | // and provide a HostKeyCallback. 102 | config := &ssh.ClientConfig{ 103 | User: *username, 104 | Auth: []ssh.AuthMethod{ 105 | ssh.PublicKeys(signer), 106 | }, 107 | HostKeyCallback: hostKeyCallback, 108 | } 109 | 110 | client, err := ssh.Dial("tcp", *addr, config) 111 | if err != nil { 112 | log.Panicf("Failed to dial: %v", err) 113 | } 114 | defer client.Close() 115 | 116 | listener, err := client.Listen("tcp", "localhost:"+*remotePort) 117 | if err != nil { 118 | client.Close() 119 | log.Panicf("Failed to listen on remote socket %s: %v", *remotePort, err) 120 | } 121 | defer listener.Close() 122 | 123 | log.Printf("Listening on remote socket %s", *remotePort) 124 | for { 125 | remote, err := listener.Accept() 126 | if err != nil { 127 | log.Printf("Failed to accept connection on remote socket %s: %v", *remotePort, err) 128 | continue 129 | } 130 | log.Printf("Accepted connection on remote socket %s", *remotePort) 131 | go func() { 132 | local, err := net.Dial("unix", *localSocket) 133 | if err != nil { 134 | log.Printf("Failed to dial local socket %s: %v", *localSocket, err) 135 | remote.Close() 136 | return 137 | } 138 | log.Printf("Connected to local socket %s", *localSocket) 139 | fmt.Println("tunnel established with", local.LocalAddr()) 140 | runTunnel(local, remote) 141 | }() 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /cmd/virtual-kubelet/set-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -n ${KUBELET_VERSION} ]; then 4 | cat << EOF > pkg/virtualkubelet/version.go 5 | package virtualkubelet 6 | 7 | var ( 8 | KubeletVersion = "$KUBELET_VERSION" 9 | ) 10 | EOF 11 | fi 12 | -------------------------------------------------------------------------------- /docker/Dockerfile.interlink: -------------------------------------------------------------------------------- 1 | FROM golang:1.22 as build-stage 2 | 3 | WORKDIR /app 4 | 5 | COPY .. . 6 | 7 | ENV GOMODCACHE="/go/pkg/mod" 8 | 9 | ENV GOCACHE="/go/build-cache" 10 | 11 | RUN mkdir -p $GOMODCACHE && mkdir -p $GOCACHE 12 | 13 | ARG VERSION 14 | RUN bash -c "KUBELET_VERSION=${VERSION} ./cmd/virtual-kubelet/set-version.sh" 15 | 16 | RUN --mount=type=cache,target=/go/pkg/mod bash -c "time go mod tidy" 17 | RUN --mount=type=cache,target=/go/build-cache bash -c "time CGO_ENABLED=0 GOOS=linux go build -o bin/interlink cmd/interlink/main.go" 18 | 19 | # Deploy the application binary into a lean image 20 | FROM gcr.io/distroless/base-debian11:latest AS build-release-stage 21 | 22 | WORKDIR / 23 | 24 | COPY --from=build-stage /app/bin/interlink /interlink 25 | 26 | USER nonroot:nonroot 27 | 28 | ENTRYPOINT ["/interlink"] 29 | -------------------------------------------------------------------------------- /docker/Dockerfile.refresh-token: -------------------------------------------------------------------------------- 1 | FROM python:3.10 2 | 3 | RUN pip3 install --no-cache-dir requests==2.31.0 4 | 5 | COPY ../docker/scripts/refresh.py /opt/refresh.py 6 | 7 | ENTRYPOINT ["python3", "/opt/refresh.py"] -------------------------------------------------------------------------------- /docker/Dockerfile.vk: -------------------------------------------------------------------------------- 1 | FROM bitnami/kubectl:1.27.14 as kubectl 2 | 3 | FROM golang:1.22 as build-stage 4 | 5 | WORKDIR /app 6 | 7 | COPY .. . 8 | 9 | ARG VERSION 10 | 11 | ENV GOMODCACHE="/go/pkg/mod" 12 | ENV GOCACHE="/go/build-cache" 13 | 14 | RUN mkdir -p $GOMODCACHE && mkdir -p $GOCACHE 15 | 16 | 17 | RUN bash -c "KUBELET_VERSION=${VERSION} ./cmd/virtual-kubelet/set-version.sh" 18 | RUN --mount=type=cache,target=/go/pkg/mod bash -c "time go mod tidy" 19 | RUN --mount=type=cache,target=/go/build-cache bash -c "time CGO_ENABLED=0 GOOS=linux go build -o bin/vk cmd/virtual-kubelet/main.go" 20 | 21 | # Deploy the application binary into a lean image 22 | FROM ubuntu:22.04 AS build-release-stage 23 | 24 | WORKDIR / 25 | 26 | COPY --from=build-stage /app/bin/vk /vk 27 | 28 | COPY --from=kubectl /opt/bitnami/kubectl/bin/kubectl /usr/local/bin/ 29 | 30 | ENTRYPOINT ["/vk"] 31 | -------------------------------------------------------------------------------- /docker/scripts/refresh.py: -------------------------------------------------------------------------------- 1 | #! /bin/env python3 2 | """ 3 | Functions and scripts to sync OIDC identities on user accounts 4 | """ 5 | 6 | import os 7 | import json 8 | import time 9 | import logging 10 | import requests 11 | from urllib import parse 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | if __name__ == '__main__': 16 | """ 17 | sync OIDC identities on user accounts 18 | """ 19 | try: 20 | verbose = os.environ.get("VERBOSE") 21 | if "True" == verbose: 22 | logging.basicConfig(level=logging.DEBUG) 23 | logger.info("Verbose mode: setting log to debug.") 24 | 25 | iam_grant_type = os.environ.get("IAM_GRANT_TYPE") 26 | iam_server = os.environ.get( 27 | "IAM_TOKEN_ENDPOINT", "https://cms-auth.web.cern.ch/token") 28 | iam_client_id = os.environ.get("IAM_CLIENT_ID") 29 | iam_client_secret = os.environ.get("IAM_CLIENT_SECRET") 30 | iam_refresh_token = os.environ.get("IAM_REFRESH_TOKEN") 31 | audience = os.environ.get("IAM_VK_AUD") 32 | output_file = os.environ.get("TOKEN_PATH", "/opt/interlink/token") 33 | except Exception: 34 | logger.exception() 35 | exit(1) 36 | 37 | while True: 38 | try: 39 | with open(output_file+"-refresh", "r") as text_file: 40 | rt = text_file.readline() 41 | if rt != "": 42 | iam_refresh_token = rt 43 | except: 44 | logger.info("No cache for refresh token, starting from ENV value") 45 | 46 | logger.info("Current refresh token: %s", iam_refresh_token) 47 | token = None 48 | 49 | if iam_grant_type == "client_credentials": 50 | try: 51 | request_data = { 52 | "audience": audience, 53 | "grant_type": iam_grant_type, 54 | "client_id" : iam_client_id, 55 | "client_secret": iam_client_secret 56 | #"scope": "openid profile email address phone offline_access" 57 | } 58 | 59 | from requests.auth import HTTPBasicAuth 60 | auth = HTTPBasicAuth(iam_client_id, iam_client_secret) 61 | headers = {'Content-Type': 'application/x-www-form-urlencoded'} 62 | r = requests.post(iam_server, data=request_data, auth=auth, headers=headers) 63 | logger.debug("Raw response text: %s", r.text) 64 | try: 65 | response = json.loads(r.text) 66 | except: 67 | try: 68 | response = dict(parse.parse_qsl(r.text)) 69 | logger.debug("Response text parsed: %s", response) 70 | except: 71 | exit(1) 72 | 73 | 74 | logger.debug("iam_client_id %s iam_client_secret %s response %s", iam_client_id, iam_client_secret, response) 75 | 76 | token = response['access_token'] 77 | try: 78 | refresh_token = response['refresh_token'] 79 | except: 80 | refresh_token = iam_refresh_token 81 | 82 | 83 | logger.info("Token retrieved") 84 | 85 | ## TODO: collect new refresh token and store it somewhere 86 | with open(output_file+"-refresh", "w") as text_file: 87 | text_file.write(refresh_token) 88 | 89 | with open(output_file, "w") as text_file: 90 | text_file.write(token) 91 | 92 | logger.info(f"Refresh token written in {output_file+'-refresh'}") 93 | 94 | except Exception as e: 95 | logger.warning("ERROR oidc get token: {}".format(e), exc_info=True) 96 | logger.warning("Response if available: %s", response) 97 | 98 | elif iam_grant_type == "authorization_code": 99 | 100 | try: 101 | request_data = { 102 | "audience": audience, 103 | "grant_type": "refresh_token", 104 | "refresh_token": iam_refresh_token, 105 | #"scope": "openid profile email address phone offline_access" 106 | } 107 | 108 | from requests.auth import HTTPBasicAuth 109 | auth = HTTPBasicAuth(iam_client_id, iam_client_secret) 110 | 111 | r = requests.post(iam_server, data=request_data, auth=auth) 112 | print(r.text) 113 | try: 114 | response = json.loads(r.text) 115 | except: 116 | try: 117 | response = dict(parse.parse_qsl(r.text)) 118 | logger.debug(response) 119 | except: 120 | exit(1) 121 | 122 | logger.debug("iam_client_id %s iam_client_secret %s response %s", iam_client_id, iam_client_secret, response) 123 | 124 | token = response['access_token'] 125 | try: 126 | refresh_token = response['refresh_token'] 127 | except: 128 | refresh_token = iam_refresh_token 129 | 130 | 131 | logger.info("Token retrieved") 132 | 133 | ## TODO: collect new refresh token and store it somewhere 134 | with open(output_file+"-refresh", "w") as text_file: 135 | text_file.write(refresh_token) 136 | 137 | 138 | with open(output_file, "w") as text_file: 139 | text_file.write(token) 140 | 141 | logger.info(f"Refresh token written in {output_file+'-refresh'}") 142 | 143 | except Exception as e: 144 | logger.warning("ERROR oidc get token: {}".format(e), exc_info=True) 145 | logger.warning("Response if available: %s", response) 146 | else: 147 | logger.error(f"Invalid grant type {iam_grant_type}", exc_info=True) 148 | exit(1) 149 | time.sleep(200) 150 | -------------------------------------------------------------------------------- /docs/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:@docusaurus/recommended"] 3 | } 4 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator. 4 | 5 | ## Dagger module to site preview 6 | 7 | ```bash 8 | dagger -m github.com/levlaz/daggerverse/docusaurus@f073c72e0a7345bba2173a15269307df297c3c13 call \ (⎈|default:default) 9 | --src ./ serve up 10 | ``` 11 | 12 | ### Installation 13 | 14 | ``` 15 | $ yarn 16 | ``` 17 | 18 | ### Local Development 19 | 20 | ``` 21 | $ yarn start --config docusaurus.config.local.ts 22 | ``` 23 | 24 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 25 | 26 | ### Build 27 | 28 | ``` 29 | $ yarn build 30 | ``` 31 | 32 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 33 | 34 | ### Deployment 35 | 36 | Using SSH: 37 | 38 | ``` 39 | $ USE_SSH=true yarn deploy 40 | ``` 41 | 42 | Not using SSH: 43 | 44 | ``` 45 | $ GIT_USER= yarn deploy 46 | ``` 47 | 48 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 49 | -------------------------------------------------------------------------------- /docs/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /docs/docs/Limitations.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 6 3 | --- 4 | 5 | # Current limitations 6 | 7 | It's not black magic, we have to pay something: 8 | 9 | - **Cluster wide shared FS**: there is no support for cluster-wide filesystem 10 | mounting on the remote container. The only volumes supported are: `Secret`, 11 | `ConfigMap`, `EmptyDir` 12 | - **InCluster pod-to-pod network**: we are in the middle of the beta period to 13 | release this feature! 14 | 15 | :::note 16 | 17 | Reach out to us if you are willing to test the network implementation as beta 18 | users! 19 | 20 | ::: 21 | 22 | That's all. If you find anything else, feel free to let it know filing a github 23 | issue. 24 | -------------------------------------------------------------------------------- /docs/docs/arch.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | import ThemedImage from '@theme/ThemedImage'; 5 | import useBaseUrl from '@docusaurus/useBaseUrl'; 6 | 7 | # Architecture 8 | 9 | InterLink aims to provide an abstraction for the execution of a Kubernetes pod on any remote resource capable of managing a Container execution lifecycle. 10 | 11 | The project consists of two main components: 12 | 13 | - __A Kubernetes Virtual Node:__ based on the [VirtualKubelet](https://virtual-kubelet.io/) technology. Translating request for a kubernetes pod execution into a remote call to the interLink API server. 14 | - __The interLink API server:__ a modular and pluggable REST server where you can create your own Container manager plugin (called sidecars), or use the existing ones: remote docker execution on a remote host, singularity Container on a remote SLURM batch system. 15 | 16 | The project got inspired by the [KNoC](https://github.com/CARV-ICS-FORTH/knoc) and [Liqo](https://github.com/liqotech/liqo/tree/master) projects, enhancing that with the implemention a generic API layer b/w the virtual kubelet component and the provider logic for the container lifecycle management. 17 | 18 | 25 | -------------------------------------------------------------------------------- /docs/docs/cookbook/2-incluster.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | import Tabs from "@theme/Tabs"; 6 | import TabItem from "@theme/TabItem"; 7 | import ThemedImage from "@theme/ThemedImage"; 8 | import useBaseUrl from "@docusaurus/useBaseUrl"; 9 | 10 | # In-cluster deployment 11 | 12 | Deploy interLink in the local K8S cluster. 13 | 14 | 21 | 22 | ## Install interLink 23 | 24 | ### Deploy Kubernetes components 25 | 26 | The deployment of the Kubernetes components are managed by the official 27 | [HELM chart](https://github.com/interlink-hq/interlink-helm-chart). Depending on 28 | the scenario you selected, there might be additional operations to be done. 29 | 30 | - Create an helm values file: 31 | 32 | ```yaml title="values.yaml" 33 | nodeName: interlink-with-socket 34 | 35 | plugin: 36 | enabled: true 37 | image: "plugin docker image here" 38 | command: ["/bin/bash", "-c"] 39 | args: ["/app/plugin"] 40 | config: | 41 | your plugin 42 | configuration 43 | goes here!!! 44 | socket: unix:///var/run/plugin.sock 45 | 46 | interlink: 47 | enabled: true 48 | socket: unix:///var/run/interlink.sock 49 | ``` 50 | 51 | Eventually deploy the latest release of the official: 52 | 53 | ```bash 54 | export INTERLINK_CHART_VERSION="X.X.X" 55 | helm upgrade --install \ 56 | --create-namespace \ 57 | -n interlink \ 58 | my-node \ 59 | oci://ghcr.io/interlink-hq/interlink-helm-chart/interlink \ 60 | --version $INTERLINK_CHART_VERSION \ 61 | --values ./interlink/manifests/values.yaml 62 | ``` 63 | 64 | :::warning 65 | 66 | Remember to pick the 67 | [version of the chart](https://github.com/interlink-hq/interlink-helm-chart/blob/main/interlink/Chart.yaml#L18) 68 | and put it into the `INTERLINK_CHART_VERSION` env var above. 69 | 70 | ::: 71 | 72 | Whenever you see the node ready, you are good to go! 73 | 74 | :::note 75 | 76 | You can find a demo pod to test your setup 77 | [here](../guides/develop-a-plugin#lets-test-is-out). 78 | 79 | ::: 80 | 81 | To start debugging in case of problems we suggest starting from the pod 82 | containers logs! 83 | -------------------------------------------------------------------------------- /docs/docs/cookbook/3-tunneled.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | import Tabs from "@theme/Tabs"; 6 | import TabItem from "@theme/TabItem"; 7 | import ThemedImage from "@theme/ThemedImage"; 8 | import useBaseUrl from "@docusaurus/useBaseUrl"; 9 | 10 | # Tunneled deployment 11 | 12 | Deploy interLink components in both systems, linked through a tunnelled 13 | communication. 14 | 15 | 22 | 23 | ## Install interLink 24 | 25 | ### Deploy Remote components 26 | 27 | :::note 28 | 29 | We do have case studies already implementing it, if you are interested reach out 30 | to the slack channel. 31 | 32 | ::: 33 | -------------------------------------------------------------------------------- /docs/docs/cookbook/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Cookbook", 3 | "position": 3, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "Practical recipes for different deployment scenarios." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /docs/docs/guides/01-deploy-interlink.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | import ThemedImage from "@theme/ThemedImage"; 6 | import useBaseUrl from "@docusaurus/useBaseUrl"; 7 | 8 | # Deploy your plugin 9 | 10 | ## Attach your favorite plugin or develop one! (remote host) 11 | 12 | [Next chapter](./develop-a-plugin) will show the basics for developing a new 13 | plugin following the interLink openAPI spec. 14 | 15 | In alterative you can start an already supported one. 16 | 17 | ### Remote SLURM job submission 18 | 19 | If you manage a SLURM batch system, and you satisfy the requirements below, you 20 | can offload pod from a kubernetes cluster to your batch system, using interLink 21 | SLURM plugin of course. 22 | 23 | - [github.com/interlink-hq/interlink-slurm-plugin](https://github.com/interlink-hq/interlink-slurm-plugin) 24 | 25 | #### Requirements 26 | 27 | - a SLURM CLI available on the remote host and configured to interact with the 28 | computing cluster 29 | - a sharedFS with all the worker nodes 30 | - an experimental feature is available for cases in which this is not possible 31 | 32 | #### Configuration 33 | 34 | Please refer to either the plugin repository or the 35 | [cookbook](../cookbook/1-edge.mdx) for more information. 36 | 37 | ### Create UNICORE jobs to run on HPC centers 38 | 39 | [UNICORE](https://www.unicore.eu/) (Uniform Interface to Computing Resources) 40 | offers a ready-to-run system including client and server software. UNICORE makes 41 | distributed computing and data resources available in a seamless and secure way 42 | in intranets and the internet. 43 | 44 | - [UNICORE plugin](https://github.com/interlink-hq/interlink-unicore-plugin) 45 | 46 | #### Configuration 47 | 48 | Please refer to either the plugin repository for more information. 49 | 50 | ### Remote docker execution 51 | 52 | You get a VM from you cloud provider, with some GPUs maybe. You can attach it to 53 | your Kubernetes cluster using interLink docker plugin. 54 | 55 | - [Docker plugin repository](https://github.com/interlink-hq/interlink-docker-plugin) 56 | 57 | #### Configuration 58 | 59 | Please refer to either the plugin repository or the 60 | [cookbook](../cookbook/1-edge.mdx) for more information. 61 | 62 | ### Submit pods to HTcondor or ARC batch systems 63 | 64 | - [HTCondor plugin repository](https://github.com/interlink-hq/interlink-htcondor-plugin) 65 | - [ARC plugin repository](https://github.com/interlink-hq/interlink-arc-plugin) 66 | 67 | ### Remote Kubernetes Plugin 68 | 69 | InterLink plugin to extend the capabilities of existing Kubernetes clusters, 70 | enabling them to offload workloads to another remote cluster. The plugin 71 | supports the offloading of PODs that expose HTTP endpoints (i.e., HTTP 72 | Microservices). 73 | 74 | - [Interlink Kubernetes Plugin](https://baltig.infn.it/mgattari/interlink-kubernetes-plugin) 75 | 76 | #### Configuration 77 | 78 | Please refer to either the plugin repository or the 79 | [cookbook](../cookbook/1-edge.mdx) for more information. 80 | 81 | ## Test your setup 82 | 83 | Please find a demo pod to test your setup 84 | [here](./develop-a-plugin#lets-test-is-out). 85 | -------------------------------------------------------------------------------- /docs/docs/guides/03-api-reference.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | # OpenAPI references 6 | 7 | ## Plugin API spec 8 | 9 | Please find the sidecar OpenAPI JSON spec can be found [here](/plugin-openapi). 10 | 11 | import ApiDocMdx from "@theme/ApiDocMdx"; 12 | 13 | 14 | 15 | ## interLink API spec 16 | 17 | Please find the interLink OpenAPI JSON spec can be found 18 | [here](/interlink-openapi). 19 | 20 | 21 | -------------------------------------------------------------------------------- /docs/docs/guides/04-oidc-IAM.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | --- 4 | 5 | import ThemedImage from "@theme/ThemedImage"; 6 | import useBaseUrl from "@docusaurus/useBaseUrl"; 7 | 8 | # Configure OpenID connect identity providers 9 | 10 | We support any OpenID compliant identity provider and also GitHub authN 11 | workflow. 12 | 13 | ## GitHub authN 14 | 15 | ### Requirements 16 | 17 | - **kubectl host**: an host with MiniKube installed and running 18 | - A GitHub account 19 | - **remote host**: A "remote" machine with a port that is reachable by the 20 | MiniKube host 21 | 22 | ### Create an OAuth GitHub app 23 | 24 | As a first step, you need to create a GitHub OAuth application to allow 25 | interLink to make authentication between your Kubernetes cluster and the remote 26 | endpoint. 27 | 28 | Head to [https://github.com/settings/apps](https://github.com/settings/apps) and 29 | click on `New GitHub App`. You should now be looking at a form like this: 30 | 31 | 35 | 36 | Provide a name for the OAuth2 application, e.g. `interlink-demo-test`, and you 37 | can skip the description, unless you want to provide one for future reference. 38 | For our purpose Homepage reference is also not used, so fill free to put there 39 | `https://interlink-hq.github.io/interLink/`. 40 | 41 | Check now that refresh token and device flow authentication: 42 | 43 | 47 | 48 | Disable webhooks and save clicking on `Create GitHub App` 49 | 50 | 54 | 55 | You can click then on your application that should now appear at 56 | [https://github.com/settings/apps](https://github.com/settings/apps) and you 57 | need to save two strings: the `Client ID` and clicking on 58 | `Generate a new client secret` you should be able to note down the relative 59 | `Client Secret`. 60 | 61 | Now it's all set for the next steps. You should be able to set it for 62 | authenticating the virtual kubelet with the interLink remote components with the 63 | following piece of the installer configuration: 64 | 65 | ```yaml 66 | oauth: 67 | provider: github 68 | issuer: https://github.com/oauth 69 | grant_type: authorization_code 70 | scopes: 71 | - "read:user" 72 | github_user: "GH USERNAME HERE" 73 | token_url: "https://github.com/login/oauth/access_token" 74 | device_code_url: "https://github.com/login/device/code" 75 | client_id: "XXXXXXX" 76 | client_secret: "XXXXXXXX" 77 | ``` 78 | 79 | ## EGI Check-in 80 | 81 | If you have an account for [EGI check-in](https://aai.egi.eu), you should be 82 | able to set it for authenticating the virtual kubelet with the interLink remote 83 | components with the following piece of the installer configuration: 84 | 85 | ```yaml 86 | oauth: 87 | provider: oidc 88 | issuer: https://aai.egi.eu/auth/realms/egi 89 | scopes: 90 | - "openid" 91 | - "email" 92 | - "offline_access" 93 | - "profile" 94 | audience: interlink 95 | grant_type: authorization_code 96 | group_claim: email 97 | group: "YOUR EMAIL HERE" 98 | token_url: "https://aai.egi.eu/auth/realms/egi/protocol/openid-connect/token" 99 | device_code_url: "https://aai.egi.eu/auth/realms/egi/protocol/openid-connect/auth/device" 100 | client_id: "oidc-agent" 101 | client_secret: "" 102 | ``` 103 | 104 | :::danger 105 | 106 | Remember to put your email in the group field! 107 | 108 | ::: 109 | 110 | ## Indigo IAM 111 | 112 | If you have an account for [Indigo IAM](https://iam.cloud.infn.it/), you should 113 | be able to set it for authenticating the virtual kubelet with the interLink 114 | remote components. Follow those steps to setup a new client in the IAM portal 115 | and get the necessary information to fill the configuration. This guide is 116 | specific for the IAM portal 117 | [https://iam.cloud.infn.it/](https://iam.cloud.infn.it/) but it should be 118 | similar for other IAM portals that are OpenID compliant. 119 | 120 | 1. Go to the [IAM portal](https://iam.cloud.infn.it/) and log in. 121 | 2. After logging in, click on the `My clients` tab on the left side of the page 122 | and then select `New client` as shown in the images below. 123 | 124 | ![Go to my clients](./img/iam-client0.png) 125 | ![Create a new client](./img/iam-client1.png) 3. Set a name you prefer for the 126 | client. 4. Select the `Scopes` tab and add the following scopes: `openid`, 127 | `email`, `offline_access`, `profile`, `wlcg`, `wlcg.groups`. 5. Select the 128 | `Grant types` tab and add the following grant types: `authorization_code`, 129 | `client_credentials`, `refresh_token`, 130 | `urn:ietf:params:oauth:grant-type:device_code`. 6. Save the client by pressing 131 | the `Save client` button. 132 | 133 | After creating the client, you will be able to see the new client in the 134 | `My clients` page as show in the image below. 135 | 136 | ![Check the created client](./img/iam-client2.png) 137 | 138 | You can click on the client to see the client details. You will find the 139 | `Client id` under the `Main` tab and the `Client secret` under the `Credentials` 140 | tab. Now, with those information, you can fill this piece of the installer 141 | configuration: 142 | 143 | ```yaml 144 | oauth: 145 | provider: oidc 146 | issuer: "https://iam.cloud.infn.it/" 147 | scopes: 148 | - "openid" 149 | - "email" 150 | - "offline_access" 151 | - "profile" 152 | audience: users 153 | grant_type: authorization_code 154 | group_claim: email 155 | group: "YOUR EMAIL HERE" 156 | token_url: "https://iam.cloud.infn.it/token" 157 | device_code_url: "https://iam.cloud.infn.it/devicecode" 158 | client_id: "YOUR CLIENT ID HERE" 159 | client_secret: "YOUR CLIENT SECRET HERE" 160 | ``` 161 | -------------------------------------------------------------------------------- /docs/docs/guides/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Admin guides", 3 | "position": 4, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "Learn how to deploy and adapt interLink plugins for your use case." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /docs/docs/guides/img/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interlink-hq/interLink/dc21f076d4f86e425f45d94e8ed1d6cc654bcd99/docs/docs/guides/img/dashboard.png -------------------------------------------------------------------------------- /docs/docs/guides/img/docsVersionDropdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interlink-hq/interLink/dc21f076d4f86e425f45d94e8ed1d6cc654bcd99/docs/docs/guides/img/docsVersionDropdown.png -------------------------------------------------------------------------------- /docs/docs/guides/img/iam-client0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interlink-hq/interLink/dc21f076d4f86e425f45d94e8ed1d6cc654bcd99/docs/docs/guides/img/iam-client0.png -------------------------------------------------------------------------------- /docs/docs/guides/img/iam-client1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interlink-hq/interLink/dc21f076d4f86e425f45d94e8ed1d6cc654bcd99/docs/docs/guides/img/iam-client1.png -------------------------------------------------------------------------------- /docs/docs/guides/img/iam-client2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interlink-hq/interLink/dc21f076d4f86e425f45d94e8ed1d6cc654bcd99/docs/docs/guides/img/iam-client2.png -------------------------------------------------------------------------------- /docs/docs/guides/img/localeDropdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interlink-hq/interLink/dc21f076d4f86e425f45d94e8ed1d6cc654bcd99/docs/docs/guides/img/localeDropdown.png -------------------------------------------------------------------------------- /docs/docs/guides/img/vk_tracing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interlink-hq/interLink/dc21f076d4f86e425f45d94e8ed1d6cc654bcd99/docs/docs/guides/img/vk_tracing.png -------------------------------------------------------------------------------- /docs/docusaurus.config.local.ts: -------------------------------------------------------------------------------- 1 | import {themes as prismThemes} from 'prism-react-renderer'; 2 | import type {Config} from '@docusaurus/types'; 3 | import type * as Preset from '@docusaurus/preset-classic'; 4 | import type * as Redocusaurus from 'redocusaurus'; 5 | 6 | const config: Config = { 7 | title: 'interLink', 8 | tagline: 'Your Virtual Kubelet ecosystem!', 9 | favicon: 'img/favicon.ico', 10 | 11 | // Set the production url of your site here 12 | url: 'https://interlink-hq.github.io', 13 | // Set the // pathname under which your site is served 14 | // For GitHub pages deployment, it is often '//' 15 | baseUrl: '/', 16 | 17 | // GitHub pages deployment config. 18 | // If you aren't using GitHub pages, you don't need these. 19 | organizationName: 'INFN', // Usually your GitHub org/user name. 20 | projectName: 'interLink', // Usually your repo name. 21 | 22 | onBrokenLinks: 'throw', 23 | onBrokenMarkdownLinks: 'warn', 24 | 25 | // Even if you don't use internationalization, you can use this field to set 26 | // useful metadata like html lang. For example, if your site is Chinese, you 27 | // may want to replace "en" with "zh-Hans". 28 | i18n: { 29 | defaultLocale: 'en', 30 | locales: ['en'], 31 | }, 32 | 33 | presets: [ 34 | [ 35 | 'classic', 36 | { 37 | docs: { 38 | sidebarPath: './sidebars.ts', 39 | // Please change this to your repo. 40 | // Remove this to remove the "edit this page" links. 41 | editUrl: 42 | 'https://github.com/interlink-hq/interLink', 43 | }, 44 | blog: false, 45 | theme: { 46 | customCss: './src/css/custom.css', 47 | }, 48 | } satisfies Preset.Options, 49 | ], 50 | [ 51 | 'redocusaurus', 52 | { 53 | // Plugin Options for loading OpenAPI files 54 | specs: [ 55 | // Pass it a path to a local OpenAPI YAML file 56 | { 57 | // Redocusaurus will automatically bundle your spec into a single file during the build 58 | id: 'plugin-api', 59 | spec: 'openapi/plugin-openapi.json', 60 | route: '/plugin-openapi/', 61 | }, 62 | { 63 | // Redocusaurus will automatically bundle your spec into a single file during the build 64 | id: 'interlink-api', 65 | spec: 'openapi/interlink-openapi.json', 66 | route: '/interlink-openapi/', 67 | }, 68 | ], 69 | // Theme Options for modifying how redoc renders them 70 | theme: { 71 | // Change with your site colors 72 | primaryColor: '#1890ff', 73 | }, 74 | }, 75 | ], 76 | 77 | ], 78 | 79 | themeConfig: { 80 | announcementBar: { 81 | id: 'support_us', 82 | content: 83 | 'We are onboarding for our contribution to CNCF Sandbox! Please let us know for any broken or missing information as we move to the new home.', 84 | backgroundColor: '#fafbfc', 85 | textColor: '#091E42', 86 | isCloseable: false, 87 | }, 88 | // Replace with your project's social card 89 | image: 'img/img/interlink_logo.png', 90 | navbar: { 91 | title: 'Home', 92 | logo: { 93 | alt: 'interLink Logo', 94 | src: 'img/interlink_logo.png', 95 | }, 96 | items: [ 97 | { 98 | type: 'docSidebar', 99 | sidebarId: 'tutorialSidebar', 100 | position: 'left', 101 | label: 'Docs', 102 | }, 103 | { 104 | href: 'https://github.com/interlink-hq/interLink', 105 | label: 'GitHub', 106 | position: 'right', 107 | }, 108 | ], 109 | }, 110 | footer: { 111 | style: 'dark', 112 | links: [ 113 | { 114 | title: 'Docs', 115 | items: [ 116 | { 117 | label: 'Docs', 118 | to: '/docs/intro', 119 | }, 120 | ], 121 | }, 122 | { 123 | title: 'Community', 124 | items: [ 125 | { 126 | label: 'interTwin project Slack', 127 | href: 'https://join.slack.com/t/intertwin/shared_invite/zt-2cs67h9wz-2DFQ6EiSQGS1vlbbbJHctA', 128 | } 129 | ], 130 | }, 131 | { 132 | title: 'More', 133 | items: [ 134 | { 135 | label: 'GitHub', 136 | href: 'https://github.com/interlink-hq/interLink', 137 | }, 138 | ], 139 | }, 140 | ], 141 | copyright: `Originally created by INFN - Copyright © interLink a Series of LF Projects, LLC.`, 142 | }, 143 | prism: { 144 | theme: prismThemes.github, 145 | darkTheme: prismThemes.dracula, 146 | }, 147 | } satisfies Preset.ThemeConfig, 148 | }; 149 | 150 | export default config; 151 | -------------------------------------------------------------------------------- /docs/docusaurus.config.ts: -------------------------------------------------------------------------------- 1 | import {themes as prismThemes} from 'prism-react-renderer'; 2 | import type {Config} from '@docusaurus/types'; 3 | import type * as Preset from '@docusaurus/preset-classic'; 4 | import type * as Redocusaurus from 'redocusaurus'; 5 | 6 | const config: Config = { 7 | title: 'interLink', 8 | tagline: 'Your Virtual Kubelet ecosystem!', 9 | favicon: 'img/favicon.ico', 10 | 11 | // Set the production url of your site here 12 | url: 'https://interlink-hq.github.io', 13 | // Set the // pathname under which your site is served 14 | // For GitHub pages deployment, it is often '//' 15 | baseUrl: '/interLink/', 16 | 17 | // GitHub pages deployment config. 18 | // If you aren't using GitHub pages, you don't need these. 19 | organizationName: 'interlink-hq', // Usually your GitHub org/user name. 20 | projectName: 'interLink', // Usually your repo name. 21 | 22 | onBrokenLinks: 'throw', 23 | onBrokenMarkdownLinks: 'warn', 24 | 25 | // Even if you don't use internationalization, you can use this field to set 26 | // useful metadata like html lang. For example, if your site is Chinese, you 27 | // may want to replace "en" with "zh-Hans". 28 | i18n: { 29 | defaultLocale: 'en', 30 | locales: ['en'], 31 | }, 32 | 33 | presets: [ 34 | [ 35 | 'classic', 36 | { 37 | docs: { 38 | sidebarPath: './sidebars.ts', 39 | // Please change this to your repo. 40 | // Remove this to remove the "edit this page" links. 41 | editUrl: 42 | 'https://github.com/interlink-hq/interLink', 43 | }, 44 | blog: false, 45 | theme: { 46 | customCss: './src/css/custom.css', 47 | }, 48 | } satisfies Preset.Options, 49 | ], 50 | [ 51 | 'redocusaurus', 52 | { 53 | // Plugin Options for loading OpenAPI files 54 | specs: [ 55 | // Pass it a path to a local OpenAPI YAML file 56 | { 57 | // Redocusaurus will automatically bundle your spec into a single file during the build 58 | id: 'plugin-api', 59 | spec: 'openapi/plugin-openapi.json', 60 | route: '/plugin-openapi/', 61 | }, 62 | { 63 | // Redocusaurus will automatically bundle your spec into a single file during the build 64 | id: 'interlink-api', 65 | spec: 'openapi/interlink-openapi.json', 66 | route: '/interlink-openapi/', 67 | }, 68 | ], 69 | // Theme Options for modifying how redoc renders them 70 | theme: { 71 | // Change with your site colors 72 | primaryColor: '#1890ff', 73 | }, 74 | }, 75 | ], 76 | 77 | ], 78 | 79 | themeConfig: { 80 | announcementBar: { 81 | id: 'support_us', 82 | content: 83 | 'We are onboarding for our contribution to CNCF Sandbox! Please let us know for any broken or missing information as we move to the new home.', 84 | backgroundColor: '#fafbfc', 85 | textColor: '#091E42', 86 | isCloseable: false, 87 | }, 88 | 89 | // Replace with your project's social card 90 | image: 'img/img/interlink_logo.png', 91 | navbar: { 92 | title: 'Home', 93 | logo: { 94 | alt: 'interLink Logo', 95 | src: 'img/interlink_logo.png', 96 | }, 97 | items: [ 98 | { 99 | type: 'docSidebar', 100 | sidebarId: 'tutorialSidebar', 101 | position: 'left', 102 | label: 'Docs', 103 | }, 104 | { 105 | href: 'https://github.com/interlink-hq/interLink', 106 | label: 'GitHub', 107 | position: 'right', 108 | }, 109 | ], 110 | }, 111 | footer: { 112 | style: 'dark', 113 | links: [ 114 | { 115 | title: 'Docs', 116 | items: [ 117 | { 118 | label: 'Docs', 119 | to: '/docs/intro', 120 | }, 121 | ], 122 | }, 123 | { 124 | title: 'Community', 125 | items: [ 126 | { 127 | label: 'interTwin project Slack', 128 | href: 'https://join.slack.com/t/intertwin/shared_invite/zt-2cs67h9wz-2DFQ6EiSQGS1vlbbbJHctA', 129 | } 130 | ], 131 | }, 132 | { 133 | title: 'More', 134 | items: [ 135 | { 136 | label: 'GitHub', 137 | href: 'https://github.com/interlink-hq/interLink', 138 | }, 139 | ], 140 | }, 141 | ], 142 | copyright: `Originally created by INFN - Copyright © interLink a Series of LF Projects, LLC.`, 143 | }, 144 | prism: { 145 | theme: prismThemes.github, 146 | darkTheme: prismThemes.dracula, 147 | }, 148 | } satisfies Preset.ThemeConfig, 149 | }; 150 | 151 | export default config; 152 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids", 15 | "typecheck": "tsc" 16 | }, 17 | "dependencies": { 18 | "@docusaurus/core": "3.0.1", 19 | "@docusaurus/preset-classic": "3.0.1", 20 | "@mdx-js/react": "^3.0.0", 21 | "clsx": "^2.0.0", 22 | "prism-react-renderer": "^2.3.0", 23 | "raw-loader": "^4.0.2", 24 | "react": "^18.0.0", 25 | "react-dom": "^18.0.0", 26 | "redocusaurus": "^2.0.2" 27 | }, 28 | "devDependencies": { 29 | "@docusaurus/eslint-plugin": "^3.5.2", 30 | "@docusaurus/module-type-aliases": "3.0.1", 31 | "@docusaurus/tsconfig": "3.0.1", 32 | "@docusaurus/types": "3.0.1", 33 | "typescript": "~5.2.2" 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.5%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 3 chrome version", 43 | "last 3 firefox version", 44 | "last 5 safari version" 45 | ] 46 | }, 47 | "engines": { 48 | "node": ">=18.0" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /docs/sidebars.ts: -------------------------------------------------------------------------------- 1 | import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; 2 | 3 | /** 4 | * Creating a sidebar enables you to: 5 | - create an ordered group of docs 6 | - render a sidebar for each doc of that group 7 | - provide next/previous navigation 8 | 9 | The sidebars can be generated from the filesystem, or explicitly defined here. 10 | 11 | Create as many sidebars as you want. 12 | */ 13 | const sidebars: SidebarsConfig = { 14 | // By default, Docusaurus generates a sidebar from the docs folder structure 15 | tutorialSidebar: [{type: 'autogenerated', dirName: '.'}], 16 | 17 | // But you can create a sidebar manually 18 | /* 19 | tutorialSidebar: [ 20 | 'intro', 21 | 'hello', 22 | { 23 | type: 'category', 24 | label: 'Tutorial', 25 | items: ['tutorial-basics/create-a-document'], 26 | }, 27 | ], 28 | */ 29 | }; 30 | 31 | export default sidebars; 32 | -------------------------------------------------------------------------------- /docs/src/components/AdoptersFeatures/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import Heading from '@theme/Heading'; 3 | import styles from './styles.module.css'; 4 | 5 | type FeatureItem = { 6 | title: string; 7 | Svg: React.ComponentType>; 8 | description: JSX.Element; 9 | }; 10 | 11 | const FeatureList: FeatureItem[] = [ 12 | { 13 | title: 'INFN', 14 | Svg: require('@site/static/img/INFN_logo_sito.svg').default, 15 | description: ( 16 | <> 17 | 18 | ), 19 | }, 20 | { 21 | title: 'EGI', 22 | Svg: require('@site/static/img/egi-logo.svg').default, 23 | description: ( 24 | <> 25 | 26 | ), 27 | }, 28 | { 29 | title: 'CERN', 30 | Svg: require('@site/static/img/cern-logo.svg').default, 31 | description: ( 32 | <> 33 | 34 | ), 35 | }, 36 | { 37 | title: 'Universitat Politècnica de València', 38 | Svg: require('@site/static/img/logo-upv.svg').default, 39 | description: ( 40 | <> 41 | 42 | ), 43 | }, 44 | { 45 | title: 'CNES', 46 | Svg: require('@site/static/img/logo-cnes.svg').default, 47 | description: ( 48 | <> 49 | 50 | ), 51 | }, 52 | { 53 | title: 'IJS', 54 | Svg: require('@site/static/img/logo-ijs.svg').default, 55 | description: ( 56 | <> 57 | 58 | ), 59 | }, 60 | { 61 | title: 'IZUM', 62 | Svg: require('@site/static/img/logo-izum.svg').default, 63 | description: ( 64 | <> 65 | 66 | ), 67 | }, 68 | { 69 | title: 'JSC', 70 | Svg: require('@site/static/img/logo-jsc.svg').default, 71 | description: ( 72 | <> 73 | 74 | ), 75 | }, 76 | { 77 | title: 'NuNet', 78 | Svg: require('@site/static/img/logo-nunet.svg').default, 79 | description: ( 80 | <> 81 | 82 | ), 83 | }, 84 | { 85 | title: 'HelixML', 86 | Svg: require('@site/static/img/logo-helix.svg').default, 87 | description: ( 88 | <> 89 | 90 | ), 91 | }, 92 | ]; 93 | 94 | function Feature({title, Svg, description}: FeatureItem) { 95 | return ( 96 |
97 |
98 | 99 |
100 |

101 |
102 | ); 103 | } 104 | 105 | export default function AdoptersFeatures(): JSX.Element { 106 | return ( 107 |
108 |
109 | 110 | Evaluators and contributors 111 |

Find out more in the ADOPTERS.md document!

112 |
113 |
114 | {FeatureList.map((props, idx) => ( 115 | 116 | ))} 117 |
118 |
119 |
120 | ); 121 | } 122 | -------------------------------------------------------------------------------- /docs/src/components/AdoptersFeatures/styles.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | display: flex; 3 | align-items: center; 4 | padding: 2rem 0; 5 | width: 100%; 6 | } 7 | 8 | .featureSvg { 9 | height: 300px; 10 | width: 300px; 11 | align-items: center; 12 | justify-content: center; 13 | } 14 | -------------------------------------------------------------------------------- /docs/src/components/HomepageFeatures/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import Heading from '@theme/Heading'; 3 | import styles from './styles.module.css'; 4 | 5 | type FeatureItem = { 6 | title: string; 7 | Svg: React.ComponentType>; 8 | description: JSX.Element; 9 | }; 10 | 11 | const FeatureList: FeatureItem[] = [ 12 | { 13 | title: 'Seamless end-user experience', 14 | Svg: require('@site/static/img/home-1.svg').default, 15 | description: ( 16 | <> 17 | Keep using all your data science frameworks just like you are 18 | interacting with a physical Kubernetes cluster. 19 | 20 | ), 21 | }, 22 | { 23 | title: 'Customize only What Matters', 24 | Svg: require('@site/static/img/home-2.svg').default, 25 | description: ( 26 | <> 27 | {/*Docusaurus lets you focus on your docs, and we'll do the chores. Go 28 | ahead and move your docs into the docs directory.*/} 29 | Forget about complex API's and kubelet internals; 30 | focus on a simple REST interface for managing container lifecycle as you wish. 31 | 32 | ), 33 | }, 34 | { 35 | title: 'Integrate your resources quickly', 36 | Svg: require('@site/static/img/home-3.svg').default, 37 | description: ( 38 | <> 39 | Integrate resources hosted on remote batch systems or FaaS instances in matter 40 | of minutes thanks to interLink plugin-based architecture. 41 | 42 | ), 43 | }, 44 | ]; 45 | 46 | function Feature({title, Svg, description}: FeatureItem) { 47 | return ( 48 |
49 |
50 | 51 |
52 |
53 | {title} 54 |

{description}

55 |
56 |
57 | ); 58 | } 59 | 60 | export default function HomepageFeatures(): JSX.Element { 61 | return ( 62 |
63 |
64 |
65 | {FeatureList.map((props, idx) => ( 66 | 67 | ))} 68 |
69 |
70 |
71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /docs/src/components/HomepageFeatures/styles.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | display: flex; 3 | align-items: center; 4 | padding: 2rem 0; 5 | width: 100%; 6 | } 7 | 8 | .featureSvg { 9 | height: 300px; 10 | width: 300px; 11 | } 12 | -------------------------------------------------------------------------------- /docs/src/components/HomepageSchema/index.tsx: -------------------------------------------------------------------------------- 1 | import Heading from '@theme/Heading'; 2 | import styles from './styles.module.css'; 3 | import ThemedImage from '@theme/ThemedImage'; 4 | import useBaseUrl from '@docusaurus/useBaseUrl'; 5 | 6 | 7 | 8 | export default function HomepageSchema(): JSX.Element { 9 | return ( 10 |
11 |
12 |
13 | 14 | Translate your pod request into a remote lifecycle command set 15 | 16 | 17 | You just have to select a (virtual)node as the pod destination. 18 | 19 | 26 |
27 |
28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /docs/src/components/HomepageSchema/styles.module.css: -------------------------------------------------------------------------------- 1 | .featureSvg { 2 | display: flex; 3 | height: 100%; 4 | width: 100%; 5 | } 6 | .features { 7 | display: flex; 8 | align-items: center; 9 | padding: 2rem 0; 10 | width: 100%; 11 | } -------------------------------------------------------------------------------- /docs/src/components/HomepageVideo/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import Heading from '@theme/Heading'; 3 | import styles from './styles.module.css'; 4 | 5 | export default function HomepageVideo(): JSX.Element { 6 | return ( 7 |
8 |
9 | 10 | Video material 11 | 12 | 13 |
14 | 15 | Interlink overview at Kubecon colocated CloudNative AI Day 16 | 17 | 18 |
19 |
20 | 21 | SLURM at a EuroHPC is at your hand with interLink 22 | 23 | 24 |
25 |
26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /docs/src/components/HomepageVideo/styles.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | display: flex; 3 | align-items: center; 4 | padding: 2rem 0; 5 | width: 100%; 6 | } 7 | -------------------------------------------------------------------------------- /docs/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Any CSS included here will be global. The classic template 3 | * bundles Infima by default. Infima is a CSS framework designed to 4 | * work well for content-centric websites. 5 | */ 6 | 7 | /* You can override the default Infima variables here. */ 8 | :root { 9 | --ifm-color-primary: #6a479e; 10 | --ifm-color-primary-dark: #5f408e; 11 | --ifm-color-primary-darker: #5a3c86; 12 | --ifm-color-primary-darkest: #4a326f; 13 | --ifm-color-primary-light: #754eae; 14 | --ifm-color-primary-lighter: #7b55b3; 15 | --ifm-color-primary-lightest: #8d6dbd; 16 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); 17 | } 18 | 19 | /* For readability concerns, you should choose a lighter palette in dark mode. */ 20 | [data-theme='dark'] { 21 | --ifm-color-primary: #ffd3b7; 22 | --ifm-color-primary-dark: #ffb88b; 23 | --ifm-color-primary-darker: #ffab75; 24 | --ifm-color-primary-darkest: #ff8334; 25 | --ifm-color-primary-light: #ffeee3; 26 | --ifm-color-primary-lighter: #fffbf9; 27 | --ifm-color-primary-lightest: #ffffff; 28 | --ifm-background-color: #000000; 29 | --ifm-code-font-size: 100%; 30 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); 31 | } 32 | -------------------------------------------------------------------------------- /docs/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS files with the .module.css suffix will be treated as CSS modules 3 | * and scoped locally. 4 | */ 5 | 6 | .heroBanner { 7 | padding: 4rem 0; 8 | text-align: center; 9 | position: relative; 10 | overflow: hidden; 11 | } 12 | 13 | .logo { 14 | padding: 1rem 0; 15 | text-align: center; 16 | position: relative; 17 | overflow: hidden; 18 | } 19 | 20 | @media screen and (max-width: 996px) { 21 | .heroBanner { 22 | padding: 2rem; 23 | } 24 | } 25 | 26 | .buttons { 27 | display: flex; 28 | align-items: center; 29 | justify-content: center; 30 | } 31 | -------------------------------------------------------------------------------- /docs/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import Link from '@docusaurus/Link'; 3 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; 4 | import Layout from '@theme/Layout'; 5 | import HomepageFeatures from '@site/src/components/HomepageFeatures'; 6 | import HomepageVideo from '@site/src/components/HomepageVideo'; 7 | import HomepageSchema from '@site/src/components/HomepageSchema'; 8 | import Heading from '@theme/Heading'; 9 | import ThemedImage from '@theme/ThemedImage'; 10 | import useBaseUrl from '@docusaurus/useBaseUrl'; 11 | 12 | import styles from './index.module.css'; 13 | import AdoptersFeatures from '../components/AdoptersFeatures'; 14 | 15 | function HomepageHeader() { 16 | const {siteConfig} = useDocusaurusContext(); 17 | return ( 18 |
19 |
20 | 21 | 29 | 30 | 31 | 32 | {siteConfig.tagline} 33 | 34 |
35 | 38 | Try it out! 🚀 39 | 40 | 41 |
42 | Stars window.location.href='https://github.com/interlink-hq/interLink'}/> 43 |
44 | GoReport window.location.href='https://goreportcard.com/report/github.com/interlink-hq/interlink'}/> 45 |
46 | Slack window.location.href='https://join.slack.com/t/intertwin/shared_invite/zt-2cs67h9wz-2DFQ6EiSQGS1vlbbbJHctA'}/> 47 |
48 |
49 | ); 50 | } 51 | 52 | export default function Home(): JSX.Element { 53 | const {siteConfig} = useDocusaurusContext(); 54 | return ( 55 | 58 | 59 |
60 | 61 |
62 | 63 |
64 | 65 |
66 | 67 | CNCF contribution 68 | 69 |

interLink is a Cloud Native Computing Foundation Sandbox project

70 | 71 |

The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.

72 |
73 |
74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /docs/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interlink-hq/interLink/dc21f076d4f86e425f45d94e8ed1d6cc654bcd99/docs/static/.nojekyll -------------------------------------------------------------------------------- /docs/static/img/37a0d3_bd169579737d47318ca1b1735db6e497~mv2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interlink-hq/interLink/dc21f076d4f86e425f45d94e8ed1d6cc654bcd99/docs/static/img/37a0d3_bd169579737d47318ca1b1735db6e497~mv2.webp -------------------------------------------------------------------------------- /docs/static/img/INFN_logo_sito.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interlink-hq/interLink/dc21f076d4f86e425f45d94e8ed1d6cc654bcd99/docs/static/img/INFN_logo_sito.png -------------------------------------------------------------------------------- /docs/static/img/cern-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interlink-hq/interLink/dc21f076d4f86e425f45d94e8ed1d6cc654bcd99/docs/static/img/cern-logo.png -------------------------------------------------------------------------------- /docs/static/img/docusaurus-social-card.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interlink-hq/interLink/dc21f076d4f86e425f45d94e8ed1d6cc654bcd99/docs/static/img/docusaurus-social-card.jpg -------------------------------------------------------------------------------- /docs/static/img/docusaurus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interlink-hq/interLink/dc21f076d4f86e425f45d94e8ed1d6cc654bcd99/docs/static/img/docusaurus.png -------------------------------------------------------------------------------- /docs/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interlink-hq/interLink/dc21f076d4f86e425f45d94e8ed1d6cc654bcd99/docs/static/img/favicon.ico -------------------------------------------------------------------------------- /docs/static/img/github-app-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interlink-hq/interLink/dc21f076d4f86e425f45d94e8ed1d6cc654bcd99/docs/static/img/github-app-new.png -------------------------------------------------------------------------------- /docs/static/img/github-app-new2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interlink-hq/interLink/dc21f076d4f86e425f45d94e8ed1d6cc654bcd99/docs/static/img/github-app-new2.png -------------------------------------------------------------------------------- /docs/static/img/github-app-new3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interlink-hq/interLink/dc21f076d4f86e425f45d94e8ed1d6cc654bcd99/docs/static/img/github-app-new3.png -------------------------------------------------------------------------------- /docs/static/img/interlink_logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interlink-hq/interLink/dc21f076d4f86e425f45d94e8ed1d6cc654bcd99/docs/static/img/interlink_logo-dark.png -------------------------------------------------------------------------------- /docs/static/img/interlink_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interlink-hq/interLink/dc21f076d4f86e425f45d94e8ed1d6cc654bcd99/docs/static/img/interlink_logo.png -------------------------------------------------------------------------------- /docs/static/img/logo-helix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interlink-hq/interLink/dc21f076d4f86e425f45d94e8ed1d6cc654bcd99/docs/static/img/logo-helix.png -------------------------------------------------------------------------------- /docs/static/img/logo-izum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interlink-hq/interLink/dc21f076d4f86e425f45d94e8ed1d6cc654bcd99/docs/static/img/logo-izum.png -------------------------------------------------------------------------------- /docs/static/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interlink-hq/interLink/dc21f076d4f86e425f45d94e8ed1d6cc654bcd99/docs/static/img/logo.png -------------------------------------------------------------------------------- /docs/static/img/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/static/img/logo_infn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interlink-hq/interLink/dc21f076d4f86e425f45d94e8ed1d6cc654bcd99/docs/static/img/logo_infn -------------------------------------------------------------------------------- /docs/static/img/logo_infn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interlink-hq/interLink/dc21f076d4f86e425f45d94e8ed1d6cc654bcd99/docs/static/img/logo_infn.jpg -------------------------------------------------------------------------------- /docs/static/img/nunet.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interlink-hq/interLink/dc21f076d4f86e425f45d94e8ed1d6cc654bcd99/docs/static/img/nunet.webp -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This file is not used in compilation. It is here just for a nice editor experience. 3 | "extends": "@docusaurus/tsconfig", 4 | "compilerOptions": { 5 | "baseUrl": "." 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | .venv 2 | -------------------------------------------------------------------------------- /example/test_pod.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: interlink-quickstart 5 | namespace: default 6 | spec: 7 | nodeSelector: 8 | kubernetes.io/hostname: my-civo-node 9 | automountServiceAccountToken: false 10 | containers: 11 | - args: 12 | - "\"600\"" 13 | command: 14 | - sleep 15 | image: "docker://ubuntu" 16 | imagePullPolicy: Always 17 | name: my-container 18 | resources: 19 | limits: 20 | cpu: "1" 21 | memory: 1Gi 22 | requests: 23 | cpu: "1" 24 | memory: 1Gi 25 | tolerations: 26 | - key: virtual-node.interlink/no-schedule 27 | operator: Exists 28 | - effect: NoExecute 29 | key: node.kubernetes.io/not-ready 30 | operator: Exists 31 | tolerationSeconds: 300 32 | - effect: NoExecute 33 | key: node.kubernetes.io/unreachable 34 | operator: Exists 35 | tolerationSeconds: 300 36 | 37 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/interlink-hq/interlink 2 | 3 | go 1.22.5 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/containerd/containerd v1.7.6 9 | github.com/google/uuid v1.6.0 10 | github.com/sirupsen/logrus v1.9.3 11 | github.com/spf13/cobra v1.7.0 12 | github.com/swaggest/openapi-go v0.2.57 13 | github.com/virtual-kubelet/virtual-kubelet v1.11.0 14 | go.opentelemetry.io/otel v1.27.0 15 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 16 | go.opentelemetry.io/otel/sdk v1.27.0 17 | go.opentelemetry.io/otel/trace v1.27.0 18 | golang.org/x/crypto v0.23.0 19 | golang.org/x/oauth2 v0.20.0 20 | google.golang.org/grpc v1.64.0 21 | gopkg.in/yaml.v2 v2.4.0 22 | gopkg.in/yaml.v3 v3.0.1 23 | k8s.io/api v0.29.1 24 | k8s.io/apimachinery v0.29.1 25 | k8s.io/client-go v0.29.1 26 | k8s.io/klog v1.0.0 27 | ) 28 | 29 | require golang.org/x/tools v0.18.0 // indirect 30 | 31 | require ( 32 | github.com/beorn7/perks v1.0.1 // indirect 33 | github.com/blang/semver/v4 v4.0.0 // indirect 34 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 35 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 36 | github.com/davecgh/go-spew v1.1.1 // indirect 37 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect 38 | github.com/go-logr/logr v1.4.1 // indirect 39 | github.com/go-logr/stdr v1.2.2 // indirect 40 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 41 | github.com/go-openapi/jsonreference v0.20.2 // indirect 42 | github.com/go-openapi/swag v0.23.0 // indirect 43 | github.com/gogo/protobuf v1.3.2 // indirect 44 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 45 | github.com/golang/protobuf v1.5.4 // indirect 46 | github.com/google/gnostic-models v0.6.8 // indirect 47 | github.com/google/go-cmp v0.6.0 // indirect 48 | github.com/google/gofuzz v1.2.0 // indirect 49 | github.com/gorilla/mux v1.8.0 // indirect 50 | github.com/gorilla/websocket v1.5.0 // indirect 51 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect 52 | github.com/imdario/mergo v0.3.16 // indirect 53 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 54 | github.com/josharian/intern v1.0.0 // indirect 55 | github.com/json-iterator/go v1.1.12 // indirect 56 | github.com/mailru/easyjson v0.7.7 // indirect 57 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect 58 | github.com/moby/spdystream v0.2.0 // indirect 59 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 60 | github.com/modern-go/reflect2 v1.0.2 // indirect 61 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 62 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect 63 | github.com/pkg/errors v0.9.1 // indirect 64 | github.com/prometheus/client_golang v1.16.0 // indirect 65 | github.com/prometheus/client_model v0.4.0 // indirect 66 | github.com/prometheus/common v0.44.0 // indirect 67 | github.com/prometheus/procfs v0.11.1 // indirect 68 | github.com/spf13/pflag v1.0.5 // indirect 69 | github.com/swaggest/jsonschema-go v0.3.73 // indirect 70 | github.com/swaggest/refl v1.3.1 // indirect 71 | go.opencensus.io v0.24.0 // indirect 72 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 // indirect 73 | go.opentelemetry.io/otel/metric v1.27.0 // indirect 74 | go.opentelemetry.io/proto/otlp v1.3.1 // indirect 75 | golang.org/x/net v0.25.0 // indirect 76 | golang.org/x/sync v0.7.0 // indirect 77 | golang.org/x/sys v0.20.0 // indirect 78 | golang.org/x/term v0.20.0 // indirect 79 | golang.org/x/text v0.15.0 // indirect 80 | golang.org/x/time v0.3.0 // indirect 81 | google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5 // indirect 82 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 // indirect 83 | google.golang.org/protobuf v1.34.1 // indirect 84 | gopkg.in/inf.v0 v0.9.1 // indirect 85 | k8s.io/apiserver v0.29.1 // indirect 86 | k8s.io/component-base v0.29.1 // indirect 87 | k8s.io/klog/v2 v2.110.1 // indirect 88 | k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect 89 | k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect 90 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 91 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect 92 | sigs.k8s.io/yaml v1.3.0 // indirect 93 | ) 94 | -------------------------------------------------------------------------------- /pkg/interlink/api/create.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/containerd/containerd/log" 11 | 12 | types "github.com/interlink-hq/interlink/pkg/interlink" 13 | 14 | "go.opentelemetry.io/otel" 15 | "go.opentelemetry.io/otel/attribute" 16 | trace "go.opentelemetry.io/otel/trace" 17 | ) 18 | 19 | // CreateHandler collects and rearranges all needed ConfigMaps/Secrets/EmptyDirs to ship them to the sidecar, then sends a response to the client 20 | func (h *InterLinkHandler) CreateHandler(w http.ResponseWriter, r *http.Request) { 21 | start := time.Now().UnixMicro() 22 | tracer := otel.Tracer("interlink-API") 23 | _, span := tracer.Start(h.Ctx, "CreateAPI", trace.WithAttributes( 24 | attribute.Int64("start.timestamp", start), 25 | )) 26 | defer span.End() 27 | defer types.SetDurationSpan(start, span) 28 | defer types.SetInfoFromHeaders(span, &r.Header) 29 | 30 | log.G(h.Ctx).Info("InterLink: received Create call") 31 | 32 | var statusCode int 33 | 34 | bodyBytes, err := io.ReadAll(r.Body) 35 | if err != nil { 36 | statusCode = http.StatusInternalServerError 37 | w.WriteHeader(statusCode) 38 | log.G(h.Ctx).Error(err) 39 | return 40 | } 41 | 42 | var req *http.Request // request to forward to sidecar 43 | var pod types.PodCreateRequests // request for interlink 44 | err = json.Unmarshal(bodyBytes, &pod) 45 | if err != nil { 46 | statusCode = http.StatusInternalServerError 47 | log.G(h.Ctx).Error(err) 48 | w.WriteHeader(statusCode) 49 | return 50 | } 51 | 52 | span.SetAttributes( 53 | attribute.String("pod.name", pod.Pod.Name), 54 | attribute.String("pod.namespace", pod.Pod.Namespace), 55 | attribute.String("pod.uid", string(pod.Pod.UID)), 56 | ) 57 | 58 | var retrievedData []types.RetrievedPodData 59 | 60 | data, err := getData(h.Ctx, h.Config, pod, span) 61 | if err != nil { 62 | statusCode = http.StatusInternalServerError 63 | log.G(h.Ctx).Error(err) 64 | w.WriteHeader(statusCode) 65 | return 66 | } 67 | 68 | if log.G(h.Ctx).Logger.IsLevelEnabled(log.DebugLevel) { 69 | // For debugging purpose only. 70 | allContainers := pod.Pod.Spec.InitContainers 71 | allContainers = append(allContainers, pod.Pod.Spec.Containers...) 72 | for _, container := range allContainers { 73 | for _, envVar := range container.Env { 74 | log.G(h.Ctx).Debug("InterLink VK environment variable to pod ", pod.Pod.Name, " container: ", container.Name, " env: ", envVar.Name, " value: ", envVar.Value) 75 | } 76 | } 77 | } 78 | 79 | retrievedData = append(retrievedData, data) 80 | 81 | if retrievedData != nil { 82 | bodyBytes, err = json.Marshal(retrievedData) 83 | if err != nil { 84 | w.WriteHeader(http.StatusInternalServerError) 85 | log.G(h.Ctx).Error(err) 86 | return 87 | } 88 | log.G(h.Ctx).Debug(string(bodyBytes)) 89 | reader := bytes.NewReader(bodyBytes) 90 | 91 | log.G(h.Ctx).Info(req) 92 | req, err = http.NewRequest(http.MethodPost, h.SidecarEndpoint+"/create", reader) 93 | if err != nil { 94 | statusCode = http.StatusInternalServerError 95 | w.WriteHeader(statusCode) 96 | log.G(h.Ctx).Error(err) 97 | return 98 | } 99 | 100 | log.G(h.Ctx).Info("InterLink: forwarding Create call to sidecar") 101 | 102 | sessionContext := GetSessionContext(r) 103 | _, err := ReqWithError(h.Ctx, req, w, start, span, true, false, sessionContext, h.ClientHTTP) 104 | if err != nil { 105 | log.L.Error(err) 106 | return 107 | } 108 | 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /pkg/interlink/api/delete.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/containerd/containerd/log" 11 | v1 "k8s.io/api/core/v1" 12 | 13 | types "github.com/interlink-hq/interlink/pkg/interlink" 14 | 15 | "go.opentelemetry.io/otel" 16 | "go.opentelemetry.io/otel/attribute" 17 | trace "go.opentelemetry.io/otel/trace" 18 | ) 19 | 20 | // DeleteHandler deletes the cached status for the provided Pod and forwards the request to the sidecar 21 | func (h *InterLinkHandler) DeleteHandler(w http.ResponseWriter, r *http.Request) { 22 | start := time.Now().UnixMicro() 23 | tracer := otel.Tracer("interlink-API") 24 | _, span := tracer.Start(h.Ctx, "DeleteAPI", trace.WithAttributes( 25 | attribute.Int64("start.timestamp", start), 26 | )) 27 | defer span.End() 28 | defer types.SetDurationSpan(start, span) 29 | defer types.SetInfoFromHeaders(span, &r.Header) 30 | 31 | log.G(h.Ctx).Info("InterLink: received Delete call") 32 | 33 | bodyBytes, err := io.ReadAll(r.Body) 34 | var statusCode int 35 | 36 | if err != nil { 37 | statusCode = http.StatusInternalServerError 38 | w.WriteHeader(statusCode) 39 | log.G(h.Ctx).Fatal(err) 40 | } 41 | 42 | var req *http.Request 43 | var pod *v1.Pod 44 | reader := bytes.NewReader(bodyBytes) 45 | err = json.Unmarshal(bodyBytes, &pod) 46 | if err != nil { 47 | statusCode = http.StatusInternalServerError 48 | w.WriteHeader(statusCode) 49 | log.G(h.Ctx).Fatal(err) 50 | } 51 | 52 | span.SetAttributes( 53 | attribute.String("pod.name", pod.Name), 54 | attribute.String("pod.namespace", pod.Namespace), 55 | attribute.String("pod.uid", string(pod.UID)), 56 | ) 57 | 58 | deleteCachedStatus(string(pod.UID)) 59 | req, err = http.NewRequest(http.MethodPost, h.SidecarEndpoint+"/delete", reader) 60 | if err != nil { 61 | statusCode = http.StatusInternalServerError 62 | w.WriteHeader(statusCode) 63 | log.G(h.Ctx).Error(err) 64 | return 65 | } 66 | 67 | req.Header.Set("Content-Type", "application/json") 68 | log.G(h.Ctx).Info("InterLink: forwarding Delete call to sidecar") 69 | sessionContext := GetSessionContext(r) 70 | _, err = ReqWithError(h.Ctx, req, w, start, span, true, false, sessionContext, h.ClientHTTP) 71 | if err != nil { 72 | log.L.Error(err) 73 | return 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /pkg/interlink/api/func.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "sync" 7 | "time" 8 | 9 | "github.com/containerd/containerd/log" 10 | "go.opentelemetry.io/otel/attribute" 11 | trace "go.opentelemetry.io/otel/trace" 12 | v1 "k8s.io/api/core/v1" 13 | 14 | types "github.com/interlink-hq/interlink/pkg/interlink" 15 | ) 16 | 17 | type MutexStatuses struct { 18 | mu sync.Mutex 19 | Statuses map[string]types.PodStatus 20 | } 21 | 22 | var PodStatuses MutexStatuses 23 | 24 | // getData retrieves ConfigMaps, Secrets and EmptyDirs from the provided pod by calling the retrieveData function. 25 | // The config is needed by the retrieveData function. 26 | // The function aggregates the return values of retrieveData function in a commonIL.RetrievedPodData variable and returns it, along with the first encountered error. 27 | func getData(ctx context.Context, config types.Config, pod types.PodCreateRequests, span trace.Span) (types.RetrievedPodData, error) { 28 | start := time.Now().UnixMicro() 29 | span.AddEvent("Retrieving data for pod " + pod.Pod.Name) 30 | log.G(ctx).Debug(pod.ConfigMaps) 31 | var retrievedData types.RetrievedPodData 32 | retrievedData.Pod = pod.Pod 33 | 34 | for _, container := range pod.Pod.Spec.InitContainers { 35 | startContainer := time.Now().UnixMicro() 36 | log.G(ctx).Info("- Retrieving Secrets and ConfigMaps for the Docker Sidecar. InitContainer: " + container.Name) 37 | log.G(ctx).Debug(container.VolumeMounts) 38 | data, err := retrieveData(ctx, config, pod, container) 39 | if err != nil { 40 | log.G(ctx).Error(err) 41 | return types.RetrievedPodData{}, err 42 | } 43 | retrievedData.Containers = append(retrievedData.Containers, data) 44 | 45 | durationContainer := time.Now().UnixMicro() - startContainer 46 | span.AddEvent("Init Container "+container.Name, trace.WithAttributes( 47 | attribute.Int64("initcontainer.getdata.duration", durationContainer), 48 | attribute.String("pod.name", pod.Pod.Name))) 49 | } 50 | 51 | for _, container := range pod.Pod.Spec.Containers { 52 | startContainer := time.Now().UnixMicro() 53 | log.G(ctx).Info("- Retrieving Secrets and ConfigMaps for the Docker Sidecar. Container: " + container.Name) 54 | log.G(ctx).Debug(container.VolumeMounts) 55 | data, err := retrieveData(ctx, config, pod, container) 56 | if err != nil { 57 | log.G(ctx).Error(err) 58 | return types.RetrievedPodData{}, err 59 | } 60 | retrievedData.Containers = append(retrievedData.Containers, data) 61 | 62 | durationContainer := time.Now().UnixMicro() - startContainer 63 | span.AddEvent("Container "+container.Name, trace.WithAttributes( 64 | attribute.Int64("container.getdata.duration", durationContainer), 65 | attribute.String("pod.name", pod.Pod.Name))) 66 | } 67 | 68 | duration := time.Now().UnixMicro() - start 69 | span.SetAttributes(attribute.Int64("getdata.duration", duration)) 70 | return retrievedData, nil 71 | } 72 | 73 | // retrieveData retrieves ConfigMaps, Secrets and EmptyDirs. 74 | // The config is needed to specify the EmptyDirs mounting point. 75 | // It returns the retrieved data in a variable of type commonIL.RetrievedContainer and the first encountered error. 76 | func retrieveData(ctx context.Context, _ types.Config, pod types.PodCreateRequests, container v1.Container) (types.RetrievedContainer, error) { 77 | retrievedData := types.RetrievedContainer{} 78 | retrievedData.Name = container.Name 79 | for _, mountVar := range container.VolumeMounts { 80 | log.G(ctx).Debug("-- Retrieving data for mountpoint ", mountVar.Name) 81 | 82 | loopVolumes: 83 | for _, vol := range pod.Pod.Spec.Volumes { 84 | if vol.Name == mountVar.Name { 85 | switch { 86 | case vol.ConfigMap != nil: 87 | log.G(ctx).Info("--- Retrieving ConfigMap ", vol.ConfigMap.Name) 88 | for _, cfgMap := range pod.ConfigMaps { 89 | if cfgMap.Name == vol.ConfigMap.Name { 90 | log.G(ctx).Debug("configMap found! Name: ", cfgMap.Name) 91 | retrievedData.ConfigMaps = append(retrievedData.ConfigMaps, cfgMap) 92 | break loopVolumes 93 | } 94 | } 95 | // This should not happen, error. Building error context. 96 | var configMapsKeys []string 97 | for _, cfgMap := range pod.ConfigMaps { 98 | configMapsKeys = append(configMapsKeys, cfgMap.Name) 99 | } 100 | log.G(ctx).Errorf("could not find in retrievedData the matching object for volume: %s (pod: %s container: %s configMap: %s) retrievedData keys: %s", vol.Name, 101 | pod.Pod.Name, container.Name, vol.ConfigMap.Name, strings.Join(configMapsKeys, ",")) 102 | 103 | case vol.Projected != nil: 104 | log.G(ctx).Info("--- Retrieving ProjectedVolume ", vol.Name) 105 | for _, projectedVolumeMap := range pod.ProjectedVolumeMaps { 106 | log.G(ctx).Debug("Comparing projectedVolumeMap.Name: ", projectedVolumeMap.Name, " with vol.Name: ", vol.Name) 107 | if projectedVolumeMap.Name == vol.Name { 108 | log.G(ctx).Debug("projectedVolumeMap found! Name: ", projectedVolumeMap.Name) 109 | 110 | retrievedData.ProjectedVolumeMaps = append(retrievedData.ProjectedVolumeMaps, projectedVolumeMap) 111 | break loopVolumes 112 | } 113 | } 114 | // This should not happen, error. Building error context. 115 | var projectedVolumeMapsKeys []string 116 | for _, projectedVolumeMap := range pod.ProjectedVolumeMaps { 117 | projectedVolumeMapsKeys = append(projectedVolumeMapsKeys, projectedVolumeMap.Name) 118 | } 119 | log.G(ctx).Errorf("could not find in retrievedData the matching object for volume: %s (pod: %s container: %s projectedVolumeMap) retrievedData keys: %s", 120 | vol.Name, pod.Pod.Name, container.Name, strings.Join(projectedVolumeMapsKeys, ",")) 121 | 122 | case vol.Secret != nil: 123 | log.G(ctx).Info("--- Retrieving Secret ", vol.Secret.SecretName) 124 | for _, secret := range pod.Secrets { 125 | if secret.Name == vol.Secret.SecretName { 126 | log.G(ctx).Debug("secret found! Name: ", secret.Name) 127 | retrievedData.Secrets = append(retrievedData.Secrets, secret) 128 | break loopVolumes 129 | } 130 | } 131 | // This should not happen, error. Building error context. 132 | var secretKeys []string 133 | for _, secret := range pod.Secrets { 134 | secretKeys = append(secretKeys, secret.Name) 135 | } 136 | log.G(ctx).Errorf("could not find in retrievedData the matching object for volume: %s (pod: %s container: %s secret: %s) retrievedData keys: %s", 137 | pod.Pod.Name, container.Name, vol.Name, vol.Secret.SecretName, strings.Join(secretKeys, ",")) 138 | 139 | case vol.EmptyDir != nil: 140 | // Deprecated: EmptyDirs is useless at VK level. It should be moved to plugin level. 141 | // edPath := filepath.Join(config.DataRootFolder, pod.Pod.Namespace+"-"+string(pod.Pod.UID), "emptyDirs", vol.Name) 142 | // retrievedData.EmptyDirs = append(retrievedData.EmptyDirs, edPath) 143 | 144 | default: 145 | log.G(ctx).Warning("ignoring unsupported volume type for ", mountVar.Name) 146 | } 147 | } 148 | } 149 | } 150 | return retrievedData, nil 151 | } 152 | 153 | // deleteCachedStatus locks the map PodStatuses and delete the uid key from that map 154 | func deleteCachedStatus(uid string) { 155 | PodStatuses.mu.Lock() 156 | delete(PodStatuses.Statuses, uid) 157 | PodStatuses.mu.Unlock() 158 | } 159 | 160 | // checkIfCached checks if the uid key is present in the PodStatuses map and returns a bool 161 | func checkIfCached(uid string) bool { 162 | _, ok := PodStatuses.Statuses[uid] 163 | 164 | return ok 165 | } 166 | 167 | // updateStatuses locks and updates the PodStatuses map with the statuses contained in the returnedStatuses slice 168 | func updateStatuses(returnedStatuses []types.PodStatus) { 169 | PodStatuses.mu.Lock() 170 | 171 | for _, new := range returnedStatuses { 172 | // log.G(ctx).Debug(PodStatuses.Statuses, new) 173 | PodStatuses.Statuses[new.PodUID] = new 174 | } 175 | 176 | PodStatuses.mu.Unlock() 177 | } 178 | -------------------------------------------------------------------------------- /pkg/interlink/api/handler.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "strconv" 10 | 11 | "github.com/containerd/containerd/log" 12 | "github.com/google/uuid" 13 | 14 | trace "go.opentelemetry.io/otel/trace" 15 | 16 | "github.com/interlink-hq/interlink/pkg/interlink" 17 | ) 18 | 19 | type InterLinkHandler struct { 20 | Config interlink.Config 21 | Ctx context.Context 22 | SidecarEndpoint string 23 | ClientHTTP *http.Client 24 | // TODO: http client with TLS 25 | } 26 | 27 | func AddSessionContext(req *http.Request, sessionContext string) { 28 | req.Header.Set("InterLink-Http-Session", sessionContext) 29 | } 30 | 31 | func GetSessionContext(r *http.Request) string { 32 | sessionContext := r.Header.Get("InterLink-Http-Session") 33 | if sessionContext == "" { 34 | id := uuid.New() 35 | sessionContext = "Request-" + id.String() 36 | } 37 | return sessionContext 38 | } 39 | 40 | func GetSessionContextMessage(sessionContext string) string { 41 | return "HTTP InterLink session " + sessionContext + ": " 42 | } 43 | 44 | // respondWithReturn: if false, return nil. Useful when body is too big to be contained in one big string. 45 | // sessionNumber: integer number for debugging purpose, generated from InterLink VK, to follow HTTP request from end-to-end. 46 | func ReqWithError( 47 | ctx context.Context, 48 | req *http.Request, 49 | w http.ResponseWriter, 50 | start int64, 51 | span trace.Span, 52 | respondWithValues bool, 53 | respondWithReturn bool, 54 | sessionContext string, 55 | clientHTTP *http.Client, 56 | ) ([]byte, error) { 57 | req.Header.Set("Content-Type", "application/json") 58 | 59 | sessionContextMessage := GetSessionContextMessage(sessionContext) 60 | log.G(ctx).Debug(sessionContextMessage, "doing request: ", fmt.Sprintf("%#v", req)) 61 | 62 | // Add session number for end-to-end from API to InterLink plugin (eg interlink-slurm-plugin) 63 | AddSessionContext(req, sessionContext) 64 | 65 | resp, err := clientHTTP.Do(req) 66 | if err != nil { 67 | statusCode := http.StatusInternalServerError 68 | w.WriteHeader(statusCode) 69 | errWithContext := fmt.Errorf(sessionContextMessage+"error doing DoReq() of ReqWithErrorWithSessionNumber error %w", err) 70 | return nil, errWithContext 71 | } 72 | defer resp.Body.Close() 73 | 74 | w.WriteHeader(resp.StatusCode) 75 | // Flush headers ASAP so that the client is not blocked in request. 76 | if f, ok := w.(http.Flusher); ok { 77 | log.G(ctx).Debug(sessionContextMessage, "Flushing client...") 78 | f.Flush() 79 | } else { 80 | log.G(ctx).Error(sessionContextMessage, "could not flush because server does not support Flusher.") 81 | } 82 | 83 | if resp.StatusCode != http.StatusOK { 84 | log.G(ctx).Error(sessionContextMessage, "HTTP request in error.") 85 | statusCode := http.StatusInternalServerError 86 | w.WriteHeader(statusCode) 87 | ret, err := io.ReadAll(resp.Body) 88 | if err != nil { 89 | return nil, fmt.Errorf(sessionContextMessage+"HTTP request in error and could not read body response error: %w", err) 90 | } 91 | errHTTP := fmt.Errorf(sessionContextMessage+"call exit status: %d. Body: %s", statusCode, ret) 92 | log.G(ctx).Error(errHTTP) 93 | _, err = w.Write([]byte(errHTTP.Error())) 94 | if err != nil { 95 | return nil, fmt.Errorf(sessionContextMessage+"HTTP request in error and could not write all body response to InterLink Node error: %w", err) 96 | } 97 | return nil, errHTTP 98 | } 99 | 100 | interlink.SetDurationSpan(start, span, interlink.WithHTTPReturnCode(resp.StatusCode)) 101 | 102 | if respondWithReturn { 103 | 104 | log.G(ctx).Debug(sessionContextMessage, "reading all body once for all") 105 | returnValue, err := io.ReadAll(resp.Body) 106 | if err != nil { 107 | w.WriteHeader(http.StatusInternalServerError) 108 | return nil, fmt.Errorf(sessionContextMessage+"error doing ReadAll() of ReqWithErrorComplex see error %w", err) 109 | } 110 | 111 | if respondWithValues { 112 | _, err = w.Write(returnValue) 113 | if err != nil { 114 | w.WriteHeader(http.StatusInternalServerError) 115 | return nil, fmt.Errorf(sessionContextMessage+"error doing Write() of ReqWithErrorComplex see error %w", err) 116 | } 117 | } 118 | 119 | return returnValue, nil 120 | } 121 | 122 | // Case no return needed. 123 | 124 | if respondWithValues { 125 | // Because no return needed, we can write continuously instead of writing one big block of data. 126 | // Useful to get following logs. 127 | log.G(ctx).Debug(sessionContextMessage, "in respondWithValues loop, reading body continuously until EOF") 128 | 129 | // In this case, we return continuously the values in the w, instead of reading it all. This allows for logs to be followed. 130 | bodyReader := bufio.NewReader(resp.Body) 131 | 132 | // 4096 is bufio.NewReader default buffer size. 133 | bufferBytes := make([]byte, 4096) 134 | 135 | // Looping until we get EOF from sidecar. 136 | for { 137 | log.G(ctx).Debug(sessionContextMessage, "trying to read some bytes from InterLink sidecar "+req.RequestURI) 138 | n, err := bodyReader.Read(bufferBytes) 139 | if err != nil { 140 | if err == io.EOF { 141 | log.G(ctx).Debug(sessionContextMessage, "received EOF and read number of bytes: "+strconv.Itoa(n)) 142 | 143 | // EOF but we still have something to read! 144 | if n != 0 { 145 | _, err = w.Write(bufferBytes[:n]) 146 | if err != nil { 147 | w.WriteHeader(http.StatusInternalServerError) 148 | return nil, fmt.Errorf(sessionContextMessage+"could not write during ReqWithError() error: %w", err) 149 | } 150 | } 151 | return nil, nil 152 | } 153 | // Error during read. 154 | w.WriteHeader(http.StatusInternalServerError) 155 | return nil, fmt.Errorf(sessionContextMessage+"could not read HTTP body: see error %w", err) 156 | } 157 | log.G(ctx).Debug(sessionContextMessage, "received some bytes from InterLink sidecar") 158 | _, err = w.Write(bufferBytes[:n]) 159 | if err != nil { 160 | w.WriteHeader(http.StatusInternalServerError) 161 | return nil, fmt.Errorf(sessionContextMessage+"could not write during ReqWithError() error: %w", err) 162 | } 163 | 164 | // Flush otherwise it will take time to appear in kubectl logs. 165 | if f, ok := w.(http.Flusher); ok { 166 | log.G(ctx).Debug(sessionContextMessage, "Wrote some logs, now flushing...") 167 | f.Flush() 168 | } else { 169 | log.G(ctx).Error(sessionContextMessage, "could not flush because server does not support Flusher.") 170 | } 171 | } 172 | } 173 | 174 | // Case no respondWithValue no respondWithReturn , it means we are doing a request and not using response. 175 | return nil, nil 176 | } 177 | -------------------------------------------------------------------------------- /pkg/interlink/api/logs.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "io" 8 | "net/http" 9 | "time" 10 | 11 | "github.com/containerd/containerd/log" 12 | 13 | types "github.com/interlink-hq/interlink/pkg/interlink" 14 | 15 | "go.opentelemetry.io/otel" 16 | "go.opentelemetry.io/otel/attribute" 17 | trace "go.opentelemetry.io/otel/trace" 18 | ) 19 | 20 | func (h *InterLinkHandler) GetLogsHandler(w http.ResponseWriter, r *http.Request) { 21 | start := time.Now().UnixMicro() 22 | tracer := otel.Tracer("interlink-API") 23 | _, span := tracer.Start(h.Ctx, "GetLogsAPI", trace.WithAttributes( 24 | attribute.Int64("start.timestamp", start), 25 | )) 26 | defer span.End() 27 | defer types.SetDurationSpan(start, span) 28 | defer types.SetInfoFromHeaders(span, &r.Header) 29 | 30 | sessionContext := GetSessionContext(r) 31 | sessionContextMessage := GetSessionContextMessage(sessionContext) 32 | 33 | var statusCode int 34 | log.G(h.Ctx).Info(sessionContextMessage, "InterLink: received GetLogs call") 35 | bodyBytes, err := io.ReadAll(r.Body) 36 | if err != nil { 37 | log.G(h.Ctx).Fatal(sessionContextMessage, err) 38 | } 39 | 40 | log.G(h.Ctx).Info(sessionContextMessage, "InterLink: unmarshal GetLogs request") 41 | var req2 types.LogStruct // incoming request. To be used in interlink API. req is directly forwarded to sidecar 42 | err = json.Unmarshal(bodyBytes, &req2) 43 | if err != nil { 44 | statusCode = http.StatusInternalServerError 45 | w.WriteHeader(statusCode) 46 | log.G(h.Ctx).Error(sessionContextMessage, err) 47 | return 48 | } 49 | 50 | span.SetAttributes( 51 | attribute.String("pod.name", req2.PodName), 52 | attribute.String("pod.namespace", req2.Namespace), 53 | attribute.Int("opts.limitbytes", req2.Opts.LimitBytes), 54 | attribute.Int("opts.since", req2.Opts.SinceSeconds), 55 | attribute.Int64("opts.sincetime", req2.Opts.SinceTime.UnixMicro()), 56 | attribute.Int("opts.tail", req2.Opts.Tail), 57 | attribute.Bool("opts.follow", req2.Opts.Follow), 58 | attribute.Bool("opts.previous", req2.Opts.Previous), 59 | attribute.Bool("opts.timestamps", req2.Opts.Timestamps), 60 | ) 61 | 62 | log.G(h.Ctx).Info(sessionContextMessage, "InterLink: new GetLogs podUID: now ", req2.PodUID) 63 | if (req2.Opts.Tail != 0 && req2.Opts.LimitBytes != 0) || (req2.Opts.SinceSeconds != 0 && !req2.Opts.SinceTime.IsZero()) { 64 | statusCode = http.StatusInternalServerError 65 | w.WriteHeader(statusCode) 66 | 67 | if req2.Opts.Tail != 0 && req2.Opts.LimitBytes != 0 { 68 | _, err = w.Write([]byte("Both Tail and LimitBytes set. Set only one of them")) 69 | if err != nil { 70 | log.G(h.Ctx).Error(errors.New(sessionContextMessage + "Failed to write to http buffer")) 71 | } 72 | return 73 | } 74 | 75 | _, err = w.Write([]byte("Both SinceSeconds and SinceTime set. Set only one of them")) 76 | if err != nil { 77 | log.G(h.Ctx).Error(errors.New(sessionContextMessage + "Failed to write to http buffer")) 78 | } 79 | 80 | } 81 | 82 | log.G(h.Ctx).Info(sessionContextMessage, "InterLink: marshal GetLogs request ") 83 | 84 | bodyBytes, err = json.Marshal(req2) 85 | if err != nil { 86 | statusCode = http.StatusInternalServerError 87 | w.WriteHeader(statusCode) 88 | log.G(h.Ctx).Error(err) 89 | return 90 | } 91 | reader := bytes.NewReader(bodyBytes) 92 | log.G(h.Ctx).Info("Sending log request to: ", h.SidecarEndpoint) 93 | req, err := http.NewRequest(http.MethodGet, h.SidecarEndpoint+"/getLogs", reader) 94 | if err != nil { 95 | log.G(h.Ctx).Fatal(err) 96 | } 97 | 98 | req.Header.Set("Content-Type", "application/json") 99 | 100 | // logTransport := http.DefaultTransport.(*http.Transport).Clone() 101 | // // logTransport.DisableKeepAlives = true 102 | // // logTransport.MaxIdleConnsPerHost = -1 103 | // var logHTTPClient = &http.Client{Transport: logTransport} 104 | 105 | log.G(h.Ctx).Info(sessionContextMessage, "InterLink: forwarding GetLogs call to sidecar") 106 | _, err = ReqWithError(h.Ctx, req, w, start, span, true, false, sessionContext, h.ClientHTTP) 107 | if err != nil { 108 | log.L.Error(sessionContextMessage, err) 109 | return 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /pkg/interlink/api/ping.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "net/http" 8 | "strconv" 9 | "time" 10 | 11 | "github.com/containerd/containerd/log" 12 | v1 "k8s.io/api/core/v1" 13 | 14 | types "github.com/interlink-hq/interlink/pkg/interlink" 15 | 16 | "go.opentelemetry.io/otel" 17 | "go.opentelemetry.io/otel/attribute" 18 | trace "go.opentelemetry.io/otel/trace" 19 | ) 20 | 21 | // Ping is just a very basic Ping function 22 | func (h *InterLinkHandler) Ping(w http.ResponseWriter, r *http.Request) { 23 | start := time.Now().UnixMicro() 24 | tracer := otel.Tracer("interlink-API") 25 | _, span := tracer.Start(h.Ctx, "PingAPI", trace.WithAttributes( 26 | attribute.Int64("start.timestamp", start), 27 | )) 28 | defer span.End() 29 | defer types.SetDurationSpan(start, span) 30 | defer types.SetInfoFromHeaders(span, &r.Header) 31 | 32 | log.G(h.Ctx).Info("InterLink: received Ping call") 33 | 34 | podsToBeChecked := []*v1.Pod{} 35 | bodyBytes, err := json.Marshal(podsToBeChecked) 36 | if err != nil { 37 | log.G(h.Ctx).Error(err) 38 | } 39 | 40 | reader := bytes.NewReader(bodyBytes) 41 | req, err := http.NewRequest(http.MethodGet, h.SidecarEndpoint+"/status", reader) 42 | if err != nil { 43 | log.G(h.Ctx).Error(err) 44 | } 45 | 46 | log.G(h.Ctx).Info("InterLink: forwarding GetStatus call to sidecar") 47 | req.Header.Set("Content-Type", "application/json") 48 | log.G(h.Ctx).Debug(req) 49 | 50 | // ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 51 | // defer cancel() 52 | // respPlugin, err := http.DefaultClient.Do(req) 53 | // respPlugin, err := DoReq(req.WithContext(ctx)) 54 | sessionContext := GetSessionContext(req) 55 | _, err = ReqWithError(h.Ctx, req, w, start, span, true, false, sessionContext, h.ClientHTTP) 56 | if err != nil { 57 | log.G(h.Ctx).Error(err) 58 | w.WriteHeader(http.StatusServiceUnavailable) 59 | _, err = w.Write([]byte(strconv.Itoa(http.StatusServiceUnavailable))) 60 | if err != nil { 61 | log.G(h.Ctx).Error(errors.New("failed to write to http buffer")) 62 | } 63 | return 64 | } 65 | // defer respPlugin.Body.Close() 66 | // 67 | // if respPlugin.StatusCode != http.StatusOK { 68 | // log.G(h.Ctx).Error("error pinging plugin") 69 | // w.WriteHeader(respPlugin.StatusCode) 70 | // _, err = w.Write([]byte(strconv.Itoa(http.StatusServiceUnavailable))) 71 | // if err != nil { 72 | // log.G(h.Ctx).Error(errors.New("Failed to write to http buffer")) 73 | // } 74 | // 75 | // return 76 | // } 77 | // 78 | // types.SetDurationSpan(start, span, types.WithHTTPReturnCode(respPlugin.StatusCode)) 79 | // 80 | // w.WriteHeader(http.StatusOK) 81 | // _, err = w.Write([]byte("0")) 82 | // if err != nil { 83 | // log.G(h.Ctx).Error(errors.New("Failed to write to http buffer")) 84 | // } 85 | } 86 | -------------------------------------------------------------------------------- /pkg/interlink/api/status.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "time" 11 | 12 | "github.com/containerd/containerd/log" 13 | v1 "k8s.io/api/core/v1" 14 | 15 | types "github.com/interlink-hq/interlink/pkg/interlink" 16 | 17 | "go.opentelemetry.io/otel" 18 | "go.opentelemetry.io/otel/attribute" 19 | trace "go.opentelemetry.io/otel/trace" 20 | ) 21 | 22 | func (h *InterLinkHandler) StatusHandler(w http.ResponseWriter, r *http.Request) { 23 | start := time.Now().UnixMicro() 24 | tracer := otel.Tracer("interlink-API") 25 | _, span := tracer.Start(h.Ctx, "StatusAPI", trace.WithAttributes( 26 | attribute.Int64("start.timestamp", start), 27 | )) 28 | defer span.End() 29 | defer types.SetDurationSpan(start, span) 30 | defer types.SetInfoFromHeaders(span, &r.Header) 31 | statusCode := http.StatusOK 32 | var pods []*v1.Pod 33 | log.G(h.Ctx).Info("InterLink: received GetStatus call") 34 | 35 | bodyBytes, err := io.ReadAll(r.Body) 36 | if err != nil { 37 | log.G(h.Ctx).Fatal(err) 38 | } 39 | 40 | err = json.Unmarshal(bodyBytes, &pods) 41 | if err != nil { 42 | errWithContext := fmt.Errorf("error doing fisrt Unmarshal() in StatusHandler() error detail: %s error: %w", fmt.Sprintf("%#v", err), err) 43 | log.G(h.Ctx).Error(errWithContext) 44 | } 45 | 46 | span.SetAttributes( 47 | attribute.Int("pods.count", len(pods)), 48 | ) 49 | 50 | var podsToBeChecked []*v1.Pod 51 | var returnedStatuses []types.PodStatus // returned from the query to the sidecar 52 | var returnPods []types.PodStatus // returned to the vk 53 | 54 | PodStatuses.mu.Lock() 55 | for _, pod := range pods { 56 | cached := checkIfCached(string(pod.UID)) 57 | if pod.Status.Phase == v1.PodRunning || pod.Status.Phase == v1.PodPending || !cached { 58 | podsToBeChecked = append(podsToBeChecked, pod) 59 | } 60 | span.AddEvent("Pod "+pod.Name+" is cached", trace.WithAttributes( 61 | attribute.String("pod.name", pod.Name), 62 | attribute.String("pod.namespace", pod.Namespace), 63 | attribute.String("pod.uid", string(pod.UID)), 64 | attribute.String("pod.phase", string(pod.Status.Phase)), 65 | )) 66 | } 67 | PodStatuses.mu.Unlock() 68 | 69 | if len(podsToBeChecked) > 0 { 70 | 71 | bodyBytes, err = json.Marshal(podsToBeChecked) 72 | if err != nil { 73 | log.G(h.Ctx).Fatal(err) 74 | } 75 | 76 | reader := bytes.NewReader(bodyBytes) 77 | req, err := http.NewRequest(http.MethodGet, h.SidecarEndpoint+"/status", reader) 78 | if err != nil { 79 | log.G(h.Ctx).Fatal(err) 80 | } 81 | 82 | log.G(h.Ctx).Info("InterLink: forwarding GetStatus call to sidecar") 83 | req.Header.Set("Content-Type", "application/json") 84 | 85 | sessionContext := GetSessionContext(r) 86 | bodyBytes, err = ReqWithError(h.Ctx, req, w, start, span, false, true, sessionContext, h.ClientHTTP) 87 | if err != nil { 88 | log.L.Error(err) 89 | return 90 | } 91 | 92 | err = json.Unmarshal(bodyBytes, &returnedStatuses) 93 | if err != nil { 94 | statusCode = http.StatusInternalServerError 95 | w.WriteHeader(statusCode) 96 | errWithContext := fmt.Errorf("error doing Unmarshal() in StatusHandler() of req %s error detail: %s error: %w", fmt.Sprintf("%#v", req), fmt.Sprintf("%#v", err), err) 97 | log.G(h.Ctx).Error(errWithContext) 98 | return 99 | } 100 | 101 | updateStatuses(returnedStatuses) 102 | types.SetDurationSpan(start, span, types.WithHTTPReturnCode(statusCode)) 103 | 104 | } 105 | 106 | if len(pods) > 0 { 107 | for _, pod := range pods { 108 | PodStatuses.mu.Lock() 109 | for _, cached := range PodStatuses.Statuses { 110 | if cached.PodUID == string(pod.UID) { 111 | returnPods = append(returnPods, cached) 112 | break 113 | } 114 | } 115 | PodStatuses.mu.Unlock() 116 | } 117 | } else { 118 | for _, pod := range PodStatuses.Statuses { 119 | returnPods = append(returnPods, pod) 120 | } 121 | } 122 | 123 | returnValue, err := json.Marshal(returnPods) 124 | if err != nil { 125 | statusCode = http.StatusInternalServerError 126 | w.WriteHeader(statusCode) 127 | log.G(h.Ctx).Error(err) 128 | return 129 | } 130 | 131 | w.WriteHeader(statusCode) 132 | _, err = w.Write(returnValue) 133 | if err != nil { 134 | log.G(h.Ctx).Error(errors.New("failed to write to http buffer")) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /pkg/interlink/api/update.go: -------------------------------------------------------------------------------- 1 | package api 2 | -------------------------------------------------------------------------------- /pkg/interlink/api/updateCache.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "net/http" 7 | 8 | "github.com/containerd/containerd/log" 9 | ) 10 | 11 | // UpdateCacheHandler is responsible for deleting not-available-anymore Pods on the Virtual Kubelet from the InterLink caching structure 12 | func (h *InterLinkHandler) UpdateCacheHandler(w http.ResponseWriter, r *http.Request) { 13 | log.G(h.Ctx).Info("InterLink: received UpdateCache call") 14 | 15 | bodyBytes, err := io.ReadAll(r.Body) 16 | statusCode := http.StatusOK 17 | if err != nil { 18 | statusCode = http.StatusInternalServerError 19 | log.G(h.Ctx).Fatal(err) 20 | } 21 | 22 | deleteCachedStatus(string(bodyBytes)) 23 | 24 | w.WriteHeader(statusCode) 25 | _, err = w.Write([]byte("Updated cache")) 26 | if err != nil { 27 | log.G(h.Ctx).Error(errors.New("failed to write to http buffer")) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /pkg/interlink/config.go: -------------------------------------------------------------------------------- 1 | package interlink 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "flag" 8 | "fmt" 9 | "os" 10 | "time" 11 | 12 | "github.com/containerd/containerd/log" 13 | "github.com/google/uuid" 14 | "go.opentelemetry.io/otel" 15 | "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" 16 | "go.opentelemetry.io/otel/propagation" 17 | "go.opentelemetry.io/otel/sdk/resource" 18 | sdktrace "go.opentelemetry.io/otel/sdk/trace" 19 | semconv "go.opentelemetry.io/otel/semconv/v1.21.0" 20 | "google.golang.org/grpc" 21 | "google.golang.org/grpc/connectivity" 22 | "google.golang.org/grpc/credentials" 23 | "google.golang.org/grpc/credentials/insecure" 24 | "gopkg.in/yaml.v2" 25 | ) 26 | 27 | // Config holds the whole configuration 28 | type Config struct { 29 | InterlinkAddress string `yaml:"InterlinkAddress"` 30 | Interlinkport string `yaml:"InterlinkPort"` 31 | Sidecarurl string `yaml:"SidecarURL"` 32 | Sidecarport string `yaml:"SidecarPort"` 33 | VerboseLogging bool `yaml:"VerboseLogging"` 34 | ErrorsOnlyLogging bool `yaml:"ErrorsOnlyLogging"` 35 | DataRootFolder string `yaml:"DataRootFolder"` 36 | } 37 | 38 | func SetupTelemetry(ctx context.Context, serviceName string) (*sdktrace.TracerProvider, error) { 39 | log.G(ctx).Info("Tracing is enabled, setting up the TracerProvider") 40 | 41 | // Get the TELEMETRY_UNIQUE_ID from the environment, if it is not set, use the hostname 42 | uniqueID := os.Getenv("TELEMETRY_UNIQUE_ID") 43 | if uniqueID == "" { 44 | log.G(ctx).Info("No TELEMETRY_UNIQUE_ID set, generating a new one") 45 | newUUID := uuid.New() 46 | uniqueID = newUUID.String() 47 | log.G(ctx).Info("Generated unique ID: ", uniqueID, " use "+serviceName+"-"+uniqueID+" as service name from Grafana") 48 | } 49 | 50 | fullServiceName := serviceName + uniqueID 51 | 52 | res, err := resource.New(ctx, 53 | resource.WithAttributes( 54 | // the service name used to display traces in backends 55 | semconv.ServiceName(fullServiceName), 56 | ), 57 | ) 58 | if err != nil { 59 | return nil, fmt.Errorf("failed to create resource: %w", err) 60 | } 61 | 62 | ctx, cancel := context.WithTimeout(ctx, time.Second) 63 | defer cancel() 64 | 65 | otlpEndpoint := os.Getenv("TELEMETRY_ENDPOINT") 66 | 67 | if otlpEndpoint == "" { 68 | otlpEndpoint = "localhost:4317" 69 | } 70 | 71 | log.G(ctx).Info("TELEMETRY_ENDPOINT: ", otlpEndpoint) 72 | 73 | caCrtFilePath := os.Getenv("TELEMETRY_CA_CRT_FILEPATH") 74 | 75 | conn := &grpc.ClientConn{} 76 | if caCrtFilePath != "" { 77 | 78 | // if the CA certificate is provided, set up mutual TLS 79 | 80 | log.G(ctx).Info("CA certificate provided, setting up mutual TLS") 81 | 82 | caCert, err := os.ReadFile(caCrtFilePath) 83 | if err != nil { 84 | return nil, fmt.Errorf("failed to load CA certificate: %w", err) 85 | } 86 | 87 | clientKeyFilePath := os.Getenv("TELEMETRY_CLIENT_KEY_FILEPATH") 88 | if clientKeyFilePath == "" { 89 | return nil, fmt.Errorf("client key file path not provided. Since a CA certificate is provided, a client key is required for mutual TLS") 90 | } 91 | 92 | clientCrtFilePath := os.Getenv("TELEMETRY_CLIENT_CRT_FILEPATH") 93 | if clientCrtFilePath == "" { 94 | return nil, fmt.Errorf("client certificate file path not provided. Since a CA certificate is provided, a client certificate is required for mutual TLS") 95 | } 96 | 97 | insecureSkipVerify := false 98 | if os.Getenv("TELEMETRY_INSECURE_SKIP_VERIFY") == "true" { 99 | insecureSkipVerify = true 100 | } 101 | 102 | certPool := x509.NewCertPool() 103 | if !certPool.AppendCertsFromPEM(caCert) { 104 | return nil, fmt.Errorf("failed to append CA certificate") 105 | } 106 | 107 | cert, err := tls.LoadX509KeyPair(clientCrtFilePath, clientKeyFilePath) 108 | if err != nil { 109 | return nil, fmt.Errorf("failed to load client certificate: %w", err) 110 | } 111 | 112 | tlsConfig := &tls.Config{ 113 | Certificates: []tls.Certificate{cert}, 114 | RootCAs: certPool, 115 | MinVersion: tls.VersionTLS12, 116 | InsecureSkipVerify: insecureSkipVerify, // #nosec 117 | } 118 | 119 | creds := credentials.NewTLS(tlsConfig) 120 | conn, err = grpc.NewClient(otlpEndpoint, grpc.WithTransportCredentials(creds)) 121 | if err != nil { 122 | return nil, fmt.Errorf("failed to create gRPC client: %w", err) 123 | } 124 | } else { 125 | conn, err = grpc.NewClient(otlpEndpoint, grpc.WithTransportCredentials(insecure.NewCredentials())) 126 | } 127 | 128 | conn.WaitForStateChange(ctx, connectivity.Ready) 129 | 130 | if err != nil { 131 | return nil, fmt.Errorf("failed to create gRPC connection to collector: %w", err) 132 | } 133 | 134 | // Set up a trace exporter 135 | traceExporter, err := otlptracegrpc.New(ctx, otlptracegrpc.WithGRPCConn(conn)) 136 | if err != nil { 137 | return nil, fmt.Errorf("failed to create trace exporter: %w", err) 138 | } 139 | 140 | // Register the trace exporter with a TracerProvider, using a batch 141 | // span processor to aggregate spans before export. 142 | bsp := sdktrace.NewBatchSpanProcessor(traceExporter) 143 | tracerProvider := sdktrace.NewTracerProvider( 144 | sdktrace.WithSampler(sdktrace.AlwaysSample()), 145 | sdktrace.WithResource(res), 146 | sdktrace.WithSpanProcessor(bsp), 147 | ) 148 | otel.SetTracerProvider(tracerProvider) 149 | 150 | // set global propagator to tracecontext (the default is no-op). 151 | otel.SetTextMapPropagator(propagation.TraceContext{}) 152 | 153 | return tracerProvider, nil 154 | } 155 | 156 | func InitTracer(ctx context.Context, serviceName string) (func(context.Context) error, error) { 157 | // Get the TELEMETRY_UNIQUE_ID from the environment, if it is not set, use the hostname 158 | tracerProvider, err := SetupTelemetry(ctx, serviceName) 159 | if err != nil { 160 | return nil, err 161 | } 162 | 163 | return tracerProvider.Shutdown, nil 164 | } 165 | 166 | // NewInterLinkConfig returns a variable of type InterLinkConfig, used in many other functions and the first encountered error. 167 | func NewInterLinkConfig() (Config, error) { 168 | var path string 169 | verbose := flag.Bool("verbose", false, "Enable or disable Debug level logging") 170 | errorsOnly := flag.Bool("errorsonly", false, "Prints only errors if enabled") 171 | InterLinkConfigPath := flag.String("interlinkconfigpath", "", "Path to InterLink config") 172 | flag.Parse() 173 | 174 | interLinkNewConfig := Config{} 175 | 176 | if *verbose { 177 | interLinkNewConfig.VerboseLogging = true 178 | interLinkNewConfig.ErrorsOnlyLogging = false 179 | } else if *errorsOnly { 180 | interLinkNewConfig.VerboseLogging = false 181 | interLinkNewConfig.ErrorsOnlyLogging = true 182 | } 183 | 184 | if *InterLinkConfigPath != "" { 185 | path = *InterLinkConfigPath 186 | } else { 187 | if os.Getenv("INTERLINKCONFIGPATH") != "" { 188 | path = os.Getenv("INTERLINKCONFIGPATH") 189 | } else { 190 | path = "/etc/interlink/InterLinkConfig.yaml" 191 | } 192 | } 193 | 194 | if _, err := os.Stat(path); err != nil { 195 | log.G(context.Background()).Error("File " + path + " doesn't exist. You can set a custom path by exporting INTERLINKCONFIGPATH. Exiting...") 196 | return Config{}, err 197 | } 198 | 199 | log.G(context.Background()).Info("Loading InterLink config from " + path) 200 | yfile, err := os.ReadFile(path) 201 | if err != nil { 202 | log.G(context.Background()).Error("Error opening config file, exiting...") 203 | return Config{}, err 204 | } 205 | 206 | err = yaml.Unmarshal(yfile, &interLinkNewConfig) 207 | if err != nil { 208 | return Config{}, err 209 | } 210 | 211 | if os.Getenv("INTERLINKURL") != "" { 212 | interLinkNewConfig.InterlinkAddress = os.Getenv("INTERLINKURL") 213 | } 214 | 215 | if os.Getenv("SIDECARURL") != "" { 216 | interLinkNewConfig.Sidecarurl = os.Getenv("SIDECARURL") 217 | } 218 | 219 | if os.Getenv("INTERLINKPORT") != "" { 220 | interLinkNewConfig.Interlinkport = os.Getenv("INTERLINKPORT") 221 | } 222 | 223 | if os.Getenv("SIDECARPORT") != "" { 224 | interLinkNewConfig.Sidecarport = os.Getenv("SIDECARPORT") 225 | } 226 | 227 | return interLinkNewConfig, nil 228 | } 229 | -------------------------------------------------------------------------------- /pkg/interlink/spans.go: -------------------------------------------------------------------------------- 1 | package interlink 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "go.opentelemetry.io/otel/attribute" 8 | trace "go.opentelemetry.io/otel/trace" 9 | ) 10 | 11 | func WithHTTPReturnCode(code int) SpanOption { 12 | return func(cfg *SpanConfig) { 13 | cfg.HTTPReturnCode = code 14 | cfg.SetHTTPCode = true 15 | } 16 | } 17 | 18 | func SetDurationSpan(startTime int64, span trace.Span, opts ...SpanOption) { 19 | endTime := time.Now().UnixMicro() 20 | config := &SpanConfig{} 21 | 22 | for _, opt := range opts { 23 | opt(config) 24 | } 25 | 26 | duration := endTime - startTime 27 | span.SetAttributes(attribute.Int64("end.timestamp", endTime), 28 | attribute.Int64("duration", duration)) 29 | 30 | if config.SetHTTPCode { 31 | span.SetAttributes(attribute.Int("exit.code", config.HTTPReturnCode)) 32 | } 33 | } 34 | 35 | func SetInfoFromHeaders(span trace.Span, h *http.Header) { 36 | var xForwardedEmail, xForwardedUser string 37 | if xForwardedEmail = h.Get("X-Forwarded-Email"); xForwardedEmail == "" { 38 | xForwardedEmail = "unknown" 39 | } 40 | if xForwardedUser = h.Get("X-Forwarded-User"); xForwardedUser == "" { 41 | xForwardedUser = "unknown" 42 | } 43 | span.SetAttributes( 44 | attribute.String("X-Forwarded-Email", xForwardedEmail), 45 | attribute.String("X-Forwarded-User", xForwardedUser), 46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /pkg/interlink/types.go: -------------------------------------------------------------------------------- 1 | package interlink 2 | 3 | import ( 4 | "time" 5 | 6 | v1 "k8s.io/api/core/v1" 7 | ) 8 | 9 | // PodCreateRequests is a struct holding data for a create request. Retrieved ConfigMaps and Secrets are held along the Pod description itself. 10 | type PodCreateRequests struct { 11 | Pod v1.Pod `json:"pod"` 12 | ConfigMaps []v1.ConfigMap `json:"configmaps"` 13 | Secrets []v1.Secret `json:"secrets"` 14 | // The projected volumes are those created by ServiceAccounts (in K8S >= 1.24). They are automatically added in the pod from kubelet code. 15 | // Here the configmap will hold the files name (as key) and content (as value). 16 | ProjectedVolumeMaps []v1.ConfigMap `json:"projectedvolumesmaps"` 17 | } 18 | 19 | // PodStatus is a simplified v1.Pod struct, holding only necessary variables to uniquely identify a job/service in the sidecar. It is used to request 20 | type PodStatus struct { 21 | PodName string `json:"name"` 22 | PodUID string `json:"UID"` 23 | PodNamespace string `json:"namespace"` 24 | JobID string `json:"JID"` 25 | Containers []v1.ContainerStatus `json:"containers"` 26 | InitContainers []v1.ContainerStatus `json:"initContainers"` 27 | } 28 | 29 | // CreateStruct is the response to be received from interLink whenever asked to create a pod. It will allow for mapping remote ID with the pod UUID 30 | type CreateStruct struct { 31 | PodUID string `json:"PodUID"` 32 | PodJID string `json:"PodJID"` 33 | } 34 | 35 | // RetrievedContainer is used in InterLink to rearrange data structure in a suitable way for the sidecar 36 | type RetrievedContainer struct { 37 | Name string `json:"name"` 38 | ConfigMaps []v1.ConfigMap `json:"configMaps"` 39 | ProjectedVolumeMaps []v1.ConfigMap `json:"projectedvolumemaps"` 40 | Secrets []v1.Secret `json:"secrets"` 41 | // Deprecated: EmptyDirs should be built on plugin side. 42 | // Currently, it holds the DATA_ROOT_DIR/emptydirs/volumeName, but this should be a plugin choice instead, 43 | // like it currently is for ConfigMaps, ProjectedVolumeMaps, Secrets. 44 | EmptyDirs []string `json:"emptyDirs"` 45 | } 46 | 47 | // RetrievedPoData is used in InterLink to rearrange data structure in a suitable way for the sidecar 48 | type RetrievedPodData struct { 49 | Pod v1.Pod `json:"pod"` 50 | Containers []RetrievedContainer `json:"container"` 51 | } 52 | 53 | // ContainerLogOpts is a struct in which it is possible to specify options to retrieve logs from the sidecar 54 | type ContainerLogOpts struct { 55 | Tail int `json:"Tail"` 56 | LimitBytes int `json:"Bytes"` 57 | Timestamps bool `json:"Timestamps"` 58 | Follow bool `json:"Follow"` 59 | Previous bool `json:"Previous"` 60 | SinceSeconds int `json:"SinceSeconds"` 61 | SinceTime time.Time `json:"SinceTime"` 62 | } 63 | 64 | // LogStruct is needed to identify the job/container running on the sidecar to retrieve the logs from. Using ContainerLogOpts struct allows to specify more options on how to collect logs 65 | type LogStruct struct { 66 | Namespace string `json:"Namespace"` 67 | PodUID string `json:"PodUID"` 68 | PodName string `json:"PodName"` 69 | ContainerName string `json:"ContainerName"` 70 | Opts ContainerLogOpts `json:"Opts"` 71 | } 72 | 73 | type SpanConfig struct { 74 | HTTPReturnCode int 75 | SetHTTPCode bool 76 | } 77 | 78 | type SpanOption func(*SpanConfig) 79 | -------------------------------------------------------------------------------- /pkg/virtualkubelet/cert-retriever.go: -------------------------------------------------------------------------------- 1 | package virtualkubelet 2 | 3 | import ( 4 | "crypto/ed25519" 5 | cryptorand "crypto/rand" 6 | "crypto/tls" 7 | "crypto/x509" 8 | "crypto/x509/pkix" 9 | "encoding/pem" 10 | "fmt" 11 | "math/big" 12 | "math/rand" 13 | "net" 14 | "time" 15 | 16 | certificates "k8s.io/api/certificates/v1" 17 | "k8s.io/client-go/kubernetes" 18 | "k8s.io/client-go/util/certificate" 19 | "k8s.io/klog" 20 | // k8s.io/kubernetes/pkg/apis/certificates" 21 | ) 22 | 23 | type Crtretriever func(*tls.ClientHelloInfo) (*tls.Certificate, error) 24 | 25 | // NewCertificateManager creates a certificate manager for the kubelet when retrieving a server certificate, or returns an error. 26 | // This function is inspired by Liqo implementation: 27 | // https://github.com/liqotech/liqo/blob/master/cmd/virtual-kubelet/root/http.go#L149 28 | func NewCertificateRetriever(kubeClient kubernetes.Interface, signer, nodeName string, nodeIP net.IP) (Crtretriever, error) { 29 | const ( 30 | vkCertsPath = "/tmp/certs" 31 | vkCertsPrefix = "virtual-kubelet" 32 | ) 33 | 34 | certificateStore, err := certificate.NewFileStore(vkCertsPrefix, vkCertsPath, vkCertsPath, "", "") 35 | if err != nil { 36 | return nil, fmt.Errorf("failed to initialize server certificate store: %w", err) 37 | } 38 | 39 | getTemplate := func() *x509.CertificateRequest { 40 | return &x509.CertificateRequest{ 41 | Subject: pkix.Name{ 42 | CommonName: fmt.Sprintf("system:node:%s", nodeName), 43 | Organization: []string{"system:nodes"}, 44 | }, 45 | IPAddresses: []net.IP{nodeIP}, 46 | } 47 | } 48 | 49 | mgr, err := certificate.NewManager(&certificate.Config{ 50 | ClientsetFn: func(_ *tls.Certificate) (kubernetes.Interface, error) { 51 | return kubeClient, nil 52 | }, 53 | GetTemplate: getTemplate, 54 | SignerName: signer, 55 | Usages: []certificates.KeyUsage{ 56 | // https://tools.ietf.org/html/rfc5280#section-4.2.1.3 57 | // 58 | // Digital signature allows the certificate to be used to verify 59 | // digital signatures used during TLS negotiation. 60 | certificates.UsageDigitalSignature, 61 | // KeyEncipherment allows the cert/key pair to be used to encrypt 62 | // keys, including the symmetric keys negotiated during TLS setup 63 | // and used for data transfer. 64 | certificates.UsageKeyEncipherment, 65 | // ServerAuth allows the cert to be used by a TLS server to 66 | // authenticate itself to a TLS client. 67 | certificates.UsageServerAuth, 68 | }, 69 | CertificateStore: certificateStore, 70 | Logf: klog.V(2).Infof, 71 | }) 72 | if err != nil { 73 | return nil, fmt.Errorf("failed to initialize server certificate manager: %w", err) 74 | } 75 | 76 | mgr.Start() 77 | 78 | return func(*tls.ClientHelloInfo) (*tls.Certificate, error) { 79 | cert := mgr.Current() 80 | if cert == nil { 81 | return nil, fmt.Errorf("no serving certificate available") 82 | } 83 | return cert, nil 84 | }, nil 85 | } 86 | 87 | // newSelfSignedCertificateRetriever creates a new retriever for self-signed certificates. 88 | func NewSelfSignedCertificateRetriever(nodeName string, nodeIP net.IP) Crtretriever { 89 | 90 | creator := func() (*tls.Certificate, time.Time, error) { 91 | expiration := time.Now().AddDate(1, 0, 0) // 1 year 92 | 93 | // Generate a new private key. 94 | publicKey, privateKey, err := ed25519.GenerateKey(cryptorand.Reader) 95 | if err != nil { 96 | return nil, expiration, fmt.Errorf("failed to generate a key pair: %w", err) 97 | } 98 | 99 | keyBytes, err := x509.MarshalPKCS8PrivateKey(privateKey) 100 | if err != nil { 101 | return nil, expiration, fmt.Errorf("failed to marshal the private key: %w", err) 102 | } 103 | 104 | // Generate the corresponding certificate. 105 | cert := &x509.Certificate{ 106 | Subject: pkix.Name{ 107 | CommonName: fmt.Sprintf("system:node:%s", nodeName), 108 | Organization: []string{"intertwin.eu"}, 109 | }, 110 | IPAddresses: []net.IP{nodeIP}, 111 | SerialNumber: big.NewInt(rand.Int63()), //nolint:gosec // A weak random generator is sufficient. 112 | NotBefore: time.Now(), 113 | NotAfter: expiration, 114 | KeyUsage: x509.KeyUsageDigitalSignature, 115 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, 116 | } 117 | 118 | certBytes, err := x509.CreateCertificate(cryptorand.Reader, cert, cert, publicKey, privateKey) 119 | if err != nil { 120 | return nil, expiration, fmt.Errorf("failed to create the self-signed certificate: %w", err) 121 | } 122 | 123 | // Encode the resulting certificate and private key as a single object. 124 | output, err := tls.X509KeyPair( 125 | pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certBytes}), 126 | pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyBytes})) 127 | if err != nil { 128 | return nil, expiration, fmt.Errorf("failed to create the X509 key pair: %w", err) 129 | } 130 | 131 | return &output, expiration, nil 132 | } 133 | 134 | // Cache the last generated cert, until it is not expired. 135 | var cert *tls.Certificate 136 | var expiration time.Time 137 | return func(*tls.ClientHelloInfo) (*tls.Certificate, error) { 138 | if cert == nil || expiration.Before(time.Now().AddDate(0, 0, 1)) { 139 | var err error 140 | cert, expiration, err = creator() 141 | if err != nil { 142 | return nil, err 143 | } 144 | } 145 | return cert, nil 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /pkg/virtualkubelet/config.go: -------------------------------------------------------------------------------- 1 | package virtualkubelet 2 | 3 | // Config holds the whole configuration 4 | type Config struct { 5 | InterlinkURL string `yaml:"InterlinkURL"` 6 | InterlinkPort string `yaml:"InterlinkPort"` 7 | KubernetesAPIAddr string `yaml:"KubernetesApiAddr"` 8 | KubernetesAPIPort string `yaml:"KubernetesApiPort"` 9 | KubernetesAPICaCrt string `yaml:"KubernetesApiCaCrt"` 10 | DisableProjectedVolumes bool `yaml:"DisableProjectedVolumes"` 11 | VKConfigPath string `yaml:"VKConfigPath"` 12 | VKTokenFile string `yaml:"VKTokenFile"` 13 | ServiceAccount string `yaml:"ServiceAccount"` 14 | Namespace string `yaml:"Namespace"` 15 | PodIP string `yaml:"PodIP"` 16 | VerboseLogging bool `yaml:"VerboseLogging"` 17 | ErrorsOnlyLogging bool `yaml:"ErrorsOnlyLogging"` 18 | HTTP HTTP `yaml:"HTTP"` 19 | KubeletHTTP HTTP `yaml:"KubeletHTTP"` 20 | Resources Resources `yaml:"Resources"` 21 | NodeLabels []string `yaml:"NodeLabels"` 22 | NodeTaints []TaintSpec `yaml:"NodeTaints"` 23 | } 24 | 25 | type HTTP struct { 26 | Insecure bool `yaml:"Insecure"` 27 | } 28 | 29 | type Resources struct { 30 | CPU string `yaml:"CPU,omitempty"` 31 | Memory string `yaml:"Memory,omitempty"` 32 | Pods string `yaml:"Pods,omitempty"` 33 | Accelerators []Accelerator `yaml:"Accelerators"` 34 | } 35 | 36 | type Accelerator struct { 37 | ResourceType string `yaml:"ResourceType"` 38 | Model string `yaml:"Model"` 39 | Available int `yaml:"Available"` 40 | } 41 | 42 | type TaintSpec struct { 43 | Key string `yaml:"Key"` 44 | Value string `yaml:"Value"` 45 | Effect string `yaml:"Effect"` 46 | } 47 | -------------------------------------------------------------------------------- /pkg/virtualkubelet/version.go: -------------------------------------------------------------------------------- 1 | package virtualkubelet 2 | 3 | var ( 4 | KubeletVersion = "test" 5 | ) 6 | -------------------------------------------------------------------------------- /systemd/user/.slurm-plugin.service.swp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interlink-hq/interLink/dc21f076d4f86e425f45d94e8ed1d6cc654bcd99/systemd/user/.slurm-plugin.service.swp -------------------------------------------------------------------------------- /systemd/user/interlink.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=This Unit is needed to automatically start the interLink API at system startup 3 | After=network.target 4 | 5 | [Service] 6 | Type=simple 7 | Restart=always 8 | RestartSec=3 9 | 10 | ExecStart=/home/USERNAME/.interlink/bin/interlink 11 | Environment="INTERLINKCONFIGPATH=/home/USERNAME/.interlink/config/InterLinkConfig.yaml" 12 | Environment="SHARED_FS=true" 13 | 14 | Environment="ENABLE_TRACING=0" 15 | 16 | StandardOutput=append:/home/USERNAME/.interlink/logs/interlink.log 17 | StandardError=append:/home/USERNAME/.interlink/logs/interlink.log 18 | 19 | [Install] 20 | WantedBy=multi-user.target 21 | -------------------------------------------------------------------------------- /systemd/user/oauth2-proxy.cfg: -------------------------------------------------------------------------------- 1 | client_id = "YOUR OIDC CLIENT ID" 2 | client_secret = "YOUR OIDC CLIENT SECRET" 3 | http_address = "0.0.0.0:30443" 4 | oidc_issuer_url = "https://iam.cloud.infn.it/" 5 | pass_authorization_header = "true" 6 | provider = "oidc" 7 | redirect_url = "http://localhost:8081/" 8 | oidc_extra_audiences = [ "users" ] 9 | upstreams = [ "unix:///leonardo/home/usera07cna/a07cna01/.interlink/interlink.sock", "http://localhost:30080/"] 10 | allowed_groups = [ "YOUR USER SUB"] 11 | validate_url = "https://iam.cloud.infn.it/token" 12 | oidc_groups_claim = "sub" 13 | email_domains = [ 14 | "*" 15 | ] 16 | cookie_secret= "2IS..nfCSKy4=" 17 | skip_auth_routes = [ 18 | "='*'" 19 | ] 20 | force_https = "true" 21 | https_address = "0.0.0.0:30443" 22 | tls_cert_file = "/home/USERNAME/.interlink/config/tls.crt" 23 | tls_key_file = "/home/USERNAME/.interlink/config/tls.key" 24 | 25 | tls_cipher_suites = ["TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA","TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA","TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256","TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA","TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384","TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256","TLS_RSA_WITH_AES_128_CBC_SHA","TLS_RSA_WITH_AES_128_GCM_SHA256","TLS_RSA_WITH_AES_256_CBC_SHA","TLS_RSA_WITH_AES_256_GCM_SHA384"] 26 | skip_jwt_bearer_tokens = "true" 27 | 28 | -------------------------------------------------------------------------------- /systemd/user/oauth2-proxy.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=This Unit is needed to automatically start the oauth2-proxy at system startup 3 | After=network.target 4 | 5 | [Service] 6 | Type=simple 7 | Restart=always 8 | RestartSec=3 9 | 10 | WorkDirectory=/home/USERNAME/.interlink/config/ 11 | 12 | ExecStart=/home/USERNAME/.interlink/bin/oauth2-proxy --config=/home/USERNAME/.config/systemd/user/oauth2-proxy.cfg 13 | 14 | StandardOutput=append:/home/USERNAME/interlink/logs/oauth2-proxy.log 15 | StandardError=append:/home/USERNAME/.interlink/logs/oauth2-proxy.log 16 | 17 | [Install] 18 | WantedBy=multi-user.target 19 | 20 | 21 | -------------------------------------------------------------------------------- /systemd/user/slurm-plugin.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=This Unit is needed to automatically start the SLURM plugin at system startup 3 | After=network.target 4 | 5 | [Service] 6 | Type=simple 7 | Restart=always 8 | RestartSec=3 9 | 10 | ExecStart=/home/USERNAME/.interlink/bin/plugin 11 | Environment="SLURMCONFIGPATH=/home/USERNAME/.interlink/config/plugin-config.yaml" 12 | Environment="SHARED_FS=true" 13 | 14 | Environment="ENABLE_TRACING=0" 15 | StandardOutput=append:/home/USERNAME/.interlink/logs/plugin.log 16 | StandardError=append:/home/USERNAME/.interlink/logs/plugin.log 17 | 18 | [Install] 19 | WantedBy=multi-user.target 20 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | --------------------------------------------------------------------------------