├── .github ├── contributors.yaml ├── linters │ ├── actionlint.yml │ ├── commitlint.config.mjs │ ├── licenserc.yml │ └── typos.toml ├── resources │ ├── cloudinit.yml │ ├── config.yml │ └── profiles.yml └── workflows │ ├── add-git-trailers.yml │ ├── build-latest.yml │ ├── build-trigger.yml │ ├── build.yml │ ├── ci.yml │ ├── ci_devel.yml │ ├── ci_on_push.yml │ ├── lint.yml │ ├── pr-trailers.yml │ ├── publish_docs.yml │ ├── stale.yml │ ├── unit_test.yml │ ├── validate-files-and-commits.yml │ └── vm_test.yml ├── .gitignore ├── .golangci.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── MAINTAINERS.md ├── Makefile ├── NOTICE ├── README.md ├── SECURITY.md ├── VERSION ├── cmd ├── containerd-shim-urunc-v2 │ └── main.go └── urunc │ ├── create.go │ ├── delete.go │ ├── kill.go │ ├── main.go │ ├── run.go │ ├── start.go │ └── utils.go ├── deployment └── urunc-deploy │ ├── Dockerfile │ ├── runtimeclasses │ └── runtimeclass.yaml │ ├── scripts │ └── install.sh │ ├── urunc-cleanup │ ├── base │ │ ├── kustomization.yaml │ │ └── urunc-cleanup.yaml │ └── overlays │ │ └── k3s │ │ ├── kustomization.yaml │ │ └── mount_k3s_conf.yaml │ ├── urunc-deploy │ ├── base │ │ ├── kustomization.yaml │ │ └── urunc-deploy.yaml │ └── overlays │ │ └── k3s │ │ ├── kustomization.yaml │ │ └── mount_k3s_conf.yaml │ └── urunc-rbac │ ├── kustomization.yaml │ └── urunc-rbac.yaml ├── docs ├── CNAME ├── Sample-images.md ├── assets │ ├── images │ │ ├── cncf-color.svg │ │ ├── cncf-white.svg │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── urunc-logo-dark.svg │ │ ├── urunc-logo-light.svg │ │ ├── urunc-logo.png │ │ └── urunc-nerdctl-example.gif │ ├── javascripts │ │ └── console-copy.js │ └── stylesheets │ │ └── theme.css ├── design │ ├── index.md │ └── seccomp.md ├── developer-guide │ ├── Code-of-Conduct.md │ ├── contribute.md │ ├── development.md │ ├── index.md │ ├── maintainers.md │ ├── security.md │ └── timestamps.md ├── hooks │ └── copyright.py ├── hypervisor-support.md ├── index.md ├── installation.md ├── overrides │ ├── main.html │ └── partials │ │ ├── breadcrumb.html │ │ ├── copyright.html │ │ └── logo.html ├── package │ ├── index.md │ ├── pre-built.md │ ├── reuse.md │ └── rootfs.md ├── quickstart.md ├── tutorials │ ├── How-to-urunc-on-k8s.md │ ├── Non-root-monitor-execution.md │ ├── eks-tutorial.md │ ├── existing-container-linux.md │ ├── index.md │ └── knative.md └── unikernel-support.md ├── go.mod ├── go.sum ├── internal ├── constants │ ├── constants.go │ └── network_constants.go └── metrics │ └── metrics.go ├── mkdocs.yml ├── netlify.toml ├── pkg ├── network │ ├── network.go │ ├── network_dynamic.go │ └── network_static.go └── unikontainers │ ├── config.go │ ├── config_test.go │ ├── hypervisors │ ├── firecracker.go │ ├── hedge.go │ ├── hvt.go │ ├── qemu.go │ ├── spt.go │ ├── utils.go │ └── vmm.go │ ├── ipc.go │ ├── ipc_message.go │ ├── ipc_test.go │ ├── storage.go │ ├── storage_test.go │ ├── unikernels │ ├── linux.go │ ├── mewz.go │ ├── mirage.go │ ├── rumprun.go │ ├── unikernel.go │ ├── unikraft.go │ └── utils.go │ ├── unikontainers.go │ ├── utils.go │ └── utils_test.go ├── requirements.txt ├── script ├── dm_create.sh ├── dm_reload.service ├── dm_reload.sh └── performance │ ├── __modules__.py │ ├── measure.py │ ├── measure_single.py │ └── measure_to_json.py └── tests ├── benchmarks └── benchmark_test.go └── e2e ├── common.go ├── crictl.go ├── ctr.go ├── docker.go ├── e2e_test.go ├── nerdctl.go ├── test_functions.go ├── tests_skeleton.go └── utils.go /.github/contributors.yaml: -------------------------------------------------------------------------------- 1 | users: 2 | ananos: 3 | name: Anastassios Nanos 4 | email: ananos@nubificus.co.uk 5 | cmainas: 6 | name: Charalampos Mainas 7 | email: cmainas@nubificus.co.uk 8 | gntouts: 9 | name: Georgios Ntoutsos 10 | email: gntouts@nubificus.co.uk 11 | johnp41: 12 | name: Ioannis Plakas 13 | email: iplakas@nubificus.co.uk 14 | -------------------------------------------------------------------------------- /.github/linters/actionlint.yml: -------------------------------------------------------------------------------- 1 | self-hosted-runner: 2 | labels: 3 | - '*-*-amd64' 4 | - '*-dind-*-amd64' 5 | - '*-*-arm*' 6 | - '*-dind-*-arm*' 7 | -------------------------------------------------------------------------------- /.github/linters/commitlint.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | extends: ['@commitlint/config-conventional'], 3 | helpUrl: 'https://www.conventionalcommits.org/', 4 | ignores: [ 5 | (msg) => /Signed-off-by: dependabot\[bot]/m.test(msg), 6 | ], 7 | rules: { 8 | 'header-max-length': [2, 'always', 72], 9 | 'body-max-line-length': [2, 'always', 80], 10 | 'subject-case': [2, 'never', ['start-case', 'pascal-case', 'upper-case']], 11 | 'trailer-exists': [2, 'always', 'Signed-off-by:'], 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /.github/linters/licenserc.yml: -------------------------------------------------------------------------------- 1 | header: 2 | license: 3 | spdx-id: Apache-2.0-short 4 | copyright-owner: "Nubificus LTD" 5 | copyright-year: "2024" 6 | software-name: urunc 7 | content: | 8 | Copyright (c) 2023-2025, Nubificus LTD 9 | 10 | Licensed under the Apache License, Version 2.0 (the "License"); 11 | you may not use this file except in compliance with the License. 12 | You may obtain a copy of the License at 13 | 14 | http://www.apache.org/licenses/LICENSE-2.0 15 | 16 | Unless required by applicable law or agreed to in writing, software 17 | distributed under the License is distributed on an "AS IS" BASIS, 18 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 19 | See the License for the specific language governing permissions and 20 | limitations under the License. 21 | 22 | paths-ignore: 23 | - '**/.*' 24 | - '**/*.md' 25 | - '**/*.txt' 26 | - '**/*.build' 27 | - '**/*.options' 28 | - '**/*.cmake' 29 | - '**/*.cmake.in' 30 | - '**/*.pc.in' 31 | - '**/*.toml' 32 | - '**/*.service' 33 | - '.github' 34 | - '**/go.mod' 35 | - '**/go.sum' 36 | - 'LICENSE' 37 | - 'NOTICE' 38 | - 'VERSION' 39 | - 'build*' 40 | - 'docs/CNAME' 41 | - 'docs/overrides/**' 42 | - 'docs/assets/stylesheets/**' 43 | - 'docs/assets/images/**' 44 | - 'docs/assets/javascripts/**' 45 | - 'docs/hooks/**' 46 | - 'pkg/unikontainers/ipc_message.go' 47 | 48 | language: 49 | Go: 50 | extensions: 51 | - ".go" 52 | bash: 53 | extensions: 54 | - ".sh" 55 | python: 56 | extensions: 57 | - ".py" 58 | 59 | comment: on-failure 60 | -------------------------------------------------------------------------------- /.github/linters/typos.toml: -------------------------------------------------------------------------------- 1 | [default] 2 | extend-ignore-re = [ 3 | "(?Rm)^.*(#|//)\\s*spellchecker:disable-line$", 4 | "(?s)(#|//)\\s*spellchecker:off.*?\\n\\s*(#|//)\\s*spellchecker:on" 5 | ] 6 | 7 | [default.extend-words] 8 | SEH = "SEH" 9 | ser = "ser" 10 | nd = "nd" 11 | 12 | [files] 13 | extend-exclude = [ 14 | "subprojects/*", 15 | "third-party/*", 16 | "test/catch2/*", 17 | "test/fff/*" 18 | ] 19 | -------------------------------------------------------------------------------- /.github/resources/cloudinit.yml: -------------------------------------------------------------------------------- 1 | #cloud-config 2 | users: 3 | - name: 'testuser' 4 | groups: users,sudo 5 | shell: /bin/bash 6 | lock_passwd: false 7 | ssh_authorized_keys: -------------------------------------------------------------------------------- /.github/resources/config.yml: -------------------------------------------------------------------------------- 1 | default: 2 | autostart: false 3 | client: local 4 | cloudinit: true 5 | cpuhotplug: false 6 | cpumodel: host-model 7 | diskinterface: virtio 8 | disks: 9 | - default: true 10 | size: 20 11 | disksize: 20 12 | diskthin: true 13 | enableroot: true 14 | guestid: guestrhel764 15 | insecure: true 16 | jenkinsmode: podman 17 | keep_networks: false 18 | memory: 4096 19 | memoryhotplug: false 20 | nested: true 21 | nets: 22 | - br0 23 | networkwait: 0 24 | notify: false 25 | notifymethods: 26 | - pushbullet 27 | numcpus: 5 28 | pool: default 29 | privatekey: false 30 | reservedns: false 31 | reservehost: false 32 | reserveip: false 33 | rhnregister: true 34 | rhnserver: https://subscription.rhsm.redhat.com 35 | rhnunregister: false 36 | rng: false 37 | sharedkey: true 38 | start: true 39 | storemetadata: false 40 | tempkey: true 41 | tpm: false 42 | tunnel: false 43 | tunneldir: /var/www/html 44 | tunnelport: 22 45 | tunneluser: root 46 | vmrules_strict: false 47 | vnc: true 48 | wait: false 49 | waittimeout: 0 50 | yamlinventory: false 51 | cmds: 52 | - echo "root:unix1234" | chpasswd 53 | dell03: 54 | host: dell03 55 | user: kcli 56 | protocol: ssh 57 | pool: default 58 | type: kvm 59 | nets: 60 | - br0 61 | fuji00: 62 | host: fuji00 63 | user: kcli 64 | protocol: ssh 65 | pool: default 66 | type: kvm 67 | nets: 68 | - br0 69 | 70 | 71 | -------------------------------------------------------------------------------- /.github/resources/profiles.yml: -------------------------------------------------------------------------------- 1 | ubuntuthin: 2 | client: dell03 3 | image: ubuntutesturunc_0.qcow2 4 | numcpus: 1 5 | memory: 1024 6 | nets: 7 | - br0 8 | disks: 9 | - size: 10 10 | keys: 11 | - mydefaultkey.pub 12 | shutdown_flag: true 13 | 14 | ubuntuurunc: 15 | client: fuji00 16 | image: ubuntuurunc 17 | numcpus: 1 18 | memory: 1024 19 | nets: 20 | - br0 21 | disks: [20] 22 | keys: 23 | - mydefaultkey.pub 24 | shutdown_flag: true 25 | 26 | ubuntufat: 27 | client: fuji00 28 | image: godockerubuntu_0.qcow2 29 | numcpus: 2 30 | memory: 4096 31 | nets: 32 | - br0 33 | disks: 34 | - size: 30 35 | keys: 36 | - mydefaultkey.pub 37 | shutdown_flag: true 38 | 39 | -------------------------------------------------------------------------------- /.github/workflows/add-git-trailers.yml: -------------------------------------------------------------------------------- 1 | name: Add Git Trailers to PR commits 2 | 3 | on: 4 | workflow_call: 5 | secrets: 6 | GIT_CLONE_PAT: 7 | required: false 8 | URUNC_BOT_PRIVATE_KEY: 9 | required: true 10 | 11 | jobs: 12 | git-trailers: 13 | name: Add Git Trailers 14 | runs-on: [base-dind-2204-amd64] 15 | permissions: 16 | contents: write 17 | pull-requests: write 18 | steps: 19 | - name: Exit if PR is not rebaseable 20 | if: ${{ github.event.pull_request.rebaseable != null && github.event.pull_request.rebaseable == false }} 21 | run: exit 1 22 | 23 | - name: Checkout code 24 | uses: actions/checkout@v4 25 | with: 26 | fetch-depth: 0 27 | ref: ${{ github.event.pull_request.head.sha }} 28 | 29 | - name: Append git trailers 30 | uses: nubificus/git-trailers@feat_use_rebase 31 | with: 32 | user_info: .github/contributors.yaml 33 | 34 | - name: Generate urunc-bot token 35 | id: generate-token 36 | uses: actions/create-github-app-token@v1 37 | with: 38 | app-id: ${{ vars.URUNC_BOT_APP_ID }} 39 | private-key: ${{ secrets.URUNC_BOT_PRIVATE_KEY }} 40 | 41 | - name: Trigger required tests re-run 42 | run: | 43 | curl -X DELETE \ 44 | -H "Accept: application/vnd.github.v3+json" \ 45 | -H "Authorization: Bearer ${{ steps.generate-token.outputs.token }}" \ 46 | "https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels/ok-to-test" 47 | sleep 5 48 | curl -X POST \ 49 | -H "Accept: application/vnd.github.v3+json" \ 50 | -H "Authorization: Bearer ${{ steps.generate-token.outputs.token }}" \ 51 | "https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels" \ 52 | -d '{ 53 | "labels": ["ok-to-test"] 54 | }' 55 | -------------------------------------------------------------------------------- /.github/workflows/build-trigger.yml: -------------------------------------------------------------------------------- 1 | name: 🍜 Build/publish urunc-deploy 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | paths: 7 | - 'deployment/urunc-deploy/Dockerfile' 8 | - 'deployment/urunc-deploy/scripts/install.sh' 9 | - 'cmd/**' 10 | - 'pkg/**' 11 | - 'internal/**' 12 | - 'Makefile' 13 | - 'VERSION' 14 | workflow_dispatch: 15 | 16 | concurrency: 17 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | get-changed-files: 22 | runs-on: ubuntu-latest 23 | outputs: 24 | version: ${{ steps.check.outputs.version_changed }} 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | 29 | - name: Check if VERSION file changed 30 | id: check 31 | run: | 32 | # Get commit range 33 | if [[ "${{ github.event_name }}" == "push" ]]; then 34 | base_ref="${{ github.event.before }}" 35 | head_ref="${{ github.sha }}" 36 | else 37 | base_ref="HEAD~1" 38 | head_ref="HEAD" 39 | fi 40 | 41 | git fetch origin $base_ref --depth=1 || true 42 | changed=$(git diff --name-only "$base_ref" "$head_ref" | grep -c '^VERSION$' || true) 43 | 44 | if [[ "$changed" -gt 0 ]]; then 45 | echo "VERSION file changed" 46 | echo "version_changed=true" >> $GITHUB_OUTPUT 47 | else 48 | echo "VERSION not changed" 49 | echo "version_changed=false" >> $GITHUB_OUTPUT 50 | fi 51 | 52 | build-dockerfiles-deploy: 53 | name: Build urunc-deploy container image 54 | needs: get-changed-files 55 | uses: ./.github/workflows/build-latest.yml 56 | secrets: inherit 57 | with: 58 | runner: '["base", "dind", "2204"]' 59 | runner-archs: '["amd64", "arm64"]' 60 | dockerfiles: 'deployment/urunc-deploy/Dockerfile' 61 | version-tag: ${{ needs.get-changed-files.outputs.version == 'true' }} 62 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | ref: 7 | required: true 8 | type: string 9 | default: '' 10 | runner: 11 | type: string 12 | default: '["base", "dind", "2204"]' 13 | runner-archs: 14 | type: string 15 | default: '["amd64", "arm64"]' 16 | runner-arch-map: 17 | type: string 18 | default: '[{"amd64":"x86_64", "arm64":"aarch64", "arm":"armv7l"}]' 19 | secrets: 20 | GIT_CLONE_PAT: 21 | required: false 22 | 23 | #pull_request: 24 | #branches: [ "main" ] 25 | #types: 26 | #- synchronize 27 | #- labeled 28 | 29 | workflow_dispatch: 30 | 31 | jobs: 32 | build: 33 | runs-on: ${{ format('{0}-{1}', join(fromJSON(inputs.runner), '-'), matrix.archconfig) }} 34 | strategy: 35 | matrix: 36 | archconfig: ["${{ fromJSON(inputs.runner-archs) }}"] 37 | fail-fast: false 38 | 39 | steps: 40 | 41 | - name: Checkout code 42 | uses: actions/checkout@v4 43 | with: 44 | ref: ${{ github.event.pull_request.head.sha }} 45 | 46 | - name: Display Go version 47 | run: | 48 | go version 49 | 50 | - name: Set ref and repo from PR or dispatch 51 | id: set-ref 52 | run: | 53 | if [[ "${{ github.event_name }}" == "pull_request" || "${{ github.event_name }}" == "pull_request_target" ]]; then 54 | echo "ref=${{ github.event.pull_request.head.ref }}" >> "$GITHUB_OUTPUT" 55 | echo "repo=${{ github.event.pull_request.head.repo.full_name }}" >> "$GITHUB_OUTPUT" 56 | else 57 | echo "ref=${{ github.ref_name }}" >> "$GITHUB_OUTPUT" 58 | echo "repo=${{ github.repository }}" >> "$GITHUB_OUTPUT" 59 | fi 60 | 61 | - name: Build urunc binaries 62 | id: build-urunc-binaries 63 | run: | 64 | make 65 | 66 | - name: Upload urunc_arm64 to S3 67 | if: matrix.archconfig == 'arm64' 68 | uses: cloudkernels/minio-upload@v3 69 | with: 70 | url: https://s3.nbfc.io 71 | access-key: ${{ secrets.AWS_ACCESS_KEY }} 72 | secret-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 73 | local-path: dist/urunc_static_arm64 74 | remote-path: nbfc-assets/github/urunc/dist/${{ steps.set-ref.outputs.ref }}/${{ matrix.archconfig }}/ 75 | policy: 1 76 | 77 | - name: Upload urunc_amd64 to S3 78 | if: matrix.archconfig == 'amd64' 79 | uses: cloudkernels/minio-upload@v3 80 | with: 81 | url: https://s3.nbfc.io 82 | access-key: ${{ secrets.AWS_ACCESS_KEY }} 83 | secret-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 84 | local-path: dist/urunc_static_amd64 85 | remote-path: nbfc-assets/github/urunc/dist/${{ steps.set-ref.outputs.ref }}/${{ matrix.archconfig }}/ 86 | policy: 1 87 | 88 | - name: Upload containerd-shim-urunc-v2_arm64 to S3 89 | if: matrix.archconfig == 'arm64' 90 | uses: cloudkernels/minio-upload@v3 91 | with: 92 | url: https://s3.nbfc.io 93 | access-key: ${{ secrets.AWS_ACCESS_KEY }} 94 | secret-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 95 | local-path: dist/containerd-shim-urunc-v2_static_arm64 96 | remote-path: nbfc-assets/github/urunc/dist/${{ steps.set-ref.outputs.ref }}/${{ matrix.archconfig }}/ 97 | policy: 1 98 | 99 | - name: Upload containerd-shim-urunc-v2_amd64 to S3 100 | if: matrix.archconfig == 'amd64' 101 | uses: cloudkernels/minio-upload@v3 102 | with: 103 | url: https://s3.nbfc.io 104 | access-key: ${{ secrets.AWS_ACCESS_KEY }} 105 | secret-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 106 | local-path: dist/containerd-shim-urunc-v2_static_amd64 107 | remote-path: nbfc-assets/github/urunc/dist/${{ steps.set-ref.outputs.ref }}/${{ matrix.archconfig }}/ 108 | policy: 1 109 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: urunc CI 2 | on: 3 | workflow_call: 4 | inputs: 5 | ref: 6 | required: true 7 | type: string 8 | skip-build: 9 | required: false 10 | type: string 11 | default: "no" 12 | skip-lint: 13 | required: false 14 | type: string 15 | default: "no" 16 | 17 | permissions: 18 | contents: read 19 | pull-requests: read 20 | packages: write 21 | id-token: write 22 | attestations: write 23 | 24 | jobs: 25 | validate-files-and-commits: 26 | if: ${{ inputs.skip-lint != 'yes' }} 27 | name: Lint Files & commits 28 | uses: ./.github/workflows/validate-files-and-commits.yml 29 | with: 30 | ref: ${{ inputs.ref }} 31 | secrets: inherit 32 | 33 | lint: 34 | name: Lint code 35 | if: ${{ inputs.skip-lint != 'yes' }} 36 | uses: ./.github/workflows/lint.yml 37 | with: 38 | ref: ${{ inputs.ref }} 39 | secrets: inherit 40 | 41 | build: 42 | if: ${{ inputs.skip-build != 'yes' }} 43 | name: Build 44 | uses: ./.github/workflows/build.yml 45 | with: 46 | ref: ${{ inputs.ref }} 47 | secrets: inherit 48 | 49 | unit_test: 50 | if: ${{ inputs.skip-build != 'yes' }} 51 | name: Unit tests 52 | uses: ./.github/workflows/unit_test.yml 53 | with: 54 | ref: ${{ inputs.ref }} 55 | secrets: inherit 56 | 57 | #FIXME: run for arm64 58 | vm_test: 59 | if: ${{ inputs.skip-build != 'yes' }} 60 | needs: [build,unit_test] 61 | name: E2E test 62 | uses: ./.github/workflows/vm_test.yml 63 | with: 64 | ref: ${{ inputs.ref }} 65 | secrets: inherit 66 | 67 | -------------------------------------------------------------------------------- /.github/workflows/ci_devel.yml: -------------------------------------------------------------------------------- 1 | name: urunc CI (manually triggered) 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | skip_build: 6 | description: 'Skip the build job?' 7 | required: false 8 | default: "no" 9 | type: string 10 | skip_lint: 11 | description: 'Skip the lint job?' 12 | required: false 13 | default: "no" 14 | type: string 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | ci-on-push: 21 | permissions: 22 | contents: read 23 | packages: write 24 | id-token: write 25 | attestations: write 26 | pull-requests: read 27 | uses: ./.github/workflows/ci.yml 28 | with: 29 | ref: ${{ github.sha }} 30 | skip-build: ${{ inputs.skip_build }} 31 | skip-lint: ${{ inputs.skip_lint }} 32 | secrets: inherit 33 | -------------------------------------------------------------------------------- /.github/workflows/ci_on_push.yml: -------------------------------------------------------------------------------- 1 | name: urunc CI 2 | 3 | on: 4 | pull_request_target: 5 | branches: ["main"] 6 | types: [synchronize, labeled, unlabeled] 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | check-labels: 14 | runs-on: ubuntu-latest 15 | outputs: 16 | skip_build: ${{ steps.set-vars.outputs.skip_build }} 17 | skip_lint: ${{ steps.set-vars.outputs.skip_lint }} 18 | ok_to_test: ${{ steps.set-vars.outputs.ok_to_test }} 19 | steps: 20 | - name: Fetch PR Labels 21 | id: get-labels 22 | uses: actions/github-script@v7 23 | with: 24 | script: | 25 | const prNumber = context.payload.pull_request.number; 26 | const labels = await github.rest.issues.listLabelsOnIssue({ 27 | ...context.repo, 28 | issue_number: prNumber 29 | }); 30 | const names = labels.data.map(l => l.name); 31 | core.setOutput("labels", names.join(',')); 32 | 33 | - name: Set skip flags 34 | id: set-vars 35 | run: | 36 | LABELS="${{ steps.get-labels.outputs.labels }}" 37 | echo "Labels: $LABELS" 38 | 39 | if [[ "$LABELS" == *"skip-build"* ]]; then 40 | echo "skip_build=yes" >> $GITHUB_OUTPUT 41 | else 42 | echo "skip_build=no" >> $GITHUB_OUTPUT 43 | fi 44 | 45 | if [[ "$LABELS" == *"skip-lint"* ]]; then 46 | echo "skip_lint=yes" >> $GITHUB_OUTPUT 47 | else 48 | echo "skip_lint=no" >> $GITHUB_OUTPUT 49 | fi 50 | 51 | if [[ "$LABELS" == *"ok-to-test"* ]]; then 52 | echo "ok_to_test=yes" >> $GITHUB_OUTPUT 53 | else 54 | echo "ok_to_test=no" >> $GITHUB_OUTPUT 55 | fi 56 | 57 | ci-on-push: 58 | needs: [check-labels] 59 | permissions: 60 | contents: read 61 | packages: write 62 | id-token: write 63 | attestations: write 64 | pull-requests: read 65 | if: ${{ needs.check-labels.outputs.ok_to_test == 'yes' }} 66 | uses: ./.github/workflows/ci.yml 67 | with: 68 | ref: ${{ github.event.pull_request.head.sha }} 69 | skip-build: ${{ needs.check-labels.outputs.skip_build }} 70 | skip-lint: ${{ needs.check-labels.outputs.skip_lint }} 71 | secrets: inherit 72 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Code linting 2 | on: 3 | workflow_call: 4 | inputs: 5 | ref: 6 | required: true 7 | type: string 8 | default: '' 9 | runner: 10 | type: string 11 | default: '["base", "dind", "2204"]' 12 | runner-archs: 13 | type: string 14 | default: '["amd64"]' 15 | runner-arch-map: 16 | type: string 17 | default: '[{"amd64":"x86_64", "arm64":"aarch64", "arm":"armv7l"}]' 18 | secrets: 19 | GIT_CLONE_PAT: 20 | required: false 21 | 22 | permissions: 23 | contents: read 24 | # allow read access to pull request. Use with `only-new-issues` option. 25 | pull-requests: read 26 | 27 | jobs: 28 | golangci: 29 | name: lint 30 | runs-on: ${{ format('{0}-{1}', join(fromJSON(inputs.runner), '-'), matrix.archconfig) }} 31 | strategy: 32 | matrix: 33 | archconfig: ["${{ fromJSON(inputs.runner-archs) }}"] 34 | fail-fast: false 35 | steps: 36 | - uses: actions/checkout@v3 37 | with: 38 | ref: ${{ github.event.pull_request.head.sha }} 39 | - uses: actions/setup-go@v4 40 | with: 41 | go-version: '1.24.1' 42 | cache: false 43 | - name: golangci-lint 44 | uses: golangci/golangci-lint-action@v3 45 | with: 46 | version: v1.64 47 | 48 | # show only new issues if it's a pull request. The default value is `false`. 49 | only-new-issues: true 50 | -------------------------------------------------------------------------------- /.github/workflows/pr-trailers.yml: -------------------------------------------------------------------------------- 1 | name: Add Git Trailers to PR commits 2 | 3 | on: 4 | pull_request_review: 5 | types: [submitted] 6 | 7 | concurrency: 8 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 9 | cancel-in-progress: true 10 | 11 | jobs: 12 | git-trailers: 13 | name: Add Git Trailers to PR commits 14 | if: ${{ github.event.pull_request.base.ref == 'main' && github.event.review.state == 'approved' }} 15 | uses: ./.github/workflows/add-git-trailers.yml 16 | secrets: inherit 17 | -------------------------------------------------------------------------------- /.github/workflows/publish_docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy documentation website 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | paths: 7 | - 'docs/**' 8 | - 'mkdocs.yml' 9 | - 'requirements.txt' 10 | 11 | permissions: 12 | contents: write # Needed for GitHub Pages deployment 13 | 14 | jobs: 15 | deploy: 16 | runs-on: [base-dind-2204-amd64] 17 | 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v4 21 | 22 | - name: Install dependencies 23 | run: | 24 | pip3 install -r requirements.txt 25 | echo "PATH=$PATH:$HOME/.local/bin" >> "$GITHUB_ENV" 26 | 27 | - name: Build and deploy MkDocs site 28 | run: | 29 | mkdocs gh-deploy --force 30 | 31 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Automatically close stale PRs' 2 | on: 3 | schedule: 4 | - cron: '0 0 * * *' 5 | workflow_dispatch: 6 | 7 | jobs: 8 | stale: 9 | runs-on: ubuntu-22.04 10 | steps: 11 | - uses: actions/stale@v9 12 | with: 13 | stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 15 days.' 14 | stale-pr-message: 'This PR is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 15 days.' 15 | close-issue-message: 'This issue was closed because it has been stalled for 75 days with no activity.' 16 | close-pr-message: 'This PR was closed because it has been stalled for 75 days with no activity.' 17 | days-before-issue-stale: 60 18 | days-before-pr-stale: 60 19 | days-before-issue-close: 15 20 | days-before-pr-close: 15 21 | exempt-issue-labels: 'design,dev,enhancement,documentation,bug,feature' 22 | -------------------------------------------------------------------------------- /.github/workflows/unit_test.yml: -------------------------------------------------------------------------------- 1 | name: Run unit tests 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | ref: 7 | type: string 8 | default: '' 9 | required: true 10 | runner: 11 | type: string 12 | default: '["base", "dind", "2204"]' 13 | runner-archs: 14 | type: string 15 | default: '["amd64", "arm64"]' 16 | runner-arch-map: 17 | type: string 18 | default: '[{"amd64":"x86_64", "arm64":"aarch64", "arm":"armv7l"}]' 19 | secrets: 20 | GIT_CLONE_PAT: 21 | required: false 22 | 23 | permissions: 24 | contents: read 25 | pull-requests: read 26 | 27 | jobs: 28 | unit-test: 29 | name: unit-test 30 | runs-on: ${{ format('{0}-{1}', join(fromJSON(inputs.runner), '-'), matrix.archconfig) }} 31 | strategy: 32 | matrix: 33 | archconfig: ["${{ fromJSON(inputs.runner-archs) }}"] 34 | fail-fast: false 35 | steps: 36 | - uses: actions/checkout@v4 37 | with: 38 | ref: ${{ github.event.pull_request.head.sha }} 39 | - name: Run unikontainers pkg unit tests 40 | run: make unittest 41 | 42 | -------------------------------------------------------------------------------- /.github/workflows/validate-files-and-commits.yml: -------------------------------------------------------------------------------- 1 | name: Validate Files and Commit Messages 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | ref: 7 | required: true 8 | type: string 9 | default: '' 10 | runner: 11 | type: string 12 | default: '["base", "dind", "2204"]' 13 | runner-archs: 14 | type: string 15 | default: '["amd64"]' 16 | runner-arch-map: 17 | type: string 18 | default: '[{"amd64":"x86_64", "arm64":"aarch64", "arm":"armv7l"}]' 19 | secrets: 20 | GIT_CLONE_PAT: 21 | required: false 22 | 23 | jobs: 24 | linter-commitlint: 25 | name: Lint Commit Messages 26 | runs-on: ${{ format('{0}-{1}', join(fromJSON(inputs.runner), '-'), matrix.arch) }} 27 | strategy: 28 | matrix: 29 | arch: ["${{ fromJSON(inputs.runner-archs) }}"] 30 | fail-fast: false 31 | steps: 32 | - name: Checkout Code 33 | uses: actions/checkout@v4 34 | 35 | - name: Run commitlint 36 | uses: wagoid/commitlint-github-action@v6 37 | with: 38 | configFile: .github/linters/commitlint.config.mjs 39 | 40 | linter-typos: 41 | name: Spell Check Repo 42 | runs-on: ${{ format('{0}-{1}', join(fromJSON(inputs.runner), '-'), matrix.arch) }} 43 | strategy: 44 | matrix: 45 | arch: ["${{ fromJSON(inputs.runner-archs) }}"] 46 | fail-fast: false 47 | steps: 48 | - name: Checkout Code 49 | uses: actions/checkout@v4 50 | 51 | - name: Spell check 52 | uses: crate-ci/typos@master 53 | with: 54 | config: .github/linters/typos.toml 55 | 56 | linter-license-eye: 57 | name: Check License Headers 58 | runs-on: ${{ format('{0}-{1}', join(fromJSON(inputs.runner), '-'), matrix.arch) }} 59 | strategy: 60 | matrix: 61 | arch: ["${{ fromJSON(inputs.runner-archs) }}"] 62 | fail-fast: false 63 | steps: 64 | - name: Checkout Code 65 | uses: actions/checkout@v4 66 | 67 | - name: Fix paths 68 | run: | 69 | echo "Unset GO PATH, as the following action conflicts with the already installed version" 70 | export GOPATH= 71 | echo "GOPATH=${GOPATH}" >> "$GITHUB_ENV" 72 | export GOTOOLDIR= 73 | echo "GOTOOLDIR=${GOTOOLDIR}" >> "$GITHUB_ENV" 74 | export GOROOT= 75 | echo "GOROOT=${GOROOT}" >> "$GITHUB_ENV" 76 | shell: bash 77 | 78 | - name: Run license-eye 79 | uses: apache/skywalking-eyes/header@main 80 | with: 81 | config: .github/linters/licenserc.yml 82 | token: ${{ secrets.GITHUB_TOKEN }} 83 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Vim swap files 9 | *.swp 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | vendor/ 19 | 20 | # unibuild image tars 21 | *.tar 22 | 23 | # binaries 24 | dist/ 25 | 26 | # temporary files and dirs used while building images 27 | temp/ 28 | 29 | # dir for test bundles 30 | bundles/ 31 | 32 | # python-related directories 33 | __pycache__/ 34 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable: 3 | - errcheck 4 | - gofmt 5 | - goimports 6 | - gosec 7 | - gocritic 8 | - misspell 9 | - revive 10 | - unused 11 | 12 | issues: 13 | uniq-by-line: false 14 | 15 | run: 16 | issues-exit-code: 1 17 | timeout: 5m -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | Please see [Code of Conduct page](/docs/developer-guide/Code-of-Conduct.md) 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribute to the `urunc` project 2 | 3 | Please see [Contributing](/docs/developer-guide/contribute.md) 4 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | # The maintainers of `urunc` 2 | 3 | Please see [Maintainers document](/docs/developer-guide/maintainers.md) 4 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | urunc Container Runtime 2 | Copyright 2023 Nubificus LTD. or its affiliates. All Rights Reserved. 3 | 4 | This product includes software developed at Nubificus LTD 5 | (https://www.nubificus.co.uk) -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security policy 2 | 3 | Please read [Security policy document](/docs/developer-guide/security.md) 4 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.5.0 -------------------------------------------------------------------------------- /cmd/containerd-shim-urunc-v2/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2025, Nubificus LTD 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "context" 19 | 20 | "github.com/containerd/containerd/runtime/v2/runc/manager" 21 | _ "github.com/containerd/containerd/runtime/v2/runc/task/plugin" 22 | "github.com/containerd/containerd/runtime/v2/shim" 23 | ) 24 | 25 | func main() { 26 | shim.RunManager(context.Background(), manager.NewShimManager("io.containerd.urunc.v2")) 27 | } 28 | -------------------------------------------------------------------------------- /cmd/urunc/delete.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2025, Nubificus LTD 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "os" 19 | "runtime" 20 | 21 | "github.com/sirupsen/logrus" 22 | "github.com/urfave/cli" 23 | ) 24 | 25 | var deleteCommand = cli.Command{ 26 | Name: "delete", 27 | Usage: "delete any resources held by the container often used with detached container", 28 | ArgsUsage: ` 29 | 30 | Where "" is the name for the instance of the container. 31 | 32 | EXAMPLE: 33 | For example, if the container id is "ubuntu01" and runc list currently shows the 34 | status of "ubuntu01" as "stopped" the following will delete resources held for 35 | "ubuntu01" removing "ubuntu01" from the runc list of containers: 36 | 37 | # runc delete ubuntu01`, 38 | Flags: []cli.Flag{ 39 | cli.BoolFlag{ 40 | Name: "force, f", 41 | Usage: "Forcibly deletes the container if it is still running (uses SIGKILL)", 42 | }, 43 | }, 44 | Action: func(context *cli.Context) error { 45 | runtime.GOMAXPROCS(1) 46 | runtime.LockOSThread() 47 | logrus.WithField("args", os.Args).Info("urunc INVOKED") 48 | if err := checkArgs(context, 1, exactArgs); err != nil { 49 | return err 50 | } 51 | 52 | // get Unikontainer data from state.json 53 | unikontainer, err := getUnikontainer(context) 54 | if err != nil { 55 | return err 56 | } 57 | if context.Bool("force") { 58 | err := unikontainer.Kill() 59 | if err != nil { 60 | return err 61 | } 62 | } 63 | return unikontainer.Delete() 64 | }, 65 | } 66 | -------------------------------------------------------------------------------- /cmd/urunc/kill.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2025, Nubificus LTD 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "os" 19 | "runtime" 20 | 21 | "github.com/sirupsen/logrus" 22 | "github.com/urfave/cli" 23 | ) 24 | 25 | var killCommand = cli.Command{ 26 | Name: "kill", 27 | Usage: "kill sends the specified signal (default: SIGTERM) to the container's init process", 28 | ArgsUsage: ` [signal] 29 | 30 | Where "" is the name for the instance of the container and 31 | "[signal]" is the signal to be sent to the init process. 32 | 33 | EXAMPLE: 34 | For example, if the container id is "ubuntu01" the following will send a "KILL" 35 | signal to the init process of the "ubuntu01" container: 36 | 37 | # runc kill ubuntu01 KILL`, 38 | Flags: []cli.Flag{ 39 | cli.BoolFlag{ 40 | Name: "all, a", 41 | Usage: "send the specified signal to all processes inside the container", 42 | }, 43 | }, 44 | Action: func(context *cli.Context) error { 45 | runtime.GOMAXPROCS(1) 46 | runtime.LockOSThread() 47 | // FIXME: Remove or change level of log 48 | logrus.WithField("args", os.Args).Info("urunc INVOKED") 49 | if err := checkArgs(context, 1, minArgs); err != nil { 50 | return err 51 | } 52 | if err := checkArgs(context, 2, maxArgs); err != nil { 53 | return err 54 | } 55 | 56 | // get Unikontainer data from state.json 57 | unikontainer, err := getUnikontainer(context) 58 | if err != nil { 59 | return err 60 | } 61 | return unikontainer.Kill() 62 | }, 63 | } 64 | -------------------------------------------------------------------------------- /cmd/urunc/run.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2025, Nubificus LTD 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | 21 | "github.com/sirupsen/logrus" 22 | "github.com/urfave/cli" 23 | ) 24 | 25 | var runUsage = ` 26 | 27 | Where "" is your name for the instance of the container that you 28 | are starting. The name you provide for the container instance must be unique on 29 | your host.` 30 | 31 | var runDescription = `The run command creates an instance of a container for a bundle. The bundle 32 | is a directory with a specification file named "` + specConfig + `" and a root 33 | filesystem. 34 | 35 | The specification file includes an args parameter. The args parameter is used 36 | to specify command(s) that get run when the container is started. To change the 37 | command(s) that get executed on start, edit the args parameter of the spec. See 38 | "runc spec --help" for more explanation.` 39 | 40 | var runCommand = cli.Command{ 41 | Name: "run", 42 | Usage: "create and run a container", 43 | ArgsUsage: runUsage, 44 | Description: runDescription, 45 | Flags: []cli.Flag{ 46 | cli.StringFlag{ 47 | Name: "bundle, b", 48 | Value: "", 49 | Usage: `path to the root of the bundle directory, defaults to the current directory`, 50 | }, 51 | cli.StringFlag{ 52 | Name: "console-socket", 53 | Value: "", 54 | Usage: "path to an AF_UNIX socket which will receive a file descriptor referencing the master end of the console's pseudoterminal", 55 | }, 56 | cli.StringFlag{ 57 | Name: "pid-file", 58 | Value: "", 59 | Usage: "specify the file to write the process id to", 60 | }, 61 | }, 62 | Action: func(context *cli.Context) error { 63 | // FIXME: Remove or change level of log 64 | logrus.WithField("args", os.Args).Info("urunc INVOKED") 65 | 66 | if err := checkArgs(context, 1, exactArgs); err != nil { 67 | return err 68 | } 69 | 70 | // FIXME: This is a refactor of what the previous code did, however I have a feeling 71 | // that it will not work... 72 | if err := reexecUnikontainer(context); err != nil { 73 | return err 74 | } 75 | if err := startUnikontainer(context); err != nil { 76 | return err 77 | } 78 | return fmt.Errorf("urunc run failed: %w", nil) 79 | }, 80 | } 81 | -------------------------------------------------------------------------------- /cmd/urunc/start.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2025, Nubificus LTD 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "os" 19 | 20 | "github.com/sirupsen/logrus" 21 | "github.com/urfave/cli" 22 | ) 23 | 24 | var startCommand = cli.Command{ 25 | Name: "start", 26 | Usage: "executes the user defined process in a created container", 27 | ArgsUsage: ` 28 | 29 | Where "" is your name for the instance of the container that you 30 | are starting. The name you provide for the container instance must be unique on 31 | your host.`, 32 | Description: `The start command executes the user defined process in a created container.`, 33 | Action: func(context *cli.Context) error { 34 | // FIXME: Remove or change level of log 35 | logrus.WithField("args", os.Args).Info("urunc INVOKED") 36 | if err := checkArgs(context, 1, exactArgs); err != nil { 37 | return err 38 | } 39 | return startUnikontainer(context) 40 | }, 41 | } 42 | 43 | // We keep it as a separate function, since it is also called from 44 | // the run command 45 | func startUnikontainer(context *cli.Context) error { 46 | // No need to check if containerID is valid, because it will get 47 | // checked later. We just want it for the metrics 48 | containerID := context.Args().First() 49 | metrics.Capture(containerID, "TS11") 50 | 51 | // get Unikontainer data from state.json 52 | unikontainer, err := getUnikontainer(context) 53 | if err != nil { 54 | return err 55 | } 56 | metrics.Capture(containerID, "TS12") 57 | 58 | err = unikontainer.SendStartExecve() 59 | if err != nil { 60 | return err 61 | } 62 | metrics.Capture(containerID, "TS13") 63 | 64 | return unikontainer.ExecuteHooks("Poststart") 65 | } 66 | -------------------------------------------------------------------------------- /cmd/urunc/utils.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2025, Nubificus LTD 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "os" 21 | "os/exec" 22 | "syscall" 23 | 24 | "github.com/nubificus/urunc/pkg/unikontainers" 25 | "github.com/sirupsen/logrus" 26 | "github.com/urfave/cli" 27 | "golang.org/x/sys/unix" 28 | ) 29 | 30 | // Argument check types for the `checkArgs` function. 31 | const ( 32 | exactArgs = iota // Checks for an exact number of arguments. 33 | minArgs // Checks for a minimum number of arguments. 34 | maxArgs // Checks for a maximum number of arguments. 35 | ) 36 | 37 | var ErrEmptyContainerID = errors.New("Container ID can not be empty") 38 | 39 | // checkArgs checks the number of arguments provided in the command-line context 40 | // against the expected number, based on the specified checkType. 41 | func checkArgs(context *cli.Context, expected, checkType int) error { 42 | var err error 43 | cmdName := context.Command.Name 44 | 45 | switch checkType { 46 | case exactArgs: 47 | if context.NArg() != expected { 48 | err = fmt.Errorf("%s: %q requires exactly %d argument(s)", os.Args[0], cmdName, expected) 49 | } 50 | case minArgs: 51 | if context.NArg() < expected { 52 | err = fmt.Errorf("%s: %q requires a minimum of %d argument(s)", os.Args[0], cmdName, expected) 53 | } 54 | case maxArgs: 55 | if context.NArg() > expected { 56 | err = fmt.Errorf("%s: %q requires a maximum of %d argument(s)", os.Args[0], cmdName, expected) 57 | } 58 | } 59 | 60 | if err != nil { 61 | fmt.Printf("Incorrect Usage.\n\n") 62 | _ = cli.ShowCommandHelp(context, cmdName) 63 | return err 64 | } 65 | return nil 66 | } 67 | 68 | func getUnikontainer(context *cli.Context) (*unikontainers.Unikontainer, error) { 69 | containerID := context.Args().First() 70 | if containerID == "" { 71 | return nil, ErrEmptyContainerID 72 | } 73 | 74 | // We have already made sure in main.go that root is not nil 75 | rootDir := context.GlobalString("root") 76 | 77 | // get Unikontainer data from state.json 78 | unikontainer, err := unikontainers.Get(containerID, rootDir) 79 | if err != nil { 80 | if errors.Is(err, unikontainers.ErrNotUnikernel) { 81 | // Exec runc to handle non unikernel containers 82 | // It should never return 83 | err = runcExec() 84 | return nil, err 85 | } 86 | return nil, err 87 | } 88 | 89 | return unikontainer, nil 90 | } 91 | 92 | func runcExec() error { 93 | args := os.Args 94 | binPath, err := exec.LookPath("runc") 95 | if err != nil { 96 | return err 97 | } 98 | args[0] = binPath 99 | return syscall.Exec(args[0], args, os.Environ()) //nolint: gosec 100 | } 101 | 102 | // newSockPair returns a new SOCK_STREAM unix socket pair. 103 | func newSockPair(name string) (parent, child *os.File, err error) { 104 | fds, err := unix.Socketpair(unix.AF_LOCAL, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) 105 | if err != nil { 106 | return nil, nil, err 107 | } 108 | parent = os.NewFile(uintptr(fds[1]), name+"-p") 109 | child = os.NewFile(uintptr(fds[0]), name+"-c") 110 | return parent, child, nil 111 | } 112 | 113 | func logrusToStderr() bool { 114 | l, ok := logrus.StandardLogger().Out.(*os.File) 115 | return ok && l.Fd() == os.Stderr.Fd() 116 | } 117 | 118 | // fatal prints the error's details if it is a libcontainer specific error type 119 | // then exits the program with an exit status of 1. 120 | func fatal(err error) { 121 | fatalWithCode(err, 1) 122 | } 123 | 124 | func fatalWithCode(err error, ret int) { 125 | // Make sure the error is written to the logger. 126 | logrus.Error(err) 127 | if !logrusToStderr() { 128 | fmt.Fprintln(os.Stderr, err) 129 | } 130 | 131 | os.Exit(ret) 132 | } 133 | -------------------------------------------------------------------------------- /deployment/urunc-deploy/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023-2025, Nubificus LTD 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | FROM debian:bullseye AS solo5-builder 16 | 17 | # Remove libc-bin files to avoid segmentation fault. 18 | # See more: https://stackoverflow.com/questions/78105004/docker-build-fails-because-unable-to-install-libc-bins 19 | RUN rm -f /var/lib/dpkg/info/libc-bin.* && \ 20 | apt-get clean && \ 21 | apt-get update && \ 22 | DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ 23 | libc-bin build-essential libseccomp-dev git pkg-config && \ 24 | rm -rf /var/lib/apt/lists/* 25 | 26 | WORKDIR /app 27 | 28 | # Clone Solo5 repository and force static linking 29 | RUN git -c http.sslVerify=false clone --branch v0.9.0 https://github.com/Solo5/solo5.git . && \ 30 | sed -i '66 a\HOSTLDFLAGS += -static-pie' tenders/GNUmakefile 31 | 32 | # Build Solo5 and verify artifacts 33 | RUN ./configure.sh && make || true && \ 34 | test -f tenders/spt/solo5-spt && test -f tenders/hvt/solo5-hvt 35 | 36 | # Copy artifacts to a separate directory 37 | WORKDIR /artifacts 38 | RUN cp /app/tenders/hvt/solo5-hvt /artifacts/ && \ 39 | cp /app/tenders/spt/solo5-spt /artifacts/ 40 | 41 | FROM golang:1.24.1-alpine3.21 AS urunc-builder 42 | RUN apk update && \ 43 | apk add --no-cache git make build-base linux-headers 44 | WORKDIR /app 45 | ARG REPO=urunc-dev/urunc 46 | ARG BRANCH=main 47 | RUN git clone --depth 1 --branch ${BRANCH} https://github.com/${REPO} . && \ 48 | make 49 | WORKDIR /artifacts 50 | RUN cp -r /app/dist . 51 | 52 | FROM alpine:3.21 AS firecracker-builder 53 | RUN apk update && apk add --no-cache curl tar 54 | RUN ARCH=$(uname -m) && \ 55 | VERSION="v1.7.0" && \ 56 | RELEASE_URL="https://github.com/firecracker-microvm/firecracker/releases" && \ 57 | curl -L -o firecracker.tgz ${RELEASE_URL}/download/${VERSION}/firecracker-${VERSION}-${ARCH}.tgz && \ 58 | tar xzf firecracker.tgz && \ 59 | mv release-${VERSION}-${ARCH}/firecracker-${VERSION}-${ARCH} firecracker 60 | WORKDIR /artifacts 61 | RUN cp /firecracker . 62 | 63 | FROM quay.io/kata-containers/kata-deploy:3.15.0 AS qemu-builder 64 | WORKDIR /artifacts 65 | RUN ARCH=$(uname -m) && \ 66 | cp /opt/kata-artifacts/opt/kata/bin/qemu-system-${ARCH} ./qemu-system 67 | 68 | 69 | FROM alpine:3.21 AS intermediate 70 | 71 | COPY --from=solo5-builder /artifacts /urunc-artifacts/hypervisors 72 | COPY --from=qemu-builder /artifacts /urunc-artifacts/hypervisors 73 | COPY --from=qemu-builder /opt/kata-artifacts/opt/kata/share/kata-qemu/qemu /urunc-artifacts/opt/kata/share/kata-qemu/qemu 74 | COPY --from=firecracker-builder /artifacts /urunc-artifacts/hypervisors 75 | COPY --from=urunc-builder /artifacts/dist /urunc-artifacts 76 | 77 | RUN apk update && \ 78 | apk add --no-cache curl && \ 79 | ARCH=$(uname -m) && \ 80 | mv /urunc-artifacts/hypervisors/qemu-system /urunc-artifacts/hypervisors/qemu-system-${ARCH} && \ 81 | if [ "${ARCH}" = "x86_64" ]; then ARCH=amd64; fi && \ 82 | if [ "${ARCH}" = "aarch64" ]; then ARCH=arm64; fi && \ 83 | mv /urunc-artifacts/containerd-shim-urunc-v2_static_${ARCH} /urunc-artifacts/containerd-shim-urunc-v2 && \ 84 | mv /urunc-artifacts/urunc_static_${ARCH} /urunc-artifacts/urunc && \ 85 | KUBECTL_VERSION=$(curl -L -s https://dl.k8s.io/release/stable.txt) && \ 86 | curl -fL --progress-bar -o /usr/bin/kubectl https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${ARCH}/kubectl && \ 87 | chmod +x /usr/bin/kubectl && \ 88 | curl -fL --progress-bar -o /usr/bin/jq https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-${ARCH} && \ 89 | chmod +x /usr/bin/jq 90 | 91 | # Final image 92 | FROM alpine:3.21 93 | COPY --from=intermediate /urunc-artifacts /urunc-artifacts 94 | COPY --from=intermediate /usr/bin/jq /usr/bin/jq 95 | COPY --from=intermediate /usr/bin/kubectl /usr/bin/kubectl 96 | COPY scripts/install.sh /urunc-artifacts/scripts/install.sh 97 | RUN apk update && \ 98 | apk add --no-cache bash curl py3-pip && \ 99 | pip install --no-cache-dir --break-system-packages yq==3.2.3 && \ 100 | chmod +x /urunc-artifacts/scripts/install.sh 101 | -------------------------------------------------------------------------------- /deployment/urunc-deploy/runtimeclasses/runtimeclass.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023-2025, Nubificus LTD 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | kind: RuntimeClass 16 | apiVersion: node.k8s.io/v1 17 | metadata: 18 | name: urunc 19 | handler: urunc 20 | -------------------------------------------------------------------------------- /deployment/urunc-deploy/urunc-cleanup/base/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023-2025, Nubificus LTD 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: kustomize.config.k8s.io/v1beta1 16 | kind: Kustomization 17 | resources: 18 | - urunc-cleanup.yaml -------------------------------------------------------------------------------- /deployment/urunc-deploy/urunc-cleanup/base/urunc-cleanup.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023-2025, Nubificus LTD 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: apps/v1 16 | kind: DaemonSet 17 | metadata: 18 | name: kubelet-urunc-cleanup 19 | namespace: kube-system 20 | spec: 21 | selector: 22 | matchLabels: 23 | name: kubelet-urunc-cleanup 24 | template: 25 | metadata: 26 | labels: 27 | name: kubelet-urunc-cleanup 28 | spec: 29 | serviceAccountName: urunc-deploy-sa 30 | hostPID: true 31 | nodeSelector: 32 | urunc.io/urunc-runtime: cleanup 33 | containers: 34 | - name: kube-urunc-cleanup 35 | image: ghcr.io/urunc-dev/urunc/urunc-deploy:latest 36 | imagePullPolicy: Always 37 | command: ["bash", "-c", "/urunc-artifacts/scripts/install.sh reset"] 38 | env: 39 | - name: NODE_NAME 40 | valueFrom: 41 | fieldRef: 42 | fieldPath: spec.nodeName 43 | securityContext: 44 | privileged: true 45 | updateStrategy: 46 | rollingUpdate: 47 | maxUnavailable: 1 48 | type: RollingUpdate 49 | -------------------------------------------------------------------------------- /deployment/urunc-deploy/urunc-cleanup/overlays/k3s/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023-2025, Nubificus LTD 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: kustomize.config.k8s.io/v1beta1 16 | kind: Kustomization 17 | resources: 18 | - ../../base 19 | patches: 20 | - path: mount_k3s_conf.yaml -------------------------------------------------------------------------------- /deployment/urunc-deploy/urunc-cleanup/overlays/k3s/mount_k3s_conf.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023-2025, Nubificus LTD 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: apps/v1 16 | kind: DaemonSet 17 | metadata: 18 | name: kubelet-urunc-cleanup 19 | namespace: kube-system 20 | spec: 21 | template: 22 | spec: 23 | containers: 24 | - name: kube-urunc-cleanup 25 | volumeMounts: 26 | - name: containerd-conf 27 | mountPath: /etc/containerd/ 28 | volumes: 29 | - name: containerd-conf 30 | hostPath: 31 | path: /var/lib/rancher/k3s/agent/etc/containerd/ -------------------------------------------------------------------------------- /deployment/urunc-deploy/urunc-deploy/base/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023-2025, Nubificus LTD 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: kustomize.config.k8s.io/v1beta1 16 | kind: Kustomization 17 | resources: 18 | - urunc-deploy.yaml -------------------------------------------------------------------------------- /deployment/urunc-deploy/urunc-deploy/base/urunc-deploy.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023-2025, Nubificus LTD 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | --- 16 | apiVersion: apps/v1 17 | kind: DaemonSet 18 | metadata: 19 | name: urunc-deploy 20 | namespace: kube-system 21 | spec: 22 | selector: 23 | matchLabels: 24 | name: urunc-deploy 25 | template: 26 | metadata: 27 | labels: 28 | name: urunc-deploy 29 | spec: 30 | serviceAccountName: urunc-deploy-sa 31 | # terminationGracePeriodSeconds: 60 32 | hostPID: true 33 | containers: 34 | - name: kube-urunc 35 | image: ghcr.io/urunc-dev/urunc/urunc-deploy:latest 36 | imagePullPolicy: Always 37 | lifecycle: 38 | preStop: 39 | exec: 40 | command: ["bash", "-c", "/urunc-artifacts/scripts/install.sh cleanup"] 41 | command: ["bash", "-c", "/urunc-artifacts/scripts/install.sh install"] 42 | env: 43 | - name: NODE_NAME 44 | valueFrom: 45 | fieldRef: 46 | fieldPath: spec.nodeName 47 | - name: HYPERVISORS 48 | value: "firecracker qemu solo5-hvt solo5-spt" 49 | - name: DEBUG 50 | value: "false" 51 | securityContext: 52 | privileged: true 53 | volumeMounts: 54 | - name: crio-conf 55 | mountPath: /etc/crio/ 56 | - name: containerd-conf 57 | mountPath: /etc/containerd/ 58 | - name: host 59 | mountPath: /host/ 60 | volumes: 61 | - name: crio-conf 62 | hostPath: 63 | path: /etc/crio/ 64 | - name: containerd-conf 65 | hostPath: 66 | path: /etc/containerd/ 67 | - name: host 68 | hostPath: 69 | path: / 70 | updateStrategy: 71 | rollingUpdate: 72 | maxUnavailable: 1 73 | type: RollingUpdate 74 | -------------------------------------------------------------------------------- /deployment/urunc-deploy/urunc-deploy/overlays/k3s/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023-2025, Nubificus LTD 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: kustomize.config.k8s.io/v1beta1 16 | kind: Kustomization 17 | resources: 18 | - ../../base 19 | patches: 20 | - path: mount_k3s_conf.yaml -------------------------------------------------------------------------------- /deployment/urunc-deploy/urunc-deploy/overlays/k3s/mount_k3s_conf.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023-2025, Nubificus LTD 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: apps/v1 16 | kind: DaemonSet 17 | metadata: 18 | name: urunc-deploy 19 | namespace: kube-system 20 | spec: 21 | template: 22 | spec: 23 | volumes: 24 | - name: containerd-conf 25 | hostPath: 26 | path: /var/lib/rancher/k3s/agent/etc/containerd/ -------------------------------------------------------------------------------- /deployment/urunc-deploy/urunc-rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023-2025, Nubificus LTD 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: kustomize.config.k8s.io/v1beta1 16 | kind: Kustomization 17 | resources: 18 | - urunc-rbac.yaml -------------------------------------------------------------------------------- /deployment/urunc-deploy/urunc-rbac/urunc-rbac.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023-2025, Nubificus LTD 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | --- 16 | apiVersion: v1 17 | kind: ServiceAccount 18 | metadata: 19 | name: urunc-deploy-sa 20 | namespace: kube-system 21 | --- 22 | kind: ClusterRole 23 | apiVersion: rbac.authorization.k8s.io/v1 24 | metadata: 25 | name: urunc-deploy-role 26 | rules: 27 | - apiGroups: [""] 28 | resources: ["nodes"] 29 | verbs: ["get", "patch"] 30 | - apiGroups: ["node.k8s.io"] 31 | resources: ["runtimeclasses"] 32 | verbs: ["create", "delete", "get", "list", "patch", "update", "watch"] 33 | - apiGroups: ["apps"] 34 | resources: ["daemonsets"] 35 | verbs: ["list"] 36 | --- 37 | kind: ClusterRoleBinding 38 | apiVersion: rbac.authorization.k8s.io/v1 39 | metadata: 40 | name: urunc-deploy-rb 41 | roleRef: 42 | apiGroup: rbac.authorization.k8s.io 43 | kind: ClusterRole 44 | name: urunc-deploy-role 45 | subjects: 46 | - kind: ServiceAccount 47 | name: urunc-deploy-sa 48 | namespace: kube-system -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | urunc.io 2 | -------------------------------------------------------------------------------- /docs/Sample-images.md: -------------------------------------------------------------------------------- 1 | # Sample Unikernel OCI images 2 | 3 | In this document, you can find the images used to perform `urunc`'s end-to-end tests. 4 | This might be helpful for anyone looking to spawn some example unikernels using `urunc`. 5 | 6 | The naming convention used for these images is $APPLICATION-$HYPERVISOR-$UNIKERNEL-$ADDITIONAL_INFO:tag 7 | We plan to create and maintain multi-platform images soon, as well as enrich this list with new images. 8 | 9 | - harbor.nbfc.io/nubificus/urunc/hello-hvt-rumprun-nonet:latest 10 | - harbor.nbfc.io/nubificus/urunc/hello-hvt-rumprun:latest 11 | - harbor.nbfc.io/nubificus/urunc/hello-hvt-mirage:latest 12 | - harbor.nbfc.io/nubificus/urunc/hello-spt-mirage:latest 13 | - harbor.nbfc.io/nubificus/urunc/hello-spt-rumprun-nonet:latest 14 | - harbor.nbfc.io/nubificus/urunc/hello-spt-rumprun:latest 15 | - harbor.nbfc.io/nubificus/urunc/hello-qemu-mewz:latest 16 | - harbor.nbfc.io/nubificus/urunc/hello-qemu-unikraft:latest 17 | - harbor.nbfc.io/nubificus/urunc/hello-world-qemu-linux-initrd:latest 18 | - harbor.nbfc.io/nubificus/urunc/hello-firecracker-unikraft:latest 19 | - harbor.nbfc.io/nubificus/urunc/hello-world-firecracker-linux-initrd:latest 20 | - harbor.nbfc.io/nubificus/urunc/hello-env-qemu-unikraft-initrd:latest 21 | - harbor.nbfc.io/nubificus/urunc/hello-env-qemu-linux-initrd:latest 22 | - harbor.nbfc.io/nubificus/urunc/hello-env-firecracker-unikraft-initrd:latest 23 | - harbor.nbfc.io/nubificus/urunc/hello-env-firecracker-linux-initrd:latest 24 | - harbor.nbfc.io/nubificus/urunc/nginx-qemu-unikraft-initrd:latest 25 | - harbor.nbfc.io/nubificus/urunc/nginx-qemu-linux:latest 26 | - harbor.nbfc.io/nubificus/urunc/nginx-qemu-linux-block:latest 27 | - harbor.nbfc.io/nubificus/urunc/nginx-hvt-rumprun-block:latest 28 | - harbor.nbfc.io/nubificus/urunc/nginx-spt-rumprun-block:latest 29 | - harbor.nbfc.io/nubificus/urunc/nginx-firecracker-unikraft-initrd:latest 30 | - harbor.nbfc.io/nubificus/urunc/nginx-firecracker-linux:latest 31 | - harbor.nbfc.io/nubificus/urunc/nginx-firecracker-linux-block:latest 32 | - harbor.nbfc.io/nubificus/urunc/hello-server-qemu-mewz:latest 33 | - harbor.nbfc.io/nubificus/urunc/httpreply-firecracker-unikraft:latest 34 | - harbor.nbfc.io/nubificus/urunc/redis-hvt-rumprun:latest 35 | - harbor.nbfc.io/nubificus/urunc/redis-spt-rumprun:latest 36 | - harbor.nbfc.io/nubificus/urunc/redis-hvt-rumprun-block:latest 37 | - harbor.nbfc.io/nubificus/urunc/redis-spt-rumprun-block:latest 38 | - harbor.nbfc.io/nubificus/urunc/redis-qemu-linux:latest 39 | - harbor.nbfc.io/nubificus/urunc/redis-qemu-unikraft-initrd:latest 40 | - harbor.nbfc.io/nubificus/urunc/redis-qemu-linux-block:latest 41 | - harbor.nbfc.io/nubificus/urunc/redis-firecracker-linux:latest 42 | - harbor.nbfc.io/nubificus/urunc/redis-firecracker-linux-block:latest 43 | - harbor.nbfc.io/nubificus/urunc/net-hvt-mirage:latest 44 | - harbor.nbfc.io/nubificus/urunc/net-spt-mirage:latest 45 | - harbor.nbfc.io/nubificus/urunc/net-qemu-mirage:latest 46 | - harbor.nbfc.io/nubificus/urunc/block-test-hvt-mirage:latest 47 | - harbor.nbfc.io/nubificus/urunc/block-test-spt-mirage:latest 48 | - harbor.nbfc.io/nubificus/urunc/whoami-qemu-linux-initrd:latest 49 | - harbor.nbfc.io/nubificus/urunc/whoami-firecracker-linux-initrd:latest 50 | -------------------------------------------------------------------------------- /docs/assets/images/cncf-color.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/assets/images/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/urunc-dev/urunc/124fee5f8f8c845bc5e2897c7597c0b05ce355e7/docs/assets/images/favicon-32x32.png -------------------------------------------------------------------------------- /docs/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/urunc-dev/urunc/124fee5f8f8c845bc5e2897c7597c0b05ce355e7/docs/assets/images/favicon.ico -------------------------------------------------------------------------------- /docs/assets/images/urunc-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/urunc-dev/urunc/124fee5f8f8c845bc5e2897c7597c0b05ce355e7/docs/assets/images/urunc-logo.png -------------------------------------------------------------------------------- /docs/assets/images/urunc-nerdctl-example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/urunc-dev/urunc/124fee5f8f8c845bc5e2897c7597c0b05ce355e7/docs/assets/images/urunc-nerdctl-example.gif -------------------------------------------------------------------------------- /docs/assets/javascripts/console-copy.js: -------------------------------------------------------------------------------- 1 | /* global document$:readonly */ 2 | document$.subscribe(() => { 3 | document.querySelectorAll("pre > code").forEach((code) => { 4 | const wrapper = code.closest("div.language-console"); 5 | 6 | if (!wrapper) return; 7 | 8 | const button = wrapper.querySelector("button.md-clipboard"); 9 | if (!button) return; 10 | 11 | button.addEventListener( 12 | "mouseenter", 13 | () => { 14 | // Only set data-copy once 15 | if (code.hasAttribute("data-copy")) return; 16 | 17 | const text = code.textContent.trimEnd(); 18 | 19 | // Merge continuation lines that end with '\', and remove '$' from prompts 20 | let lines = []; 21 | let mergeNext = false; 22 | 23 | text.split("\n").forEach((line) => { 24 | // If we need to merge the current line with the next one 25 | if (mergeNext) { 26 | // Merge the line with the previous one and reset the flag 27 | lines[lines.length - 1] += " " + line.trimStart(); 28 | if (lines[lines.length - 1].endsWith(" \\")) { 29 | lines[lines.length - 1] = lines[lines.length - 1].slice(0, -2); 30 | } else if (lines[lines.length - 1].endsWith("\\")) { 31 | lines[lines.length - 1] = lines[lines.length - 1].slice(0, -1); 32 | } else { 33 | mergeNext = false; 34 | } 35 | } 36 | 37 | // If the line starts with '$' and ends with '\' (continuation line) 38 | if (line.startsWith("$ ")) { 39 | if (line.endsWith(" \\")) { 40 | // Remove the '$' and backslash, and mark that we need to merge with the next line 41 | lines.push(line.slice(2, -2)); // Remove "$ " and the trailing "\" 42 | mergeNext = true; 43 | } else if (line.endsWith("\\")) { 44 | lines.push(line.slice(2, -1)); // Remove "$ " and the trailing " \" 45 | mergeNext = true; 46 | } else { 47 | // If it's a prompt line without continuation, just remove the '$' 48 | lines.push(line.slice(2)); 49 | } 50 | } 51 | }); 52 | 53 | const cleaned = lines.join("\n"); 54 | 55 | code.setAttribute("data-copy", cleaned); 56 | }, 57 | { once: true }, 58 | ); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /docs/assets/stylesheets/theme.css: -------------------------------------------------------------------------------- 1 | /* 2 | * urunc Custom Theme Styling for MkDocs Material 3 | */ 4 | 5 | /* ======================= 6 | Light Theme: urunc 7 | ======================= */ 8 | [data-md-color-scheme="urunc"] { 9 | --md-primary-fg-color: #FAFAFF; 10 | --md-primary-fg-color--light: #001524; 11 | --md-primary-fg-color--dark: #001524; 12 | --md-primary-bg-color: #001524; 13 | --md-primary-bg-color--light: #001524; 14 | --md-primary-bg-color--dark: #001524; 15 | 16 | --md-accent-fg-color: #FF7D00; 17 | --md-accent-fg-color--transparent: hsla(32, 100%, 55%, 0.1); 18 | 19 | --md-footer-logo-dark-mode: none; 20 | --md-footer-logo-light-mode: block; 21 | } 22 | 23 | /* ======================= 24 | Dark Theme: Slate 25 | ======================= */ 26 | [data-md-color-scheme="slate"] { 27 | --md-primary-fg-color: #001524; 28 | --md-primary-fg-color--light: #841a6b; 29 | --md-primary-fg-color--dark: #001524; 30 | --md-primary-bg-color: #fff3f0; 31 | 32 | --md-accent-fg-color: #FF7D00; 33 | --md-accent-fg-color--transparent: hsla(32, 100%, 55%, 0.1); 34 | 35 | --md-footer-logo-dark-mode: block; 36 | --md-footer-logo-light-mode: none; 37 | } 38 | 39 | /* ======================= 40 | Logo Sizing 41 | ======================= */ 42 | .md-nav__title .md-nav__button.md-logo img { 43 | height: 2rem; 44 | } 45 | 46 | #logo_light_mode { 47 | display: var(--md-footer-logo-light-mode); 48 | } 49 | 50 | #logo_dark_mode { 51 | display: var(--md-footer-logo-dark-mode); 52 | } 53 | 54 | /* ======================= 55 | Breadcrumb Styling 56 | ======================= */ 57 | :root { 58 | --md-path-icon: url('data:image/svg+xml;charset=utf-8,'); 59 | } 60 | 61 | .md-path { 62 | font-size: 0.75rem; 63 | margin: 0 0.8rem; 64 | overflow: auto; 65 | padding-top: 1.2rem; 66 | } 67 | 68 | .md-path:not([hidden]) { 69 | display: block; 70 | } 71 | 72 | @media screen and (min-width: 76.25em) { 73 | .md-path { 74 | margin: 0 1.2rem; 75 | } 76 | } 77 | 78 | .md-path__list { 79 | display: flex; 80 | align-items: center; 81 | gap: 0.25rem; 82 | list-style: none; 83 | margin: 0; 84 | padding: 0; 85 | } 86 | 87 | .md-path__item:not(:first-child) { 88 | display: inline-flex; 89 | gap: 0.25rem; 90 | white-space: nowrap; 91 | } 92 | 93 | .md-path__item:not(:first-child)::before { 94 | content: ""; 95 | width: 0.8rem; 96 | height: 0.8rem; 97 | display: inline; 98 | background-color: var(--md-default-fg-color--lighter); 99 | mask-image: var(--md-path-icon); 100 | -webkit-mask-image: var(--md-path-icon); 101 | } 102 | 103 | .md-path__link { 104 | display: flex; 105 | align-items: center; 106 | color: var(--md-default-fg-color--light); 107 | } 108 | 109 | .md-path__link:hover, 110 | .md-path__link:focus { 111 | color: var(--md-accent-fg-color); 112 | } 113 | 114 | /* ======================= 115 | Permalink Anchor Styling 116 | ======================= */ 117 | .md-typeset .headerlink { 118 | font-size: 1rem; 119 | display: inline-block; 120 | vertical-align: middle; 121 | } 122 | 123 | /* ======================= 124 | Code Block Styling 125 | ======================= */ 126 | .language-console span.gp { 127 | user-select: none; 128 | -webkit-user-select: none; 129 | pointer-events: none; 130 | } 131 | 132 | /* ======================= 133 | General Typography (Optional) 134 | ======================= */ 135 | .md-typeset h1 { 136 | font-size: 2rem; 137 | border-bottom: 2px solid var(--md-accent-fg-color--transparent); 138 | padding-bottom: 0.25em; 139 | } 140 | 141 | .md-typeset h2 { 142 | font-size: 1.5rem; 143 | margin-top: 2em; 144 | } 145 | 146 | .md-typeset code, 147 | .md-typeset pre { 148 | font-family: 'JetBrains Mono', 'Fira Code', monospace; 149 | } 150 | 151 | /* Fix active link color in dark mode sidebar */ 152 | [data-md-color-scheme="slate"] .md-nav__link--active { 153 | color: var(--md-primary-bg-color) !important; 154 | font-weight: bold; 155 | } 156 | 157 | [data-md-color-scheme="slate"] .md-nav__link:hover, 158 | [data-md-color-scheme="slate"] .md-nav__link:focus { 159 | color: var(--md-primary-bg-color); 160 | } 161 | 162 | /* Improve contrast for all links in dark mode */ 163 | [data-md-color-scheme="slate"] a { 164 | color: var(--md-primary-bg-color); 165 | } 166 | 167 | /* Optional: Slight hover effect for links */ 168 | [data-md-color-scheme="slate"] a:hover { 169 | text-decoration: underline; 170 | } 171 | 172 | [data-md-color-scheme="urunc"] img[src$="#only-dark"], 173 | [data-md-color-scheme="urunc"] img[src$="#gh-dark-mode-only"] { 174 | display: none; /* Hide dark images in light mode */ 175 | } 176 | 177 | [data-md-color-scheme="slate"] img[src$="#only-light"], 178 | [data-md-color-scheme="slate"] img[src$="#gh-light-mode-only"] { 179 | display: none; /* Hide light images in dark mode */ 180 | } 181 | 182 | -------------------------------------------------------------------------------- /docs/design/index.md: -------------------------------------------------------------------------------- 1 | This section describes the high-level architecture of `urunc`, along with the 2 | design choices and limitations. 3 | 4 | ## Overview 5 | 6 | `urunc` is a container runtime designed to bridge the gap between traditional 7 | unikernels and containerized environments. It enables seamless integration with 8 | cloud-native architectures by leveraging familiar OCI (Open Container 9 | Initiative) tools and methodologies. By acting as a unikernel container runtime 10 | compatible with the Container Runtime Interface (CRI), `urunc` allows unikernels 11 | to be managed like containers, opening up possibilities for lightweight, 12 | secure, and high-performance application deployment. 13 | 14 | In `urunc`, the user code runs inside a unikernel on top of a Virtual Machine 15 | Monitor (VMM) or a sandbox monitor. As a result, `urunc` guarantees strong 16 | isolation among the containers and inherits the enhanced security features of 17 | unikernels, such as their small attack surface. 18 | 19 | In the unikernel context a single-process application runs directly on top of a 20 | Virtual Machine (VM) or a sandbox. At the same time, in the VM context, every 21 | VM runs as a process. Subsequently, `urunc` combines these two characteristics 22 | and treats the VM's process, which executes the unikernel that runs the 23 | application, as the container's process. This way, `urunc` does not reuire any 24 | auxiliary process running alongside the unikernel, maintaining as less overhead 25 | as possible. Instead `urunc` directly manages the application running in the 26 | unikernel through the VMM or the sandbox monitor. Moreover, `urunc` does not 27 | require any modifications in the unikernel framework and hence all unikernel 28 | frameworks and similar technologies can easily integrate with `urunc`. 29 | 30 | ## Execution flow 31 | 32 | The process of starting a new unikernel container with `urunc`, starts at the 33 | higher-level runtime (`containerd`) level: 34 | 35 | - `Containerd` unpacks the image into a supported snapshotter (e.g. `devmapper`) 36 | and invokes `urunc`, as any other OCI runtime. 37 | - `urunc` parses the image's rootfs and annotations, initiating the required 38 | setup procedures. In particular, it creates essential pipes for stdio, it 39 | creates the container's state file and runs the `prestart` hooks (if any). 40 | - Subsequently, `urunc` spawns a new process within a distinct network 41 | namespace, stores its PID and invokes the `createRuntime` and 42 | `createContainer` hooks. 43 | - When `Containerd` starts the container `urunc` configures any required 44 | resources such as block devices or network interfaces and runs the 45 | `statContainer` hooks. 46 | - Depending on the specified unikernel type and annotations, `urunc` selects the 47 | appropriate VMM or sandbox monitor (e.g. Qemu, Solo5-spt) and boots the 48 | unikernel. The unikernel runs inside its own isolated environment, interacting 49 | with external systems through the namespaces and devices configured by 50 | `urunc`. 51 | - Finally the unikernel is up and running as a container, and we can manage its 52 | lifecycle like any other container through `urunc` (e.g., stopping, 53 | restarting, or deleting the container). 54 | 55 | ## Image Format and Annotations 56 | 57 | To support unikernels in a containerized environment, `urunc` requires specific 58 | metadata embedded in OCI container images. These images must include the 59 | unikernel binary, configuration and any other files required from the application 60 | or the unikernel and the aforementioned metadata which dictate how the unikernel 61 | should be run. The metadata can be passed to `urunc` either in the form of 62 | [annotations](https://github.com/opencontainers/runtime-spec/blob/main/config.md#annotations) 63 | or as a specific file in the container's rootfs. For a detailed explanation and 64 | an up-to-date list of the currently supported annotations take alook at the 65 | [packaging unikernels page](../package/#annotations). 66 | 67 | Although `urunc`-formatted unikernel images are not designed to be executed by 68 | other container runtimes, they can still be stored and distributed via generic 69 | container registries, such as Docker Hub or Harbor. This ensures compatibility 70 | with standard cloud-native workflows for building, shipping, and deploying 71 | applications. 72 | -------------------------------------------------------------------------------- /docs/design/seccomp.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: "Seccomp" 4 | description: "Seccomp in urunc" 5 | --- 6 | 7 | # Seccomp in Urunc 8 | 9 | ## Overview 10 | 11 | Seccomp (Secure Computing Mode) is a Linux kernel security feature that 12 | restricts the system calls a process can make, limiting the kernel exposure 13 | to the processes. Container runtimes make use of this mechanism to 14 | further limit a container and enhance overall security. 15 | 16 | ## How Seccomp is used in 'urunc' 17 | 18 | In 'urunc' the application does not execute directly in the host kernel. Instead, 19 | 'urunc' makes use of either a VMM (Virtual Machine Monitor) or the `solo5-spt` 20 | tender to execute the application inside a unikernel. As a result, in contrast 21 | with other container runtimes, in 'urunc' the applications do not share the same 22 | kernel. 23 | 24 | Thus, a malicious user must take control of the guest kernel and escape to the 25 | VMM before attacking the host. To further limit the exposure of 26 | the host kernel to the VMM, 'urunc' uses seccomp filters for each 27 | supported VMM. In particular, in the case of: 28 | - Firecracker, 'urunc' does not have to do anything more, since Firecracker by 29 | default makes uses seccomp filters. 30 | - Qemu, 'urunc' makes use of Qemu's sandbox command line options to activate 31 | all possible seccomp filters in Qemu. 32 | - Solo5-hvt, 'urunc' applies the seccomp filters before executing 33 | 'Solo5-hvt'. 34 | - Solo5-spt, 'urunc' can not do anything since solo5-spt makes use of seccomp by 35 | itself. 36 | 37 | ## Caveats of using seccomp in 'urunc' 38 | 39 | Since 'urunc', in most cases, makes use of the VMM's mechanisms to enforce the 40 | seccomp filters, 'urunc' heavily relies on the VMM to properly restrict the system 41 | calls the VMM can use. 42 | 43 | In the case of 'Solo5-hvt', since 'urunc' is responsible for applying the seccomp 44 | filters, proper identification of the required system calls is necessary. 45 | Unfortunately, due to dynamic linking and Go's runtime, it is 46 | impossible to always predict correctly for every system the necessary system 47 | calls for 'Solo5-hvt' execution. 48 | 49 | Nevertheless, 'Solo5-hvt' with seccomp in 'urunc' has been tested in Ubuntu 20.04 50 | and Ubuntu 22.04. Using 'urunc' and solo5-hvt on different platforms might result 51 | in failed execution. For that reason, we strongly recommend running the seccomp 52 | test first, by `make test_nerdctl_Seccomp`. In case the test fails, the seccomp 53 | profile for 'Solo5-hvt' needs to get updated. 54 | 55 | For that reason, we created a toolset to identify the required system calls. 56 | The toolset, along with instructions on how to use it, can be found in [goscall 57 | repository](https://github.com/nubificus/goscall). 58 | 59 | ## Setting a seccomp profile 60 | 61 | Due to its design, 'urunc' does not allow the definition of a seccomp profile other 62 | than the default. However, users can totally disable seccomp by using 63 | the `--security-opt seccomp=unconfined` command line option. In that scenario, 64 | 'urunc' will not make use of any seccomp filters in all the supported VMMs, except 65 | of 'Solo5-spt'. 66 | -------------------------------------------------------------------------------- /docs/developer-guide/Code-of-Conduct.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: "Code of Conduct" 4 | description: "Code of Conduct" 5 | --- 6 | 7 | All maintainers and communicty members of `urunc` must abide the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md). 8 | 9 | -------------------------------------------------------------------------------- /docs/developer-guide/development.md: -------------------------------------------------------------------------------- 1 | title: Setup a Dev environment 2 | ------ 3 | 4 | Most of the steps are covered in the [installation](../../installation) document. 5 | Please refer to it for: 6 | 7 | - installing a recent version of Go (e.g. 1.24) 8 | - installing `containerd` and `runc` 9 | - setting up the devmapper snapshotter 10 | - installing `nerdctl` and the `CNI` plugins 11 | - installing the relevant hypervisors 12 | 13 | In addition to the above, we strongly suggest to install 14 | [crictl](https://github.com/kubernetes-sigs/cri-tools/tree/master) which `urunc` 15 | uses for its end-to-end tests. The following commands will install `crictl` 16 | 17 | ```bash 18 | $ VERSION="v1.30.0" # check latest version in /releases page 19 | $ wget https://github.com/kubernetes-sigs/cri-tools/releases/download/$VERSION/crictl-$VERSION-linux-amd64.tar.gz 20 | $ sudo tar zxvf crictl-$VERSION-linux-amd64.tar.gz -C /usr/local/bin 21 | $ rm -f crictl-$VERSION-linux-amd64.tar.gz 22 | ``` 23 | 24 | Since default endpoints for `crictl` are now deprecated, we need to set them up: 25 | 26 | ``` 27 | $ sudo tee -a /etc/crictl.yaml > /dev/null <<'EOT' 28 | runtime-endpoint: unix:///run/containerd/containerd.sock 29 | image-endpoint: unix:///run/containerd/containerd.sock 30 | timeout: 20 31 | EOT 32 | ``` 33 | 34 | The next step is to clone and build `urunc`: 35 | 36 | ```bash 37 | $ git clone https://github.com/nubificus/urunc.git 38 | $ cd urunc 39 | $ make && sudo make install 40 | ``` 41 | 42 | At last, please validate that the dev environment has been set correctly 43 | by running the: 44 | 45 | - unit tests: `make unittest` and 46 | 47 | - end-to-end tests: `sudo make e2etest` 48 | 49 | > Note: When running `make` commands for `urunc` that will use go (i.e. build, 50 | > unitest, e2etest) you might need to specify the path to the go binary 51 | with `sudo GO=$(which go) make`. 52 | -------------------------------------------------------------------------------- /docs/developer-guide/index.md: -------------------------------------------------------------------------------- 1 | In this section we provide useful information regarding the development of 2 | `urunc`. In particular, the section contains information about setting up a dev 3 | environment, checking performance traces and more. 4 | 5 | We kindly ask you to check the [contributing](../developer-guide/contribute) page before 6 | submitting any pull requests, or opening new issues. 7 | -------------------------------------------------------------------------------- /docs/developer-guide/maintainers.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: "Maintainers" 4 | description: "The list of urunc maintainers" 5 | --- 6 | 7 | # The current maintainers of `urunc` 8 | 9 | | Name | GitHub Username | Email | Responsibility | 10 | | ---------- | --------------- | ------- | --------------------------------- | 11 | | Charalampos Mainas | [@cmainas](https://github.com/cmainas) | cmainas@nubificus.co.uk | Core Maintainer | 12 | | Georgios Ntoutsos | [@gntouts](https://github.com/gntouts) | gntouts@nubificus.co.uk | Core Maintainer | 13 | | Anastassios Nanos | [@ananos](https://github.com/ananos) | ananos@nubificus.co.uk | Core Maintainer | 14 | -------------------------------------------------------------------------------- /docs/developer-guide/security.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: "Security Policy" 4 | description: "Security and Vulnerability Reporting" 5 | --- 6 | 7 | Security is one of the main goals of `urunc`. If you discover a security issue, 8 | we ask that you report it promptly and privately to the maintainers. This page 9 | provides information on when and how to report a vulnerability, along with the 10 | description of the process of handling such reports. 11 | 12 | ## Security disclosure policy 13 | 14 | The `urunc` project follows a [responsible disclosure 15 | model](https://en.wikipedia.org/wiki/Coordinated_vulnerability_disclosure). 16 | All vulnerability reports are reviewed by the `urunc` maintainers. If 17 | necessary, the report may be shared with other trusted contributors to aid in 18 | finding a proper fix. 19 | 20 | ## Reporting Vulnerabilities 21 | 22 | Please do not open public issues or PRs to report a vulnerability. Instead, use 23 | the private vulnerability reporting of the `urunc`'s Github repository. In 24 | particular, in `urunc`'s repository page, navigate to the [Security 25 | tab](https://github.com/nubificus/urunc/security), click 26 | [`Advisories`](https://github.com/nubificus/urunc/security/advisories) and then 27 | [`Report a 28 | vulnerability`](https://github.com/nubificus/urunc/security/advisories/new). 29 | Alternatively, the report can be filed via email at `security@urunc.io`. This 30 | address delivers your message securely to all maintainers. 31 | 32 | ### Vulnerability handling process 33 | 34 | Upon the receival of a vulnerability report, the following process will take place: 35 | 36 | - the `urunc` maintainers will acknowledge and analyze the report within 48 37 | hours 38 | - A timeline (embargo period) will be agreed upon with the reporter(s) to keep 39 | the vulnerability confidential until a fix is ready 40 | - The maintainers will prioritize and begin addressing the issue. They may 41 | request additional details or involve trusted contributors to help resolve 42 | the problem securely 43 | - Reporters are encouraged to participate in solution design or testing. The 44 | maintainers will keep them updated throughout the process 45 | - At the end of the timeline: a) a proper fix will be merged, b) a new (patched) 46 | version of `urunc` will get released and c) a public advisory will get published, 47 | giving credits to the reporter(s), unless they prefer to remain anonymous 48 | 49 | ### What to include in the report 50 | 51 | To help the maintainers assess and resolve the issue efficiently, 52 | please use the following template: 53 | 54 | ``` 55 | ## Title 56 | _Short title describing the problem._ 57 | 58 | ## Description 59 | 60 | ### Summary 61 | _Short summary of the problem. Make the impact and severity as clear as possible. For example: Supplementary groups are not set up properly inside a container._ 62 | 63 | ### Details 64 | _Give all details on the vulnerability. Pointing to the incriminated source code is very helpful for the maintainer._ 65 | 66 | ### PoC 67 | _Complete instructions, including specific configuration details, to reproduce the vulnerability._ 68 | 69 | ### Impact 70 | _What kind of vulnerability is it? Who is impacted?_ 71 | 72 | ## Affected Products 73 | 74 | ### Ecosystem 75 | _Should be something related to Go, C, Github Actions etc._ 76 | 77 | ### Package Name 78 | _eg. github.com/nubificus/urunc_ 79 | 80 | ### Affected Versions 81 | _eg. < 0.5.0_ 82 | 83 | ### Patched Versions 84 | _eg. 0.5.1 85 | 86 | ### Severity 87 | _eg. Low, Critical etc._ 88 | 89 | ``` 90 | 91 | Also, please use one report per vulnerability and try to keep in touch in 92 | case the `urunc` maintainers require more information. 93 | 94 | ### Scope clarification 95 | 96 | As a sandboxed container runtime, `urunc` makes use of VM or software based 97 | monitors to spawn workloads. Therefore, before submitting a report, please 98 | ensure the issue lies within `urunc` itself and not in the guest (uni)kernel or 99 | the monitor. If the vulnerability is in those components, kindly report it to 100 | their respective teams. However, if urunc uses those components in a way that 101 | introduces a security issue, please report it to the urunc maintainers. 102 | -------------------------------------------------------------------------------- /docs/hooks/copyright.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | from datetime import datetime 4 | 5 | 6 | def on_config(config, **kwargs): 7 | author = config.get("site_author", "Author") 8 | year = datetime.now().year 9 | start_year = config.get("copyright", year) 10 | if start_year != year: 11 | config["copyright"] = f"Copyright © {start_year} - {year} {author}" 12 | else: 13 | config["copyright"] = f"Copyright © {year} {author}" 14 | return config 15 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # urunc: A Lightweight Container Runtime for Unikernels 2 | 3 | The main goal of `urunc` is to bridge the gap between traditional unikernels and 4 | containerized environments, enabling seamless integration with cloud-native 5 | architectures. Designed to fully leverage the container semantics and benefits 6 | from the OCI tools and methodology, `urunc` aims to become 7 | “runc for unikernels”, while offering compatibility with the Container 8 | Runtime Interface (CRI). Unikernels are packaged inside OCI-compatible images 9 | and `urunc` launches the unikernel on top of the underlying Virtual Machine or 10 | seccomp monitors. Thus, developers and administrators can package, deliver, 11 | deploy and manage unikernels using familiar cloud-native practises. 12 | 13 | For the above purpose `urunc` acts as any other OCI runtime. The main 14 | difference of `urunc` with other container runtimes is that instead of 15 | spawning a simple process, it uses a Virtual Machine Monitor (VMM) or a sandbox 16 | monitor to run the unikernel. It is important to note that `urunc` does not 17 | require any particular software running alongise the user's application inside 18 | or outside the unikernel. As a result, `urunc` is able to support any unikernel 19 | framework or similar technologies, while maintaining as low overhead as 20 | possible. 21 | 22 | ## Key features 23 | 24 | - OCI Compatibility: Compatible with the Open Container Initiative (OCI) standards, enabling the use of existing container tools and workflows. 25 | - Container Runtime Interface (CRI) Support: Compatible with Kubernetes and other CRI-based systems for seamless integration into container orchestration platforms. 26 | - Unikernel Support: Run applications and user code as unikernels, unlocking the performance and security advantages of unikernel technology. 27 | - Integration with VMMs and other strong sandboxing mechanisms: Use lightweight VMMs or sandbox monitors to launch unikernels, facilitating efficient resource isolation and management. 28 | - Un-opinionated and Extensible: Straightforward and easy integration of new unikernel frameworks and sandboxing mechanisms without any porting overhead. 29 | 30 | ## Use cases 31 | 32 | Unikernels are well known as a good fit for a variety of use cases, such as: 33 | 34 | - Microservices: The lightweight and almost deminished *OS noise* of unikernels 35 | can significantly improve the execution of applications, making unikernels an 36 | attractive fit for microservices. 37 | - Serverless and FaaS: The extremely fast instantiation time of unikernels 38 | satisfies the event-driven, short-lived and scalable characteristics of 39 | serverless computing 40 | - Edge computing: The lightweight notion of unikernels suits very well with edge 41 | devices, where resources constraints and performance are critical. 42 | - Sensitive environments: The inherited strong VM-based isolation, along with 43 | the minimized attack surface of unikernels, provide strong security guarantees 44 | for sensitive applications which demand high security standards. 45 | 46 | In all the above use cases, `urunc` facilitates the seamless integration of 47 | unikernels with existing cloud-native tools and technologies, enabling the effortless 48 | distribution and management of applications running as unikernels. 49 | 50 | ## Current support of unikernels and VM/Sandbox monitors 51 | 52 | The following table provides an overview of the currently supported VMMs and 53 | Sandbox monitors, along with the unikernels that can run on top of them. 54 | 55 | 56 | | Unikernel | VM/Sandbox Monitor | Arch | Storage | 57 | |---------------------------------------- |--------------------- |------------- |----------- | 58 | | [Rumprun](./unikernel-support#rumprun) | [Solo5-hvt](./hypervisor-support#solo5-hvt), [Solo5-spt](./hypervisor-support#solo5-spt) | x86, aarch64 | Block/Devmapper | 59 | | [Unikraft](./unikernel-support#unikraft)| [Qemu](./hypervisor-support#qemu), [Firecracker](./hypervisor-support#aws-firecracker) | x86 | Initrd | 60 | | [MirageOS](./unikernel-support#mirage)| [Qemu](./hypervisor-support#qemu), [Solo5-hvt](./hypervisor-support#solo5-hvt), [Solo5-spt](./hypervisor-support#solo5-spt) | x86, aarch64 | Block/Devmapper | 61 | | [Mewz](./unikernel-support#mewz)| [Qemu](./hypervisor-support#qemu) | x86 | In-memory | 62 | | [Linux](./unikernel-support#linux)| [Qemu](./hypervisor-support#qemu), [Firecracker](./hypervisor-support#aws-firecracker) | x86, aarch64 | Initrd, Block/Devmapper | 63 | 64 | 65 | 66 | 67 | ## Quick links 68 | 69 | - [Contributing](developer-guide/contribute/) 70 | - [Getting metrics from `urunc`](developer-guide/timestamps) 71 | - [Integration with k8s](tutorials/How-to-urunc-on-k8s/) 72 | 73 |
74 | 75 |

76 | urunc is a Cloud Native Computing Foundation sandbox project. 77 |

78 | 79 |

80 | 81 | 82 |

83 | -------------------------------------------------------------------------------- /docs/overrides/main.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% if page.ancestors %} 4 | 10 | {% endif %} 11 | 12 | {% block outdated %} 13 | You're not viewing the latest version. 14 | 15 | Click here to go to latest. 16 | 17 | {% endblock %} 18 | 19 | {% block container %} 20 |
21 | {% include "partials/breadcrumb.html" %} 22 |
23 | {% block content %} 24 | {% include "partials/content.html" %} 25 | {% endblock %} 26 |
27 |
28 | {% endblock %} 29 | -------------------------------------------------------------------------------- /docs/overrides/partials/breadcrumb.html: -------------------------------------------------------------------------------- 1 | {% macro render_breadcrumbs(nav_items, path=[]) %} 2 | {% for item in nav_items %} 3 | {% set new_path = path + [item] %} 4 | {% if item.children %} 5 | {{ render_breadcrumbs(item.children, new_path) }} 6 | {% elif item.url == page.url %} 7 | {% if new_path | length > 1 %} 8 | 20 | {% endif %} 21 | {% endif %} 22 | {% endfor %} 23 | {% endmacro %} 24 | 25 | {{ render_breadcrumbs(nav) }} 26 | -------------------------------------------------------------------------------- /docs/overrides/partials/copyright.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/overrides/partials/logo.html: -------------------------------------------------------------------------------- 1 | logo 2 | logo 3 | -------------------------------------------------------------------------------- /docs/package/pre-built.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: "Pre-built unikernels" 4 | description: "Packaging pre-built unikernels" 5 | --- 6 | 7 | # Packaging pre-built unikernels for `urunc` 8 | 9 | In this page we will explain the process of packaging an existing / pre-built 10 | unikernel as an OCI image with the necessary annotations for `urunc`. As an 11 | example, we will use a network example over 12 | [MirageOS](https://github.com/mirage) from 13 | [mirage-skeleton](https://github.com/mirage/mirage-skeleton/tree/main/device-usage/network) 14 | targeting [Solo5-hvt](https://github.com/Solo5/solo5). 15 | 16 | For simply packaging pre-built unikernel images, we can use both 17 | [bunny](https://github.com/nubificus/bunny) and 18 | [bunix](https://github.com/nubificus/bunix). 19 | 20 | > **NOTE**: The below steps can be easily adjusted to any pre-built unikernel image. 21 | 22 | ## Using `bunny` 23 | 24 | In the case of [bunny](https://github.com/nubificus/bunny) and pre-built 25 | unikernel images, we can use both supported file syntaxes: a) `bunnyfile` and 26 | b) the Dockerfile-like syntax. 27 | 28 | ### Using a `bunnyfile` 29 | 30 | In order to package an existing pre-built unikernel image with [bunny](https://github.com/nubificus/bunny) and a 31 | `bunnyfile` we can define the `bunnyfile` as: 32 | 33 | ``` 34 | #syntax=harbor.nbfc.io/nubificus/bunny:latest 35 | version: v0.1 36 | 37 | platforms: 38 | framework: mirage 39 | monitor: hvt 40 | architecture: x86 41 | 42 | kernel: 43 | from: local 44 | path: network.hvt 45 | 46 | cmdline: "" 47 | ``` 48 | 49 | In the above file we specify the following: 50 | 51 | - We want to package a [MirageOS](https://github.com/mirage) unikernel that 52 | will execute on top of [Solo5-hvt](https://github.com/Solo5/solo5) over x86 53 | architecture. 54 | - We want to use the `network.hvt` binary as the unikernel to boot. 55 | - We do not specify any command line, since the unikernel does not necessarily require one. 56 | 57 | We can build the OCI image with the following command: 58 | 59 | ``` 60 | docker build -f bunnyfile -t urunc/prebuilt/network-mirage-hvt:test . 61 | ``` 62 | 63 | ### Using a Dockerfile-like syntax 64 | 65 | In order to package an existing pre-built unikernel image with 66 | [bunny](https://github.com/nubificus/bunny) and a Dockerfile-like syntax file, 67 | we can define the `Containerfile` as: 68 | 69 | ``` 70 | #syntax=harbor.nbfc.io/nubificus/bunny:latest 71 | FROM scratch 72 | 73 | COPY network.hvt /unikernel/network.hvt 74 | 75 | LABEL com.urunc.unikernel.binary=/unikernel/network.hvt 76 | LABEL "com.urunc.unikernel.cmdline"="" 77 | LABEL "com.urunc.unikernel.unikernelType"="mirage" 78 | LABEL "com.urunc.unikernel.hypervisor"="hvt" 79 | LABEL "com.urunc.unikernel.useDMBlock"="false" 80 | ``` 81 | 82 | In the above file: 83 | 84 | - We directly copy the unikernel binary in the OCI's image rootfs. 85 | - We manually specify through labels the `urunc` annotations. 86 | 87 | We can build the OCI image with the following command: 88 | 89 | ``` 90 | docker build -f Containerfile -t urunc/prebuilt/network-mirage-hvt:test . 91 | ``` 92 | 93 | ## Using `bunix` 94 | 95 | In the case of [bunix](https://github.com/nubificus/bunix) we need to clone the whole 96 | repository in the same directly as the 97 | unikernel. Then, we simply need to edit the `args.nix` file as: 98 | 99 | ```Nix 100 | { 101 | name = "urunc/prebuilt/network-mirage-hvt"; 102 | tag = "test"; 103 | files = { 104 | "./network.hvt" = "/unikernel/network.hvt"; 105 | }; 106 | annotations = { 107 | unikernelType = "mirage"; 108 | hypervisor = "hvt"; 109 | binary = "/unikernel/network.hvt"; 110 | cmdline = ""; 111 | unikernelVersion = ""; 112 | initrd = ""; 113 | block = ""; 114 | blkMntPoint = ""; 115 | useDMBlock = "false"; 116 | }; 117 | } 118 | ``` 119 | 120 | In the above file: 121 | 122 | - We directly specify the files to copy inside the OCI's image rootfs. 123 | - We manually specify all `urunc` annotations. 124 | 125 | We can build the OCI image by simply running the following command: 126 | 127 | ```bash 128 | nix-build default.nix 129 | ``` 130 | 131 | The above command will create a container image in a tar inside Nix's store. For 132 | easier access of the tar, Nix creates a symlink of the tar file in the CWD. The 133 | symlink will be named as `result`. Therefore, we can load the container image with: 134 | 135 | ```bash 136 | docker load < result 137 | ``` 138 | -------------------------------------------------------------------------------- /docs/package/reuse.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: "Reusing unikernels from OCI images" 4 | description: "Reusing OCI images that contain unikernels" 5 | --- 6 | 7 | # Reusing OCI images that contain unikernels 8 | 9 | In this page we will explain how we can reuse existing OCI images that contain 10 | unikernels to either update or append `urunc` annotations. As an 11 | example, we will use an existing [Unikraft](https://unikraft.org) Unikernel 12 | image from [Unikraft's catalog](https://github.com/unikraft/catalog), The goal 13 | will be to transform this image to an OCI image that `urunc` can handle, by 14 | simply appending the necessary annotations. 15 | 16 | Currently only `bunny` supports reusing an existing OCI image. However, both 17 | file formats, `bunnyfile` and Dockerfile-like syntax files, can be used. 18 | 19 | > **NOTE**: The below steps can be easily adjusted to any existing OCI image. 20 | 21 | ## Using a `bunnyfile` 22 | 23 | In order to append `urunc` annotations in an existing [Unikraft](https://unikraft.org) OCI image, 24 | we can define the `bunnyfile` as: 25 | 26 | ``` 27 | #syntax=harbor.nbfc.io/nubificus/bunny:latest 28 | version: v0.1 29 | 30 | platforms: 31 | framework: unikraft 32 | monitor: qemu 33 | architecture: x86 34 | 35 | kernel: 36 | from: unikraft.org/nginx:1.15 37 | path: /unikraft/bin/kernel 38 | 39 | cmdline: "nginx -c /nginx/conf/nginx.conf" 40 | ``` 41 | 42 | In the above file we specify the followings: 43 | 44 | - We want to use a [Unikraft](https://unikraft.org) unikernel that will execute on top of Qemu over x86 45 | architecture. 46 | - We want to use the unikernel binary `/unikraft/bin/kernel` from the 47 | `unikraft.org/nginx:1.15` OCI image. 48 | - We specify the cmdline for the unikernel as `nginx -c /nginx/conf/nginx.conf"` 49 | 50 | With the above file, `bunny` will fetch the OCI image and append the `urunc` 51 | annotations. We can build the OCI image with the following command: 52 | 53 | ``` 54 | docker build -f bunnyfile -t urunc/reuse/nginx-unikraft-qemu:test . 55 | ``` 56 | 57 | ## Using a Dockerfile-like syntax 58 | 59 | In the case of the Dockerfile-like syntax file, we need to manually specify the 60 | `urunc` annotations, using the respective labels. Therefore, to transform the 61 | above `bunnyfile` to the equivalent `Containerfile`: 62 | 63 | ``` 64 | #syntax=harbor.nbfc.io/nubificus/bunny:latest 65 | FROM unikraft.org/nginx:1.15 66 | 67 | LABEL com.urunc.unikernel.binary="/unikraft/bin/kernel" 68 | LABEL "com.urunc.unikernel.cmdline"="nginx -c /nginx/conf/nginx.conf" 69 | LABEL "com.urunc.unikernel.unikernelType"="unikraft" 70 | LABEL "com.urunc.unikernel.hypervisor"="qemu" 71 | ``` 72 | 73 | In the above file: 74 | 75 | - We set the `unikraft.org/nginx:1.15` as the base for our OCI image. 76 | - We manually specify through labels the `urunc` annotations. 77 | 78 | We can build the OCI image with the following command: 79 | 80 | ``` 81 | docker build -f Containerfile -t urunc/prebuilt/nginx-unikraft-qemu:test . 82 | ``` 83 | -------------------------------------------------------------------------------- /docs/tutorials/Non-root-monitor-execution.md: -------------------------------------------------------------------------------- 1 | # Non-root execution of monitor 2 | 3 | To enhance security, `urunc` supports running the monitor process (VMM or 4 | seccomp monitor) as a non-root user. In this tutorial, we will walk through the 5 | necessary steps to set up the environment and successfully execute the monitor 6 | as a non-root user. 7 | 8 | ## Requirements 9 | 10 | The vast majority of supported monitors use KVM and therefore require access to 11 | `/dev/kvm`. This includes monitors like 12 | [Solo5-hvt](https://github.com/Solo5/solo5), [Qemu](https://www.qemu.org), and 13 | [Firecracker](https://github.com/firecracker-microvm/firecracker). In contrast, 14 | [Solo5-spt](https://github.com/Solo5/solo5) does not require access to 15 | `/dev/kvm`. As such, when spawning the monitor process, we must ensure that it 16 | has the necessary permissions to access `/dev/kvm`. 17 | 18 | Usually `/dev/kvm` has the following filesystem permissions: 19 | 20 | ``` 21 | $ ls -l /dev/kvm 22 | crw-rw---- 1 root kvm 10, 232 Apr 3 08:10 /dev/kvm 23 | ``` 24 | 25 | In case the above permissions are different in your system, we strongly 26 | recommend to perform the following steps: 27 | 28 | ``` 29 | $ sudo groupadd kvm -r 30 | $ sudo chown root:kvm /dev/kvm 31 | $ sudo chmod 660 /dev/kvm 32 | ``` 33 | 34 | An important information we need to obtain is the group ID of `kvm` group: 35 | 36 | ``` 37 | $ getent group kvm 38 | kvm:x:108:ubuntu 39 | ``` 40 | 41 | ## Running the monitor as non-root user 42 | 43 | By default `urunc` will execute the monitor setting up the `uid`, `gid` and 44 | `additionalGids` from the [container's OCI 45 | configuration](https://github.com/opencontainers/runtime-spec/blob/main/config.md#posix-platform-user). 46 | As a result, we simply need to instruct `urunc` to run a container as the desired 47 | user. However, since most monitors require access to `/dev/kvm`, we must ensure 48 | that the container is a member of the `kvm` group to grant the necessary 49 | permissions. 50 | 51 | ### Docker and Nerdctl 52 | 53 | In the case of docker and nerdctl, we can set the user and the groups of the 54 | container with the `--user :` option and the additional groups using 55 | `--group-add ` for each additional group. Therefore, to run a KVM-enabled 56 | monitor with `urunc` as `nobody`, we use the following command: 57 | 58 | ``` 59 | $ sudo nerdctl run --user 65534:65534 --group-add 108 --runtime "io.containerd.urunc.v2" --rm -it harbor.nbfc.io/nubificus/urunc/nginx-firecracker-unikraft-initrd:latest 60 | ``` 61 | 62 | Pay attention to the `--group-add 108` option which instructs `urunc` to add 63 | the group with id `108` as an additional group for the container. The 64 | `108` id is the group id of `kvm` that we found previously. 65 | 66 | On the other hand, if we are using a monitor that does not require access to 67 | `/dev/kvm`, such as [Solo5-spt](https://github.com/Solo5/solo5), then we can 68 | omit the `--group-add` command. As a result, the command will transform to: 69 | 70 | ``` 71 | $ sudo nerdctl run --user 65534:65534 --runtime "io.containerd.urunc.v2" --rm -it harbor.nbfc.io/nubificus/urunc/net-spt-mirage:latest 72 | ``` 73 | 74 | > Note The commands are the same for docker. 75 | 76 | 77 | ### In a k8s deployment 78 | 79 | Similarly, in the case of Kubernetes, we can specify the monitor's process user 80 | and groups by defining the container's user and groups. We can do that in 81 | the `securityContext` field of the deployment yaml: 82 | 83 | ``` 84 | securityContext: 85 | runAsUser: 65534 86 | runAsGroup: 65534 87 | supplementalGroups: [108] 88 | ``` 89 | 90 | As previously mentioned, if we want to run a monitor that requires access to 91 | `/dev/kvm`, we need to add the `kvm`'s groupid in the `supplementalGroups`. 92 | Otherwise, we do not have to specify any supplementary group. 93 | 94 | For more information regarding the Security Context of a Pod / Container take a 95 | look at [Kubernetes's 96 | documentation](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/). 97 | 98 | 99 | ## Caveats 100 | 101 | For monitors that require access to `/dev/kvm`, we need to ensure that the 102 | container (and, by extension, the monitor) is a member of the `kvm` group. This 103 | is unavoidable. Additionally, it is essential that the `kvm` 104 | group has the same ID across all nodes to make sure that all containers, 105 | regardless of the node, can access `/dev/kvm`. 106 | 107 | Another caveat of this setup is access to the device mapper snapshot. In some 108 | cases, `urunc` uses the device mapper snapshot as a block device for the 109 | unikernel. However, the snapshot is typically created by root and belongs to the 110 | `disk` group. As a result, to use this feature of `urunc`, we will also need 111 | to add the container to the `disk` group, similar to how we handle the `kvm` 112 | group. Fortunately, there are options we can explore to address this issue, and 113 | we are actively working towards a solution. 114 | -------------------------------------------------------------------------------- /docs/tutorials/index.md: -------------------------------------------------------------------------------- 1 | In this section we include some end-to-end tutorials on deploying 2 | `urunc`-compatible workloads to various environments: 3 | 4 | - [`urunc` in Kubernetes](../tutorials/How-to-urunc-on-k8s) 5 | - [`urunc` in EKS](../tutorials/eks-tutorial) 6 | - [Knative integration](../tutorials/knative) 7 | - [Non root monitor execution](../tutorials/Non-root-monitor-execution) 8 | - [Running existing containers over Linux](../tutorials/existing-container-linux) 9 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nubificus/urunc 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 9 | github.com/containerd/containerd v1.7.27 10 | github.com/creack/pty v1.1.24 11 | github.com/elastic/go-seccomp-bpf v1.5.0 12 | github.com/hashicorp/go-version v1.7.0 13 | github.com/jackpal/gateway v1.0.16 14 | github.com/moby/sys/mount v0.3.4 15 | github.com/nubificus/hedge_cli v0.0.3 16 | github.com/opencontainers/runc v1.2.6 17 | github.com/opencontainers/runtime-spec v1.2.1 18 | github.com/prometheus-community/pro-bing v0.7.0 19 | github.com/rs/zerolog v1.33.0 20 | github.com/sirupsen/logrus v1.9.3 21 | github.com/stretchr/testify v1.10.0 22 | github.com/urfave/cli v1.22.16 23 | github.com/vishvananda/netlink v1.3.0 24 | github.com/vishvananda/netns v0.0.5 25 | golang.org/x/sys v0.31.0 26 | k8s.io/cri-api v0.32.3 27 | ) 28 | 29 | require ( 30 | github.com/Microsoft/go-winio v0.6.2 // indirect 31 | github.com/Microsoft/hcsshim v0.12.9 // indirect 32 | github.com/cilium/ebpf v0.17.3 // indirect 33 | github.com/containerd/cgroups/v3 v3.0.5 // indirect 34 | github.com/containerd/console v1.0.4 // indirect 35 | github.com/containerd/containerd/api v1.8.0 // indirect 36 | github.com/containerd/continuity v0.4.5 // indirect 37 | github.com/containerd/errdefs v1.0.0 // indirect 38 | github.com/containerd/errdefs/pkg v0.3.0 // indirect 39 | github.com/containerd/fifo v1.1.0 // indirect 40 | github.com/containerd/go-runc v1.0.0 // indirect 41 | github.com/containerd/log v0.1.0 // indirect 42 | github.com/containerd/platforms v0.2.1 // indirect 43 | github.com/containerd/ttrpc v1.2.7 // indirect 44 | github.com/containerd/typeurl/v2 v2.2.3 // indirect 45 | github.com/coreos/go-systemd/v22 v22.5.0 // indirect 46 | github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect 47 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 48 | github.com/docker/go-events v0.0.0-20250114142523-c867878c5e32 // indirect 49 | github.com/docker/go-units v0.5.0 // indirect 50 | github.com/godbus/dbus/v5 v5.1.0 // indirect 51 | github.com/gogo/protobuf v1.3.2 // indirect 52 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect 53 | github.com/google/go-cmp v0.7.0 // indirect 54 | github.com/google/uuid v1.6.0 // indirect 55 | github.com/klauspost/compress v1.18.0 // indirect 56 | github.com/mattn/go-colorable v0.1.14 // indirect 57 | github.com/mattn/go-isatty v0.0.20 // indirect 58 | github.com/moby/sys/mountinfo v0.7.2 // indirect 59 | github.com/moby/sys/sequential v0.6.0 // indirect 60 | github.com/moby/sys/user v0.3.0 // indirect 61 | github.com/moby/sys/userns v0.1.0 // indirect 62 | github.com/opencontainers/go-digest v1.0.0 // indirect 63 | github.com/opencontainers/image-spec v1.1.1 // indirect 64 | github.com/pkg/errors v0.9.1 // indirect 65 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 66 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 67 | github.com/stretchr/objx v0.5.2 // indirect 68 | go.opencensus.io v0.24.0 // indirect 69 | golang.org/x/net v0.38.0 // indirect 70 | golang.org/x/sync v0.13.0 // indirect 71 | golang.org/x/text v0.23.0 // indirect 72 | google.golang.org/genproto v0.0.0-20250313205543-e70fdf4c4cb4 // indirect 73 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect 74 | google.golang.org/grpc v1.71.0 // indirect 75 | google.golang.org/protobuf v1.36.5 // indirect 76 | gopkg.in/yaml.v3 v3.0.1 // indirect 77 | ) 78 | -------------------------------------------------------------------------------- /internal/constants/constants.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2025, Nubificus LTD 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package constants 16 | 17 | const TimestampTargetFile = "/tmp/urunc.zlog" 18 | -------------------------------------------------------------------------------- /internal/constants/network_constants.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2025, Nubificus LTD 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package constants 16 | 17 | const ( 18 | StaticNetworkTapIP = "172.16.1.1" 19 | StaticNetworkUnikernelIP = "172.16.1.2" 20 | // TODO: Experiment with DynamicNetworkTapIP starting from 172.16.X.1 21 | DynamicNetworkTapIP = "172.16.X.2" 22 | QueueProxyRedirectIP = "172.16.1.2" 23 | ) 24 | -------------------------------------------------------------------------------- /internal/metrics/metrics.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2025, Nubificus LTD 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package metrics 16 | 17 | import ( 18 | "os" 19 | 20 | "github.com/rs/zerolog" 21 | ) 22 | 23 | var enableTimestamps = os.Getenv("URUNC_TIMESTAMPS") 24 | 25 | type Writer interface { 26 | Capture(containerID string, timestampID string) 27 | } 28 | 29 | type zerologMetrics struct { 30 | logger *zerolog.Logger 31 | } 32 | 33 | func (z *zerologMetrics) Capture(containerID string, timestampID string) { 34 | z.logger.Log().Str("containerID", containerID).Str("timestampID", timestampID).Msg("") 35 | } 36 | 37 | func NewZerologMetrics(target string) Writer { 38 | if enableTimestamps == "1" { 39 | file, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) 40 | if err != nil { 41 | return nil 42 | } 43 | logger := zerolog.New(file).Level(zerolog.InfoLevel).With().Timestamp().Logger() 44 | zerolog.TimeFieldFormat = zerolog.TimeFormatUnixNano 45 | return &zerologMetrics{ 46 | logger: &logger, 47 | } 48 | } 49 | return &mockWriter{} 50 | } 51 | 52 | type mockWriter struct{} 53 | 54 | func (m *mockWriter) Capture(_, _ string) {} 55 | 56 | func NewMockMetrics(_ string) Writer { 57 | return &mockWriter{} 58 | } 59 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023-2025, Nubificus LTD 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | site_name: urunc Documentation 16 | site_name: urunc Documentation 17 | site_url: https://urunc.io/ 18 | site_author: urunc, a Series of LF Projects, LLC 19 | 20 | repo_url: https://github.com/urunc-dev/urunc 21 | repo_name: urunc-dev/urunc 22 | 23 | #not_in_nav: /index.md 24 | 25 | nav: 26 | - Overview: index.md 27 | - Quickstart: quickstart.md 28 | - Installation: installation.md 29 | - VMM/Sandbox Support: hypervisor-support.md 30 | - Unikernel Support: unikernel-support.md 31 | - Building/Packaging unikernels: 32 | - package/*.md 33 | - Design: 34 | - design/*.md 35 | - Developer Guide: 36 | - developer-guide/*.md 37 | - API Reference: https://pkg.go.dev/github.com/nubificus/urunc@v0.3.0/pkg/unikontainers 38 | - Tutorials: 39 | - tutorials/*.md 40 | 41 | theme: 42 | name: material 43 | favicon: assets/images/favicon-32x32.png 44 | logo_dark_mode: 'assets/images/urunc-logo-dark.svg' 45 | logo_light_mode: 'assets/images/urunc-logo-light.svg' 46 | 47 | custom_dir: docs/overrides 48 | palette: 49 | # Palette toggle for automatic mode 50 | - media: "(prefers-color-scheme)" 51 | toggle: 52 | icon: material/brightness-auto 53 | name: Switch to light mode 54 | 55 | # Palette toggle for light mode 56 | - media: "(prefers-color-scheme: light)" 57 | scheme: urunc 58 | primary: custom 59 | accent: custom 60 | toggle: 61 | icon: material/brightness-7 62 | name: Switch to dark mode 63 | 64 | # Palette toggle for dark mode 65 | - media: "(prefers-color-scheme: dark)" 66 | scheme: slate 67 | primary: custom 68 | accent: custom 69 | #primary: black 70 | #accent: light blue 71 | toggle: 72 | icon: material/brightness-4 73 | name: Switch to light mode 74 | 75 | features: 76 | - navigation.instant 77 | - navigation.sections 78 | - navigation.expand 79 | - toc.integrate 80 | 81 | markdown_extensions: 82 | - footnotes 83 | - attr_list 84 | 85 | toc: 86 | - permalink: true 87 | 88 | copyright: "2022" 89 | 90 | extra_css: 91 | - assets/stylesheets/theme.css 92 | extra_javascript: 93 | - assets/javascripts/console-copy.js 94 | 95 | extra: 96 | version: 97 | provider: mike 98 | alias: true 99 | analytics: 100 | provider: google 101 | property: G-X2S0PFR6ZY 102 | 103 | markdown_extensions: 104 | - admonition 105 | - attr_list 106 | - footnotes 107 | - md_in_html 108 | - toc: 109 | permalink: '🔗' 110 | toc_depth: 3 111 | - pymdownx.blocks.tab: 112 | alternate_style: True 113 | combine_header_slug: True 114 | slugify: !!python/object/apply:pymdownx.slugs.slugify 115 | kwds: 116 | case: lower 117 | - pymdownx.blocks.caption 118 | - pymdownx.details 119 | - pymdownx.highlight: 120 | anchor_linenums: true 121 | line_spans: __span 122 | pygments_lang_class: true 123 | - pymdownx.inlinehilite 124 | - pymdownx.snippets 125 | - pymdownx.superfences: 126 | custom_fences: 127 | # exclude '$' from copying 128 | - name: console 129 | class: console 130 | validator: ^.* 131 | format: !!python/name:pymdownx.superfences.fence_code_format 132 | validator: ^.* 133 | format: !!python/name:pymdownx.superfences.fence_code_format 134 | - name: mermaid 135 | class: mermaid 136 | format: !!python/name:pymdownx.superfences.fence_code_format 137 | - pymdownx.emoji: 138 | emoji_index: !!python/name:material.extensions.emoji.twemoji 139 | emoji_generator: !!python/name:material.extensions.emoji.to_svg 140 | 141 | plugins: 142 | - search 143 | - section-index 144 | - literate-nav 145 | - macros: 146 | j2_block_start_string: "[%" 147 | j2_block_end_string: "%]" 148 | j2_variable_start_string: "[[" 149 | j2_variable_end_string: "]]" 150 | j2_comment_start_string: '[#' 151 | j2_comment_end_string: '#]' 152 | - minify: 153 | minify_html: true 154 | minify_css: true 155 | minify_js: true 156 | htmlmin_opts: 157 | remove_comments: true 158 | cache_safe: true 159 | css_files: 160 | - assets/stylesheets/theme.css 161 | js_files: 162 | - assets/javascripts/console-copy.js 163 | - autorefs 164 | 165 | hooks: 166 | - docs/hooks/copyright.py 167 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | # Uncomment the line below to ignore any configuration changes in `docs/` 3 | #ignore = "git diff --quiet origin/main -- ':!*.toml' ./docs/" 4 | ignore = "git diff --quiet origin/main ./docs/" 5 | command = "pip3 install mkdocs mkdocs-material mkdocs-section-index mkdocs-literate-nav && python3 -m mkdocs build" 6 | -------------------------------------------------------------------------------- /pkg/network/network_dynamic.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2025, Nubificus LTD 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package network 16 | 17 | import ( 18 | "fmt" 19 | "strconv" 20 | "strings" 21 | 22 | "github.com/nubificus/urunc/internal/constants" 23 | "github.com/vishvananda/netlink" 24 | ) 25 | 26 | type DynamicNetwork struct { 27 | } 28 | 29 | // NetworkSetup checks if any tap device is available in the current netns. If it is, it assumes a running unikernel 30 | // is present in the current netns and returns an error, because network functionality for more than one unikernels 31 | // is not yet implemented. 32 | // If no TAP devices are available in the current netns, it creates a new tap device and 33 | // sets TC rules between the veth interface and the tap device inside the namespace. 34 | // 35 | // FIXME: CUrrently only one tap device per netns can provide functional networking. We need to find a proper way to handle networking 36 | // for multiple unikernels in the same pod/network namespace. 37 | // See: https://github.com/nubificus/urunc/issues/13 38 | func (n DynamicNetwork) NetworkSetup(uid uint32, gid uint32) (*UnikernelNetworkInfo, error) { 39 | tapIndex, err := getTapIndex() 40 | if err != nil { 41 | return nil, err 42 | } 43 | if tapIndex > 0 { 44 | return nil, fmt.Errorf("unsupported operation: can't spawn multiple unikernels in the same network namespace") 45 | } 46 | redirectLink, err := netlink.LinkByName(DefaultInterface) 47 | if err != nil { 48 | netlog.Errorf("failed to find %s interface", DefaultInterface) 49 | return nil, err 50 | } 51 | newTapName := strings.ReplaceAll(DefaultTap, "X", strconv.Itoa(tapIndex)) 52 | ipTemplate := fmt.Sprintf("%s/24", constants.DynamicNetworkTapIP) 53 | newIPAddr := strings.ReplaceAll(ipTemplate, "X", strconv.Itoa(tapIndex+1)) 54 | newTapDevice, err := networkSetup(newTapName, newIPAddr, redirectLink, true, uid, gid) 55 | if err != nil { 56 | return nil, err 57 | } 58 | ifInfo, err := getInterfaceInfo(DefaultInterface) 59 | if err != nil { 60 | return nil, err 61 | } 62 | return &UnikernelNetworkInfo{ 63 | TapDevice: newTapDevice.Attrs().Name, 64 | EthDevice: ifInfo, 65 | }, nil 66 | } 67 | -------------------------------------------------------------------------------- /pkg/network/network_static.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2025, Nubificus LTD 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package network 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "os" 21 | "os/exec" 22 | "strings" 23 | 24 | "github.com/nubificus/urunc/internal/constants" 25 | "github.com/vishvananda/netlink" 26 | ) 27 | 28 | var StaticIPAddr = fmt.Sprintf("%s/24", constants.StaticNetworkTapIP) 29 | 30 | type StaticNetwork struct { 31 | } 32 | 33 | // Apply the following rule: 34 | // iptables -t nat -A POSTROUTING -o -s -j MASQUERADE --wait 1 35 | // and write 1 to /proc/sys/net/ipv4/ip_forward to enable IP forwarding. 36 | func setNATRule(iface string, sourceIP string) error { 37 | var args []string 38 | var stdout, stderr bytes.Buffer 39 | 40 | path, err := exec.LookPath("iptables") 41 | if err != nil { 42 | return err 43 | } 44 | 45 | file, err := os.OpenFile("/proc/sys/net/ipv4/ip_forward", os.O_WRONLY, 0644) 46 | if err != nil { 47 | return fmt.Errorf("failed to open /proc/sys/net/ipv4/ip_forward: %w", err) 48 | } 49 | defer file.Close() 50 | 51 | _, err = file.WriteString("1") 52 | if err != nil { 53 | return fmt.Errorf("failed to enable IP forwarding: %w", err) 54 | } 55 | netlog.Debugf("Enabled IP forwarding") 56 | 57 | args = append(args, path) 58 | args = append(args, "-t") 59 | args = append(args, "nat") 60 | args = append(args, "-A") 61 | args = append(args, "POSTROUTING") 62 | args = append(args, "-s") 63 | args = append(args, sourceIP) 64 | args = append(args, "-o") 65 | args = append(args, iface) 66 | args = append(args, "-j") 67 | args = append(args, "MASQUERADE") 68 | args = append(args, "--wait") 69 | args = append(args, "1") 70 | 71 | cmd := exec.Cmd{ 72 | Path: path, 73 | Args: args, 74 | Stdout: &stdout, 75 | Stderr: &stderr, 76 | } 77 | err = cmd.Run() 78 | if err != nil { 79 | switch err.(type) { 80 | case *exec.ExitError: 81 | return fmt.Errorf("iptables command %s failed: %s", cmd.String(), stderr.String()) 82 | default: 83 | return err 84 | } 85 | } 86 | 87 | netlog.Infof("Applied iptables rule for NAT") 88 | 89 | return nil 90 | } 91 | 92 | func (n StaticNetwork) NetworkSetup(uid uint32, gid uint32) (*UnikernelNetworkInfo, error) { 93 | newTapName := strings.ReplaceAll(DefaultTap, "X", "0") 94 | addTCRules := false 95 | redirectLink, err := netlink.LinkByName(DefaultInterface) 96 | if err != nil { 97 | netlog.Errorf("failed to find %s interface", DefaultInterface) 98 | return nil, err 99 | } 100 | newTapDevice, err := networkSetup(newTapName, StaticIPAddr, redirectLink, addTCRules, uid, gid) 101 | if err != nil { 102 | return nil, err 103 | } 104 | err = setNATRule(DefaultInterface, StaticIPAddr) 105 | if err != nil { 106 | return nil, err 107 | } 108 | return &UnikernelNetworkInfo{ 109 | TapDevice: newTapDevice.Attrs().Name, 110 | EthDevice: Interface{ 111 | IP: constants.StaticNetworkUnikernelIP, 112 | DefaultGateway: constants.StaticNetworkTapIP, 113 | Mask: "255.255.255.0", 114 | Interface: DefaultInterface, // or tap0_urunc? 115 | MAC: redirectLink.Attrs().HardwareAddr.String(), 116 | }, 117 | }, nil 118 | } 119 | -------------------------------------------------------------------------------- /pkg/unikontainers/hypervisors/firecracker.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2025, Nubificus LTD 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package hypervisors 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | "os" 21 | "runtime" 22 | "strings" 23 | "syscall" 24 | 25 | "github.com/nubificus/urunc/pkg/unikontainers/unikernels" 26 | ) 27 | 28 | const ( 29 | FirecrackerVmm VmmType = "firecracker" 30 | FirecrackerBinary string = "firecracker" 31 | FCJsonFilename string = "fc.json" 32 | ) 33 | 34 | type Firecracker struct { 35 | binaryPath string 36 | binary string 37 | } 38 | 39 | type FirecrackerBootSource struct { 40 | ImagePath string `json:"kernel_image_path"` 41 | BootArgs string `json:"boot_args"` 42 | InitrdPath string `json:"initrd_path,omitempty"` 43 | } 44 | 45 | type FirecrackerMachine struct { 46 | VcpuCount uint `json:"vcpu_count"` 47 | MemSizeMiB uint64 `json:"mem_size_mib"` 48 | Smt bool `json:"smt"` 49 | TrackDirtyPages bool `json:"track_dirty_pages"` 50 | } 51 | 52 | type FirecrackerDrive struct { 53 | DriveID string `json:"drive_id"` 54 | IsRO bool `json:"is_read_only"` 55 | IsRootDev bool `json:"is_root_device"` 56 | HostPath string `json:"path_on_host"` 57 | } 58 | 59 | type FirecrackerNet struct { 60 | IfaceID string `json:"iface_id"` 61 | GuestMAC string `json:"guest_mac,omitempty"` 62 | HostIF string `json:"host_dev_name"` 63 | } 64 | 65 | type FirecrackerConfig struct { 66 | Source FirecrackerBootSource `json:"boot-source"` 67 | Machine FirecrackerMachine `json:"machine-config"` 68 | Drives []FirecrackerDrive `json:"drives"` 69 | NetIfs []FirecrackerNet `json:"network-interfaces"` 70 | } 71 | 72 | func (fc *Firecracker) Stop(_ string) error { 73 | return nil 74 | } 75 | 76 | func (fc *Firecracker) Ok() error { 77 | return nil 78 | } 79 | func (fc *Firecracker) Path() string { 80 | return fc.binaryPath 81 | } 82 | 83 | func (fc *Firecracker) Execve(args ExecArgs, _ unikernels.Unikernel) error { 84 | // FIXME: Note for getting unikernel specific options. 85 | // Due to the way FC operates, we have not encountered any guest specific 86 | // options yet. However, we need to revisit how we can use guest specific 87 | // options in FC, since the string return value of the Monitor related 88 | // functions in the unikernel interface do not integrate well with FC's 89 | // json configuration. 90 | cmdString := fc.Path() + " --no-api --config-file " 91 | JSONConfigFile := FCJsonFilename 92 | cmdString += JSONConfigFile 93 | if !args.Seccomp { 94 | cmdString += " --no-seccomp" 95 | } 96 | 97 | // VM config for Firecracker 98 | fcMem := DefaultMemory 99 | if args.MemSizeB != 0 { 100 | fcMem = bytesToMiB(args.MemSizeB) 101 | // Check if memory is too small 102 | if fcMem == 0 { 103 | fcMem = DefaultMemory 104 | } 105 | } 106 | FCMachine := FirecrackerMachine{ 107 | VcpuCount: 1, // TODO: Use value from configuration or Environment variable 108 | MemSizeMiB: fcMem, 109 | Smt: false, 110 | TrackDirtyPages: false, 111 | } 112 | 113 | // Net config for Firecracker 114 | FCNet := make([]FirecrackerNet, 0) 115 | AnIF := FirecrackerNet{ 116 | IfaceID: "net1", 117 | GuestMAC: args.GuestMAC, 118 | HostIF: args.TapDevice, 119 | } 120 | FCNet = append(FCNet, AnIF) 121 | 122 | // Block config for Firecracker 123 | // TODO: Add support for block devices in FIrecracker 124 | FCDrives := make([]FirecrackerDrive, 0) 125 | 126 | if args.BlockDevice != "" { 127 | aBlock := FirecrackerDrive{ 128 | DriveID: "rootfs", 129 | IsRO: false, 130 | IsRootDev: true, 131 | HostPath: args.BlockDevice, 132 | } 133 | FCDrives = append(FCDrives, aBlock) 134 | } 135 | // TODO: Check if this check causes any performance drop 136 | // or explore alternative implementations 137 | if runtime.GOARCH == "arm64" { 138 | consoleStr := " console=ttyS0" 139 | args.Command += consoleStr 140 | } 141 | 142 | FCSource := FirecrackerBootSource{ 143 | ImagePath: args.UnikernelPath, 144 | BootArgs: args.Command, 145 | InitrdPath: args.InitrdPath, 146 | } 147 | FCConfig := &FirecrackerConfig{ 148 | Source: FCSource, 149 | Machine: FCMachine, 150 | Drives: FCDrives, 151 | NetIfs: FCNet, 152 | } 153 | FCConfigJSON, _ := json.Marshal(FCConfig) 154 | if err := os.WriteFile(JSONConfigFile, FCConfigJSON, 0o644); err != nil { //nolint: gosec 155 | return fmt.Errorf("failed to save Firecracker json config: %w", err) 156 | } 157 | vmmLog.WithField("Json=", string(FCConfigJSON)).Info("Firecracker json config") 158 | 159 | exArgs := strings.Split(cmdString, " ") 160 | vmmLog.WithField("Firecracker command", exArgs).Info("Ready to execve Firecracker") 161 | 162 | return syscall.Exec(fc.Path(), exArgs, args.Environment) //nolint: gosec 163 | } 164 | -------------------------------------------------------------------------------- /pkg/unikontainers/hypervisors/hedge.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2025, Nubificus LTD 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package hypervisors 16 | 17 | import ( 18 | "fmt" 19 | 20 | hedge "github.com/nubificus/hedge_cli/hedge_api" 21 | "github.com/nubificus/urunc/pkg/unikontainers/unikernels" 22 | ) 23 | 24 | const ( 25 | HedgeVmm VmmType = "hedge" 26 | maxVMListRetries int = 20 27 | ConsoleEndpoint = "/proc/vmcons" 28 | ) 29 | 30 | type Hedge struct{} 31 | 32 | func (h *Hedge) Ok() error { 33 | return fmt.Errorf("hedge not implemented yet") 34 | } 35 | 36 | func (h *Hedge) Stop(_ string) error { 37 | return fmt.Errorf("hedge not implemented yet") 38 | } 39 | 40 | func (h *Hedge) Path() string { 41 | return "" 42 | } 43 | 44 | func (h *Hedge) Execve(_ ExecArgs, _ unikernels.Unikernel) error { 45 | return fmt.Errorf("hedge not implemented yet") 46 | } 47 | 48 | func (h *Hedge) VMState(name string) string { 49 | vms, err := hedge.ListVMs() 50 | if err != nil { 51 | return "error" 52 | } 53 | for _, vm := range vms { 54 | if vm.Name == name { 55 | return "running" 56 | } 57 | } 58 | return "unknown" 59 | } 60 | -------------------------------------------------------------------------------- /pkg/unikontainers/hypervisors/hvt.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2025, Nubificus LTD 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package hypervisors 16 | 17 | import ( 18 | "os/exec" 19 | "strings" 20 | "syscall" 21 | 22 | seccomp "github.com/elastic/go-seccomp-bpf" 23 | "github.com/nubificus/urunc/pkg/unikontainers/unikernels" 24 | ) 25 | 26 | const ( 27 | HvtVmm VmmType = "hvt" 28 | HvtBinary string = "solo5-hvt" 29 | ) 30 | 31 | type HVT struct { 32 | binaryPath string 33 | binary string 34 | } 35 | 36 | // applySeccompFilter applies some secomp filters for the Hvt process. 37 | // By default all systemcalls will cause a SIGSYS, except the ones that we whitelist 38 | func applySeccompFilter() error { 39 | syscalls := []string{ 40 | "rt_sigaction", 41 | "ioctl", 42 | "pread64", 43 | "mmap", 44 | "recvmsg", 45 | "openat", 46 | "sendto", 47 | "mprotect", 48 | "write", 49 | "epoll_ctl", 50 | "epoll_create1", 51 | "read", 52 | "open", 53 | "close", 54 | "fstat", 55 | "stat", 56 | "munmap", 57 | "brk", 58 | "access", 59 | "execve", 60 | "timerfd_create", 61 | "arch_prctl", 62 | "lseek", 63 | "personality", 64 | "socket", 65 | "bind", 66 | "getsockname", 67 | "exit", 68 | "exit_group", 69 | "getpid", 70 | "tgkill", 71 | "nanosleep", 72 | "futex", 73 | "epoll_pwait", 74 | "rt_sigreturn", 75 | "timerfd_settime", 76 | "pwrite64", 77 | "newfstatat", 78 | "set_tid_address", 79 | "set_robust_list", 80 | "rseq", 81 | "prlimit64", 82 | "getrandom", 83 | } 84 | // Some of the actions that we can take for accessing non-permitted system calls are: 85 | // - seccomp.ActionKillThread will kill the thread that tried to use a non-permitted 86 | // system call, but the rest of the threads can still run 87 | // - seccomp.ActionErrno will result to returning EPERM error in all non-permitted 88 | // system calls. 89 | // - ActionTrap will cause a SIGSYS trap to the process. 90 | // 91 | // For the time being, we choose ActionTrap, but we can change this in the future. 92 | filter := seccomp.Filter{ 93 | // Set the threads no_new_privs bit, disabling any new child or execve 94 | // system call to grant privileges that the parent does not have. 95 | NoNewPrivs: true, 96 | // Sync the filter to all threads created by the Go runtime. 97 | Flag: seccomp.FilterFlagTSync, 98 | Policy: seccomp.Policy{ 99 | DefaultAction: seccomp.ActionTrap, 100 | Syscalls: []seccomp.SyscallGroup{ 101 | { 102 | Action: seccomp.ActionAllow, 103 | Names: syscalls, 104 | }, 105 | }, 106 | }, 107 | } 108 | 109 | err := seccomp.LoadFilter(filter) 110 | if err != nil { 111 | vmmLog.Error("Could not load seccomp filters") 112 | return err 113 | } 114 | 115 | vmmLog.Info("Loaded seccomp filters") 116 | vmmLog.Debug("Whitelisted system calls ", syscalls) 117 | 118 | return nil 119 | } 120 | 121 | // Stop is an empty function to satisfy VMM interface compatibility requirements. 122 | // It does not perform any actions and always returns nil. 123 | func (h *HVT) Stop(_ string) error { 124 | return nil 125 | } 126 | 127 | // Path returns the path to the hvt binary. 128 | func (h *HVT) Path() string { 129 | return h.binaryPath 130 | } 131 | 132 | // Ok checks if the hvt binary is available in the system's PATH. 133 | func (h *HVT) Ok() error { 134 | if _, err := exec.LookPath(HvtBinary); err != nil { 135 | return ErrVMMNotInstalled 136 | } 137 | return nil 138 | } 139 | 140 | func (h *HVT) Execve(args ExecArgs, ukernel unikernels.Unikernel) error { 141 | hvtString := string(HvtVmm) 142 | hvtMem := bytesToStringMB(args.MemSizeB) 143 | cmdString := h.binaryPath + " --mem=" + hvtMem 144 | cmdString = appendNonEmpty(cmdString, " "+ukernel.MonitorNetCli(hvtString), args.TapDevice) 145 | cmdString = appendNonEmpty(cmdString, " "+ukernel.MonitorBlockCli(hvtString), args.BlockDevice) 146 | cmdString = appendNonEmpty(cmdString, " ", ukernel.MonitorCli(hvtString)) 147 | cmdString += " " + args.UnikernelPath + " " + args.Command 148 | cmdArgs := strings.Split(cmdString, " ") 149 | if args.Seccomp { 150 | err := applySeccompFilter() 151 | if err != nil { 152 | return err 153 | } 154 | } 155 | vmmLog.WithField("hvt command", cmdString).Error("Ready to execve hvt") 156 | return syscall.Exec(h.binaryPath, cmdArgs, args.Environment) //nolint: gosec 157 | } 158 | -------------------------------------------------------------------------------- /pkg/unikontainers/hypervisors/qemu.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2025, Nubificus LTD 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package hypervisors 16 | 17 | import ( 18 | "runtime" 19 | "strings" 20 | "syscall" 21 | 22 | "github.com/nubificus/urunc/pkg/unikontainers/unikernels" 23 | ) 24 | 25 | const ( 26 | QemuVmm VmmType = "qemu" 27 | QemuBinary string = "qemu-system-" 28 | ) 29 | 30 | type Qemu struct { 31 | binaryPath string 32 | binary string 33 | } 34 | 35 | func (q *Qemu) Stop(_ string) error { 36 | return nil 37 | } 38 | 39 | func (q *Qemu) Ok() error { 40 | return nil 41 | } 42 | func (q *Qemu) Path() string { 43 | return q.binaryPath 44 | } 45 | 46 | func (q *Qemu) Execve(args ExecArgs, ukernel unikernels.Unikernel) error { 47 | qemuString := string(QemuVmm) 48 | qemuMem := bytesToStringMB(args.MemSizeB) 49 | cmdString := q.binaryPath + " -m " + qemuMem + "M" 50 | cmdString += " -cpu host" // Choose CPU 51 | cmdString += " -enable-kvm" // Enable KVM to use CPU virt extensions 52 | cmdString += " -nographic -vga none" // Disable graphic output 53 | 54 | if args.Seccomp { 55 | // Enable Seccomp in QEMU 56 | cmdString += " --sandbox on" 57 | // Allow or Deny Obsolete system calls 58 | cmdString += ",obsolete=deny" 59 | // Allow or Deny set*uid|gid system calls 60 | cmdString += ",elevateprivileges=deny" 61 | // Allow or Deny *fork and execve 62 | cmdString += ",spawn=deny" 63 | // Allow or Deny process affinity and schedular priority 64 | cmdString += ",resourcecontrol=deny" 65 | } 66 | 67 | // TODO: Check if this check causes any performance drop 68 | // or explore alternative implementations 69 | if runtime.GOARCH == "arm64" { 70 | machineType := " -M virt" 71 | cmdString += machineType 72 | } 73 | 74 | cmdString += " -kernel " + args.UnikernelPath 75 | if args.TapDevice != "" { 76 | netcli := ukernel.MonitorNetCli(qemuString) 77 | if netcli == "" { 78 | netcli += " -net nic,model=virtio" 79 | netcli += " -net tap,script=no,downscript=no,ifname=" 80 | } 81 | netcli += args.TapDevice 82 | cmdString += netcli 83 | } else { 84 | cmdString += " -nic none" 85 | } 86 | if args.BlockDevice != "" { 87 | blockCli := ukernel.MonitorBlockCli(qemuString) 88 | if blockCli == "" { 89 | blockCli += " -device virtio-blk-pci,id=blk0,drive=hd0,scsi=off" 90 | blockCli += " -drive format=raw,if=none,id=hd0,file=" 91 | } 92 | blockCli += args.BlockDevice 93 | cmdString += blockCli 94 | } 95 | if args.InitrdPath != "" { 96 | cmdString += " -initrd " + args.InitrdPath 97 | } 98 | cmdString += ukernel.MonitorCli(qemuString) 99 | exArgs := strings.Split(cmdString, " ") 100 | exArgs = append(exArgs, "-append", args.Command) 101 | vmmLog.WithField("qemu command", exArgs).Info("Ready to execve qemu") 102 | return syscall.Exec(q.Path(), exArgs, args.Environment) //nolint: gosec 103 | } 104 | -------------------------------------------------------------------------------- /pkg/unikontainers/hypervisors/spt.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2025, Nubificus LTD 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package hypervisors 16 | 17 | import ( 18 | "os/exec" 19 | "strings" 20 | "syscall" 21 | 22 | "github.com/nubificus/urunc/pkg/unikontainers/unikernels" 23 | ) 24 | 25 | const ( 26 | SptVmm VmmType = "spt" 27 | SptBinary string = "solo5-spt" 28 | ) 29 | 30 | type SPT struct { 31 | binaryPath string 32 | binary string 33 | } 34 | 35 | // Stop is an empty function to satisfy VMM interface compatibility requirements. 36 | // It does not perform any actions and always returns nil. 37 | func (s *SPT) Stop(_ string) error { 38 | return nil 39 | } 40 | 41 | // Path returns the path to the spt binary. 42 | func (s *SPT) Path() string { 43 | return s.binaryPath 44 | } 45 | 46 | // Ok checks if the spt binary is available in the system's PATH. 47 | func (s *SPT) Ok() error { 48 | if _, err := exec.LookPath(SptBinary); err != nil { 49 | return ErrVMMNotInstalled 50 | } 51 | return nil 52 | } 53 | 54 | func (s *SPT) Execve(args ExecArgs, ukernel unikernels.Unikernel) error { 55 | sptString := string(SptVmm) 56 | sptMem := bytesToStringMB(args.MemSizeB) 57 | cmdString := s.binaryPath + " --mem=" + sptMem 58 | cmdString = appendNonEmpty(cmdString, " "+ukernel.MonitorNetCli(sptString), args.TapDevice) 59 | cmdString = appendNonEmpty(cmdString, " "+ukernel.MonitorBlockCli(sptString), args.BlockDevice) 60 | cmdString = appendNonEmpty(cmdString, " ", ukernel.MonitorCli(sptString)) 61 | cmdString += " " + args.UnikernelPath + " " + args.Command 62 | cmdArgs := strings.Split(cmdString, " ") 63 | vmmLog.WithField("spt command", cmdString).Error("Ready to execve spt") 64 | return syscall.Exec(s.binaryPath, cmdArgs, args.Environment) //nolint: gosec 65 | } 66 | -------------------------------------------------------------------------------- /pkg/unikontainers/hypervisors/utils.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2025, Nubificus LTD 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package hypervisors 16 | 17 | import ( 18 | "runtime" 19 | "strconv" 20 | ) 21 | 22 | func cpuArch() string { 23 | switch runtime.GOARCH { 24 | case "arm64": 25 | return "aarch64" 26 | case "amd64": 27 | return "x86_64" 28 | default: 29 | return "" 30 | } 31 | } 32 | 33 | func appendNonEmpty(body, prefix, value string) string { 34 | if value != "" { 35 | return body + prefix + value 36 | } 37 | return body 38 | } 39 | 40 | func bytesToMiB(bytes uint64) uint64 { 41 | const bytesInMiB = 1024 * 1024 42 | return bytes / bytesInMiB 43 | } 44 | 45 | func bytesToMB(bytes uint64) uint64 { 46 | const bytesInMB = 1000 * 1000 47 | return bytes / bytesInMB 48 | } 49 | 50 | func bytesToStringMB(argMem uint64) string { 51 | stringMem := strconv.FormatUint(DefaultMemory, 10) 52 | if argMem != 0 { 53 | userMem := bytesToMB(argMem) 54 | // Check for too low memory 55 | if userMem == 0 { 56 | userMem = DefaultMemory 57 | } 58 | stringMem = strconv.FormatUint(userMem, 10) 59 | } 60 | 61 | return stringMem 62 | } 63 | -------------------------------------------------------------------------------- /pkg/unikontainers/hypervisors/vmm.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2025, Nubificus LTD 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package hypervisors 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "os/exec" 21 | 22 | "github.com/nubificus/urunc/pkg/unikontainers/unikernels" 23 | "github.com/sirupsen/logrus" 24 | ) 25 | 26 | const DefaultMemory uint64 = 256 // The default memory for every hypervisor: 256 MB 27 | 28 | // ExecArgs holds the data required by Execve to start the VMM 29 | // FIXME: add extra fields if required by additional VMM's 30 | type ExecArgs struct { 31 | Container string // The container ID 32 | UnikernelPath string // The path of the unikernel inside rootfs 33 | TapDevice string // The TAP device name 34 | BlockDevice string // The block device path 35 | InitrdPath string // The path to the initrd of the unikernel 36 | Command string // The unikernel's command line 37 | IPAddress string // The IP address of the TAP device 38 | GuestMAC string // The MAC address of the guest network device 39 | Seccomp bool // Enable or disable seccomp filters for the VMM 40 | MemSizeB uint64 // The size of the memory provided to the VM in bytes 41 | Environment []string // Environment 42 | } 43 | 44 | type VmmType string 45 | 46 | var ErrVMMNotInstalled = errors.New("vmm not found") 47 | var vmmLog = logrus.WithField("subsystem", "hypervisors") 48 | 49 | type VMM interface { 50 | Execve(args ExecArgs, ukernel unikernels.Unikernel) error 51 | Stop(t string) error 52 | Path() string 53 | Ok() error 54 | } 55 | 56 | func NewVMM(vmmType VmmType) (vmm VMM, err error) { 57 | defer func() { 58 | if err != nil { 59 | vmmLog.Error(err.Error()) 60 | } 61 | }() 62 | switch vmmType { 63 | case SptVmm: 64 | vmmPath, err := exec.LookPath(SptBinary) 65 | if err != nil { 66 | return nil, ErrVMMNotInstalled 67 | } 68 | return &SPT{binary: SptBinary, binaryPath: vmmPath}, nil 69 | case HvtVmm: 70 | vmmPath, err := exec.LookPath(HvtBinary) 71 | if err != nil { 72 | return nil, ErrVMMNotInstalled 73 | } 74 | return &HVT{binary: HvtBinary, binaryPath: vmmPath}, nil 75 | case QemuVmm: 76 | vmmPath, err := exec.LookPath(QemuBinary + cpuArch()) 77 | if err != nil { 78 | return nil, ErrVMMNotInstalled 79 | } 80 | return &Qemu{binary: QemuBinary, binaryPath: vmmPath}, nil 81 | case FirecrackerVmm: 82 | vmmPath, err := exec.LookPath(FirecrackerBinary) 83 | if err != nil { 84 | return nil, ErrVMMNotInstalled 85 | } 86 | return &Firecracker{binary: FirecrackerBinary, binaryPath: vmmPath}, nil 87 | case HedgeVmm: 88 | hedge := Hedge{} 89 | err := hedge.Ok() 90 | if err != nil { 91 | return nil, ErrVMMNotInstalled 92 | } 93 | return &hedge, nil 94 | default: 95 | return nil, fmt.Errorf("vmm \"%s\" is not supported", vmmType) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /pkg/unikontainers/ipc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2025, Nubificus LTD 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package unikontainers 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "io/fs" 21 | "net" 22 | "os" 23 | "path/filepath" 24 | "time" 25 | 26 | "github.com/sirupsen/logrus" 27 | ) 28 | 29 | type IPCMessage string 30 | 31 | const ( 32 | uruncSock = "urunc.sock" 33 | ReexecStarted IPCMessage = "BOOTED" 34 | AckReexec IPCMessage = "ACK" 35 | StartExecve IPCMessage = "START" 36 | maxRetries = 50 37 | waitTime = 5 * time.Millisecond 38 | ) 39 | 40 | func getSockAddr(dir string, name string) string { 41 | return filepath.Join(dir, name) 42 | } 43 | 44 | func getUruncSockAddr(containerDir string) string { 45 | return getSockAddr(containerDir, uruncSock) 46 | } 47 | 48 | func ensureValidSockAddr(sockAddr string) error { 49 | if sockAddr == "" { 50 | return fmt.Errorf("socket address is empty") 51 | } 52 | if len(sockAddr) > 108 { 53 | return fmt.Errorf("socket address \"%s\" is too long", sockAddr) 54 | } 55 | return nil 56 | } 57 | 58 | // sockAddrExists returns true if if given sock address exists 59 | // returns false if any error is encountered 60 | func SockAddrExists(sockAddr string) bool { 61 | _, err := os.Stat(sockAddr) 62 | if err == nil { 63 | return true 64 | } 65 | if errors.Is(err, fs.ErrNotExist) { 66 | return false 67 | } 68 | Log.WithError(err).Errorf("Failed to get file info for %s", sockAddr) 69 | return false 70 | } 71 | 72 | // SendIPCMessage creates a new connection to socketAddress, sends the message and closes the connection 73 | func SendIPCMessage(socketAddress string, message IPCMessage) error { 74 | conn, err := net.Dial("unix", socketAddress) 75 | if err != nil { 76 | return err 77 | } 78 | defer conn.Close() 79 | 80 | if _, err := conn.Write([]byte(message)); err != nil { 81 | return fmt.Errorf("failed to send message \"%s\" to \"%s\": %w", message, socketAddress, err) 82 | } 83 | return nil 84 | } 85 | 86 | // sendIPCMessageWithRetry attempts to connect to socketAddress. if successful, sends the message and closes the connection 87 | func sendIPCMessageWithRetry(socketAddress string, message IPCMessage, mustBeValid bool) error { 88 | if mustBeValid { 89 | err := ensureValidSockAddr(socketAddress) 90 | if err != nil { 91 | return err 92 | } 93 | } 94 | var conn *net.UnixConn 95 | var err error 96 | retry := 0 97 | for { 98 | conn, err = net.DialUnix("unix", nil, &net.UnixAddr{Name: socketAddress, Net: "unix"}) 99 | if err == nil { 100 | break 101 | } 102 | retry++ 103 | if retry >= maxRetries { 104 | return fmt.Errorf("failed to connect to %s, exceeded max retries", socketAddress) 105 | } 106 | time.Sleep(waitTime) 107 | } 108 | defer func() { 109 | err = conn.Close() 110 | if err != nil { 111 | logrus.WithError(err).Error("failed to close connection") 112 | } 113 | }() 114 | _, err = conn.Write([]byte(message)) 115 | if err != nil { 116 | logrus.WithError(err).Errorf("failed to send message \"%s\" to \"%s\"", message, socketAddress) 117 | } 118 | return err 119 | } 120 | 121 | // createListener sets up a listener for new connection to socketAddress 122 | func CreateListener(socketAddress string, mustBeValid bool) (*net.UnixListener, error) { 123 | if mustBeValid { 124 | err := ensureValidSockAddr(socketAddress) 125 | if err != nil { 126 | return nil, err 127 | } 128 | } 129 | 130 | return net.ListenUnix("unix", &net.UnixAddr{Name: socketAddress, Net: "unix"}) 131 | } 132 | 133 | // awaitMessage opens a new connection to socketAddress 134 | // and waits for a given message 135 | func AwaitMessage(listener *net.UnixListener, expectedMessage IPCMessage) error { 136 | conn, err := listener.AcceptUnix() 137 | if err != nil { 138 | return err 139 | } 140 | defer func() { 141 | err = conn.Close() 142 | if err != nil { 143 | logrus.WithError(err).Error("failed to close connection") 144 | } 145 | }() 146 | buf := make([]byte, len(expectedMessage)) 147 | n, err := conn.Read(buf) 148 | if err != nil { 149 | return fmt.Errorf("failed to read from socket: %w", err) 150 | } 151 | msg := string(buf[0:n]) 152 | if msg != string(expectedMessage) { 153 | return fmt.Errorf("received unexpected message: %s", msg) 154 | } 155 | return nil 156 | } 157 | -------------------------------------------------------------------------------- /pkg/unikontainers/ipc_message.go: -------------------------------------------------------------------------------- 1 | // The below code is taken from runc's libcontainer/message_linux.go 2 | // Commit: ba0b5e2 // spellchecker:disable-line 3 | // The code is shipped under the Apache 2.0 License. For more information 4 | // regarding licensing for this particular file and authors, please 5 | // check runc's repository: https://github.com/opencontainers/runc 6 | // 7 | // There are a few changes made to the code, which follow the 8 | // same exact license with runc and urunc. 9 | 10 | package unikontainers 11 | 12 | import ( 13 | "fmt" 14 | "math" 15 | 16 | "github.com/vishvananda/netlink/nl" 17 | "golang.org/x/sys/unix" 18 | ) 19 | 20 | // list of known message types we want to send to bootstrap program 21 | // The number is randomly chosen to not conflict with known netlink types 22 | const ( 23 | initMsg uint16 = 62000 24 | cloneFlagsAttr uint16 = 27281 25 | nsPathsAttr uint16 = 27282 26 | uidmapAttr uint16 = 27283 27 | gidmapAttr uint16 = 27284 28 | setgroupAttr uint16 = 27285 29 | oomScoreAdjAttr uint16 = 27286 30 | rootlessEUIDAttr uint16 = 27287 31 | uidmapPathAttr uint16 = 27288 32 | gidmapPathAttr uint16 = 27289 33 | timeOffsetsAttr uint16 = 27290 34 | ) 35 | 36 | // netlinkError is an error wrapper type for use by custom netlink message 37 | // types. Panics with errors are wrapped in netlinkError so that the recover 38 | // in bootstrapData can distinguish intentional panics. 39 | type netlinkError struct{ error } 40 | 41 | type int32msg struct { 42 | Type uint16 43 | Value uint32 44 | } 45 | 46 | // serialize serializes the message. 47 | // int32msg has the following representation 48 | // | nlattr len | nlattr type | 49 | // | uint32 value | 50 | func (msg *int32msg) Serialize() []byte { 51 | buf := make([]byte, msg.Len()) 52 | native := nl.NativeEndian() 53 | native.PutUint16(buf[0:2], uint16(msg.Len())) //nolint: gosec 54 | native.PutUint16(buf[2:4], msg.Type) 55 | native.PutUint32(buf[4:8], msg.Value) 56 | return buf 57 | } 58 | 59 | func (msg *int32msg) Len() int { 60 | return unix.NLA_HDRLEN + 4 61 | } 62 | 63 | // bytemsg has the following representation 64 | // | nlattr len | nlattr type | 65 | // | value | pad | 66 | type bytemsg struct { 67 | Type uint16 68 | Value []byte 69 | } 70 | 71 | func (msg *bytemsg) Serialize() []byte { 72 | l := msg.Len() 73 | if l > math.MaxUint16 { 74 | // We cannot return nil nor an error here, so we panic with 75 | // a specific type instead, which is handled via recover in 76 | // bootstrapData. 77 | panic(netlinkError{fmt.Errorf("netlink: cannot serialize bytemsg of length %d (larger than UINT16_MAX)", l)}) 78 | } 79 | buf := make([]byte, (l+unix.NLA_ALIGNTO-1) & ^(unix.NLA_ALIGNTO-1)) 80 | native := nl.NativeEndian() 81 | native.PutUint16(buf[0:2], uint16(l)) //nolint: gosec 82 | native.PutUint16(buf[2:4], msg.Type) 83 | copy(buf[4:], msg.Value) 84 | return buf 85 | } 86 | 87 | func (msg *bytemsg) Len() int { 88 | return unix.NLA_HDRLEN + len(msg.Value) + 1 // null-terminated 89 | } 90 | -------------------------------------------------------------------------------- /pkg/unikontainers/storage_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2025, Nubificus LTD 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package unikontainers 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/stretchr/testify/assert" 21 | ) 22 | 23 | func TestGetBlockDevice(t *testing.T) { 24 | // Create a mock partition 25 | tmpMnt := RootFs{ 26 | Path: "/proc", 27 | Device: "proc", 28 | FsType: "proc", 29 | } 30 | 31 | rootFs, err := getBlockDevice(tmpMnt.Path) 32 | assert.NoError(t, err, "Expected no error in getting block device") 33 | assert.Equal(t, tmpMnt.Path, rootFs.Path, "Expected path to be /mock/path") 34 | assert.Equal(t, tmpMnt.Device, rootFs.Device, "Expected device to be dm-0") 35 | assert.Equal(t, tmpMnt.FsType, rootFs.FsType, "Expected filesystem type to be ext4") 36 | } 37 | -------------------------------------------------------------------------------- /pkg/unikontainers/unikernels/linux.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2025, Nubificus LTD 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package unikernels 16 | 17 | import ( 18 | "fmt" 19 | "strings" 20 | ) 21 | 22 | const LinuxUnikernel string = "linux" 23 | 24 | type Linux struct { 25 | App string 26 | Command string 27 | Env []string 28 | Net LinuxNet 29 | RootFsType string 30 | } 31 | 32 | type LinuxNet struct { 33 | Address string 34 | Gateway string 35 | Mask string 36 | } 37 | 38 | func (l *Linux) CommandString() (string, error) { 39 | rdinit := "" 40 | bootParams := "panic=-1 console=ttyS0" 41 | if l.RootFsType == "block" { 42 | rootParams := "root=/dev/vda rw" 43 | bootParams += " " + rootParams 44 | } else if l.RootFsType == "initrd" { 45 | rootParams := "root=/dev/ram0 rw" 46 | rdinit = "rd" 47 | bootParams += " " + rootParams 48 | } 49 | if l.Net.Address != "" { 50 | netParams := fmt.Sprintf("ip=%s::%s:%s:urunc:eth0:off", 51 | l.Net.Address, 52 | l.Net.Gateway, 53 | l.Net.Mask) 54 | bootParams += " " + netParams 55 | } 56 | for _, eVar := range l.Env { 57 | bootParams += " " + eVar 58 | } 59 | if l.App != "" { 60 | initParams := rdinit + "init=" + l.App + " -- " + l.Command 61 | bootParams += " " + initParams 62 | } 63 | 64 | return bootParams, nil 65 | } 66 | 67 | func (l *Linux) SupportsBlock() bool { 68 | return true 69 | } 70 | 71 | func (l *Linux) SupportsFS(_ string) bool { 72 | return true 73 | } 74 | 75 | func (l *Linux) MonitorNetCli(_ string) string { 76 | return "" 77 | } 78 | 79 | func (l *Linux) MonitorBlockCli(monitor string) string { 80 | switch monitor { 81 | case "qemu": 82 | bcli := " -device virtio-blk-pci,id=blk0,drive=hd0" 83 | bcli += " -drive format=raw,if=none,id=hd0,file=" 84 | return bcli 85 | default: 86 | return "" 87 | } 88 | } 89 | 90 | func (l *Linux) MonitorCli(monitor string) string { 91 | switch monitor { 92 | case "qemu": 93 | return " -no-reboot -serial stdio -nodefaults" 94 | default: 95 | return "" 96 | } 97 | } 98 | 99 | func (l *Linux) Init(data UnikernelParams) error { 100 | // Handling of args with spaces: 101 | // In Linux boot parameters we can not pass multi-word cli arguments 102 | // in init, because they are treated as separate cli arguments. 103 | // TO overcome this we make a convention with urunit: 104 | // 1. We wrap every multi-word cli argument in "'" 105 | // 2. When urunit reads such arguments will combine them and 106 | // pass them to the app as one argument. 107 | for i, arg := range data.CmdLine { 108 | arg = strings.TrimSpace(arg) 109 | spaces := strings.Index(arg, " ") 110 | if spaces > 0 { 111 | data.CmdLine[i] = "'" + arg + "'" 112 | } 113 | } 114 | // we use the first argument in the cli args as the app name and the 115 | // rest as its arguments. 116 | switch len(data.CmdLine) { 117 | case 0: 118 | return fmt.Errorf("No init was specified") 119 | case 1: 120 | l.App = data.CmdLine[0] 121 | l.Command = "" 122 | default: 123 | l.App = data.CmdLine[0] 124 | l.Command = strings.Join(data.CmdLine[1:], " ") 125 | } 126 | 127 | l.Net.Address = data.EthDeviceIP 128 | l.Net.Gateway = data.EthDeviceGateway 129 | l.Net.Mask = data.EthDeviceMask 130 | 131 | l.RootFsType = data.RootFSType 132 | l.Env = data.EnvVars 133 | return nil 134 | } 135 | 136 | func newLinux() *Linux { 137 | linuxStruct := new(Linux) 138 | return linuxStruct 139 | } 140 | -------------------------------------------------------------------------------- /pkg/unikontainers/unikernels/mewz.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2025, Nubificus LTD 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package unikernels 16 | 17 | import ( 18 | "fmt" 19 | "strings" 20 | ) 21 | 22 | const MewzUnikernel string = "mewz" 23 | 24 | type Mewz struct { 25 | Command string 26 | Net MewzNet 27 | } 28 | 29 | type MewzNet struct { 30 | Address string 31 | Mask int 32 | Gateway string 33 | } 34 | 35 | func (m *Mewz) CommandString() (string, error) { 36 | return fmt.Sprintf("ip=%s/%d gateway=%s ", m.Net.Address, m.Net.Mask, 37 | m.Net.Gateway), nil 38 | } 39 | 40 | func (m *Mewz) SupportsBlock() bool { 41 | return false 42 | } 43 | 44 | func (m *Mewz) SupportsFS(_ string) bool { 45 | return false 46 | } 47 | 48 | func (m *Mewz) MonitorNetCli(monitor string) string { 49 | switch monitor { 50 | case "qemu": 51 | ncli := " -device virtio-net-pci,netdev=net0,disable-legacy=on,disable-modern=off" 52 | ncli += " -netdev tap,script=no,downscript=no,id=net0,ifname=" 53 | return ncli 54 | default: 55 | return "" 56 | } 57 | } 58 | 59 | // Mewz does not seem to support virtio block or anu other kind of block/fs. 60 | func (m *Mewz) MonitorBlockCli(_ string) string { 61 | return "" 62 | } 63 | 64 | // Mewz does not require any monitor specific cli option 65 | func (m *Mewz) MonitorCli(monitor string) string { 66 | switch monitor { 67 | case "qemu": 68 | return " -no-reboot -device isa-debug-exit,iobase=0x501,iosize=2" 69 | default: 70 | return "" 71 | } 72 | } 73 | 74 | func (m *Mewz) Init(data UnikernelParams) error { 75 | var mask int 76 | if data.EthDeviceMask != "" { 77 | var err error 78 | mask, err = subnetMaskToCIDR(data.EthDeviceMask) 79 | if err != nil { 80 | return err 81 | } 82 | } else { 83 | mask = 24 84 | } 85 | m.Command = strings.Join(data.CmdLine, " ") 86 | m.Net.Address = data.EthDeviceIP 87 | m.Net.Gateway = data.EthDeviceGateway 88 | m.Net.Mask = mask 89 | 90 | return nil 91 | } 92 | 93 | func newMewz() *Mewz { 94 | mewzStruct := new(Mewz) 95 | return mewzStruct 96 | } 97 | -------------------------------------------------------------------------------- /pkg/unikontainers/unikernels/mirage.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2025, Nubificus LTD 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package unikernels 16 | 17 | import ( 18 | "fmt" 19 | "strings" 20 | ) 21 | 22 | const MirageUnikernel string = "mirage" 23 | 24 | type Mirage struct { 25 | Command string 26 | Net MirageNet 27 | Block MirageBlock 28 | } 29 | 30 | type MirageNet struct { 31 | Address string 32 | Gateway string 33 | } 34 | 35 | type MirageBlock struct { 36 | RootFS string 37 | } 38 | 39 | func (m *Mirage) CommandString() (string, error) { 40 | return fmt.Sprintf("%s %s %s", m.Net.Address, 41 | m.Net.Gateway, 42 | m.Command), nil 43 | } 44 | 45 | func (m *Mirage) SupportsBlock() bool { 46 | return true 47 | } 48 | 49 | func (m *Mirage) SupportsFS(_ string) bool { 50 | return false 51 | } 52 | 53 | func (m *Mirage) MonitorNetCli(monitor string) string { 54 | switch monitor { 55 | case "hvt", "spt": 56 | return "--net:service=" 57 | default: 58 | return "" 59 | } 60 | } 61 | 62 | func (m *Mirage) MonitorBlockCli(monitor string) string { 63 | switch monitor { 64 | case "hvt", "spt": 65 | return "--block:storage=" 66 | default: 67 | return "" 68 | } 69 | } 70 | 71 | func (m *Mirage) MonitorCli(_ string) string { 72 | return "" 73 | } 74 | 75 | func (m *Mirage) Init(data UnikernelParams) error { 76 | // if EthDeviceMask is empty, there is no network support 77 | if data.EthDeviceMask != "" { 78 | m.Net.Address = "--ipv4=" + data.EthDeviceIP + "/24" 79 | m.Net.Gateway = "--ipv4-gateway=" + data.EthDeviceGateway 80 | } 81 | 82 | m.Command = strings.Join(data.CmdLine, " ") 83 | 84 | return nil 85 | } 86 | 87 | func newMirage() *Mirage { 88 | mirageStruct := new(Mirage) 89 | return mirageStruct 90 | } 91 | -------------------------------------------------------------------------------- /pkg/unikontainers/unikernels/rumprun.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2025, Nubificus LTD 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package unikernels 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | "strings" 21 | ) 22 | 23 | const RumprunUnikernel string = "rumprun" 24 | const SubnetMask125 = "128.0.0.0" 25 | 26 | type Rumprun struct { 27 | Command string `json:"cmdline"` 28 | Net RumprunNet `json:"net"` 29 | Blk RumprunBlk `json:"blk"` 30 | } 31 | 32 | type RumprunNoNet struct { 33 | Command string `json:"cmdline"` 34 | Blk RumprunBlk `json:"blk"` 35 | } 36 | 37 | type RumprunCmd struct { 38 | Cmdline string `json:"cmdline"` 39 | } 40 | 41 | type RumprunNet struct { 42 | Interface string `json:"if"` 43 | Cloner string `json:"cloner"` 44 | Type string `json:"type"` 45 | Method string `json:"method"` 46 | Address string `json:"addr"` 47 | Mask string `json:"mask"` 48 | Gateway string `json:"gw"` 49 | } 50 | 51 | type RumprunBlk struct { 52 | Source string `json:"source"` 53 | Path string `json:"path"` 54 | FsType string `json:"fstype"` 55 | Mountpoint string `json:"mountpoint"` 56 | } 57 | 58 | func (r *Rumprun) CommandString() (string, error) { 59 | // if EthDeviceMask is empty, there is no network support. omit every relevant field 60 | if r.Net.Mask == "" { 61 | tmp := RumprunNoNet{ 62 | Command: r.Command, 63 | Blk: r.Blk, 64 | } 65 | jsonData, err := json.Marshal(tmp) 66 | if err != nil { 67 | return "", err 68 | } 69 | jsonStr := string(jsonData) 70 | return jsonStr, nil 71 | } 72 | jsonData, err := json.Marshal(r) 73 | if err != nil { 74 | return "", err 75 | } 76 | jsonStr := string(jsonData) 77 | return jsonStr, nil 78 | } 79 | 80 | func (r *Rumprun) SupportsBlock() bool { 81 | return true 82 | } 83 | 84 | func (r *Rumprun) SupportsFS(fsType string) bool { 85 | switch fsType { 86 | case "ext2": 87 | return true 88 | default: 89 | return false 90 | } 91 | } 92 | 93 | func (r *Rumprun) MonitorNetCli(monitor string) string { 94 | switch monitor { 95 | case "hvt", "spt": 96 | return "--net:tap=" 97 | default: 98 | return "" 99 | } 100 | } 101 | 102 | func (r *Rumprun) MonitorBlockCli(monitor string) string { 103 | switch monitor { 104 | case "hvt", "spt": 105 | return "--block:rootfs=" 106 | default: 107 | return "" 108 | } 109 | } 110 | 111 | // Rumprun can execute only on top of Solo5 and currently there 112 | // are no generic Solo5-specific arguments that Rumprun requires 113 | func (r *Rumprun) MonitorCli(_ string) string { 114 | return "" 115 | } 116 | 117 | func (r *Rumprun) Init(data UnikernelParams) error { 118 | // if EthDeviceMask is empty, there is no network support 119 | if data.EthDeviceMask != "" { 120 | // FIXME: in the case of rumprun & k8s, we need to explicitly set the mask 121 | // to an inclusive value (eg 1 or 0), as NetBSD complains and does not set the default gw 122 | // if it is not reachable from the IP address directly. 123 | mask, err := subnetMaskToCIDR(SubnetMask125) 124 | if err != nil { 125 | return err 126 | } 127 | r.Net.Interface = "ukvmif0" 128 | r.Net.Cloner = "True" 129 | r.Net.Type = "inet" 130 | r.Net.Method = "static" 131 | r.Net.Address = data.EthDeviceIP 132 | r.Net.Mask = fmt.Sprintf("%d", mask) 133 | r.Net.Gateway = data.EthDeviceGateway 134 | } 135 | 136 | r.Blk.Source = "etfs" 137 | r.Blk.Path = "/dev/ld0a" 138 | r.Blk.FsType = "blk" 139 | r.Blk.Mountpoint = "/data" 140 | 141 | r.Command = strings.Join(data.CmdLine, " ") 142 | 143 | return nil 144 | } 145 | 146 | func newRumprun() *Rumprun { 147 | rumprunStruct := new(Rumprun) 148 | 149 | return rumprunStruct 150 | } 151 | -------------------------------------------------------------------------------- /pkg/unikontainers/unikernels/unikernel.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2025, Nubificus LTD 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package unikernels 16 | 17 | import ( 18 | "errors" 19 | ) 20 | 21 | type Unikernel interface { 22 | Init(UnikernelParams) error 23 | CommandString() (string, error) 24 | SupportsBlock() bool 25 | SupportsFS(string) bool 26 | MonitorNetCli(string) string 27 | MonitorBlockCli(string) string 28 | MonitorCli(string) string 29 | } 30 | 31 | // UnikernelParams holds the data required to build the unikernels commandline 32 | type UnikernelParams struct { 33 | CmdLine []string // The cmdline provided by the image 34 | EnvVars []string // The environment variables provided by the image 35 | EthDeviceIP string // The eth device IP 36 | EthDeviceMask string // The eth device mask 37 | EthDeviceGateway string // The eth device gateway 38 | RootFSType string // The rootfs type of the Unikernel 39 | BlockMntPoint string // The mount point for the block device 40 | Version string // The version of the unikernel 41 | } 42 | 43 | var ErrNotSupportedUnikernel = errors.New("unikernel is not supported") 44 | 45 | func New(unikernelType string) (Unikernel, error) { 46 | switch unikernelType { 47 | case RumprunUnikernel: 48 | unikernel := newRumprun() 49 | return unikernel, nil 50 | case UnikraftUnikernel: 51 | unikernel := newUnikraft() 52 | return unikernel, nil 53 | case MirageUnikernel: 54 | unikernel := newMirage() 55 | return unikernel, nil 56 | case MewzUnikernel: 57 | unikernel := newMewz() 58 | return unikernel, nil 59 | case LinuxUnikernel: 60 | unikernel := newLinux() 61 | return unikernel, nil 62 | default: 63 | return nil, ErrNotSupportedUnikernel 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /pkg/unikontainers/unikernels/unikraft.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2025, Nubificus LTD 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package unikernels 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "strings" 21 | 22 | version "github.com/hashicorp/go-version" 23 | ) 24 | 25 | const UnikraftUnikernel string = "unikraft" 26 | const UnikraftCompatVersion string = "0.16.1" 27 | 28 | var ErrUndefinedVersion = errors.New("version is undefined, using default version") 29 | var ErrVersionParsing = errors.New("failed to parse provided version, using default version") 30 | 31 | type Unikraft struct { 32 | AppName string 33 | Command string 34 | Env []string 35 | Net UnikraftNet 36 | VFS UnikraftVFS 37 | Version string 38 | } 39 | 40 | type UnikraftNet struct { 41 | Address string 42 | Mask string 43 | Gateway string 44 | } 45 | 46 | type UnikraftVFS struct { 47 | RootFS string 48 | } 49 | 50 | func (u *Unikraft) CommandString() (string, error) { 51 | envVarString := "" 52 | 53 | if len(u.Env) > 0 { 54 | envVarString = "env.vars=[ " + strings.Join(u.Env, " ") + " ]" 55 | } 56 | 57 | return fmt.Sprintf("%s %s %s %s %s %s -- %s", u.AppName, 58 | envVarString, 59 | u.Net.Address, 60 | u.Net.Gateway, 61 | u.Net.Mask, 62 | u.VFS.RootFS, 63 | u.Command), nil 64 | } 65 | 66 | func (u *Unikraft) SupportsBlock() bool { 67 | return false 68 | } 69 | 70 | func (u *Unikraft) SupportsFS(_ string) bool { 71 | return false 72 | } 73 | 74 | // There is no need for any changes here yet. 75 | func (u *Unikraft) MonitorNetCli(_ string) string { 76 | return "" 77 | } 78 | 79 | // We have not managed to make Unikraft run with block yet. 80 | func (u *Unikraft) MonitorBlockCli(_ string) string { 81 | return "" 82 | } 83 | 84 | // There are no generic CLI hypervisor options for Unikraft yet. 85 | func (u *Unikraft) MonitorCli(_ string) string { 86 | return "" 87 | } 88 | 89 | func (u *Unikraft) Init(data UnikernelParams) error { 90 | u.Env = data.EnvVars 91 | u.Version = data.Version 92 | // We use the first argument in the CLI args as the app name and the 93 | // rest as its arguments. 94 | switch len(data.CmdLine) { 95 | case 0: 96 | u.AppName = "" 97 | u.Command = "" 98 | case 1: 99 | u.AppName = data.CmdLine[0] 100 | u.Command = "" 101 | default: 102 | u.AppName = data.CmdLine[0] 103 | u.Command = strings.Join(data.CmdLine[1:], " ") 104 | } 105 | 106 | return u.configureUnikraftArgs(data.RootFSType, data.EthDeviceIP, data.EthDeviceGateway, data.EthDeviceMask) 107 | } 108 | 109 | func (u *Unikraft) configureUnikraftArgs(rootFsType, ethDeviceIP, ethDeviceGateway, ethDeviceMask string) error { 110 | setCompatArgs := func() { 111 | u.Net.Address = "netdev.ipv4_addr=" + ethDeviceIP 112 | u.Net.Gateway = "netdev.ipv4_gw_addr=" + ethDeviceGateway 113 | u.Net.Mask = "netdev.ipv4_subnet_mask=" + ethDeviceMask 114 | // TODO: We need to add support for actual block devices (e.g. virtio-blk) 115 | // and sharedfs or any other Unikraft related ways to pass data to guest. 116 | if rootFsType == "initrd" { 117 | u.VFS.RootFS = "vfs.rootfs=" + "initrd" 118 | } else { 119 | u.VFS.RootFS = "" 120 | } 121 | } 122 | 123 | setCurrentArgs := func() { 124 | u.Net.Address = "netdev.ip=" + ethDeviceIP + "/24:" + ethDeviceGateway + ":8.8.8.8" 125 | // TODO: We need to add support for actual block devices (e.g. virtio-blk) 126 | // and sharedfs or any other Unikraft related ways to pass data to guest. 127 | if rootFsType == "initrd" { 128 | // TODO: This needs better handling. We need to revisit this 129 | // when we better understand all the available options for 130 | // passing info inside unikraft unikernels. 131 | u.VFS.RootFS = "vfs.fstab=[ \"initrd0:/:extract:::\" ]" 132 | } else { 133 | u.VFS.RootFS = "" 134 | } 135 | } 136 | 137 | if u.Version == "" { 138 | setCurrentArgs() 139 | return ErrUndefinedVersion 140 | } 141 | 142 | unikernelVersion, err := version.NewVersion(u.Version) 143 | if err != nil { 144 | setCurrentArgs() 145 | return ErrVersionParsing 146 | } 147 | 148 | targetVersion, err := version.NewVersion(UnikraftCompatVersion) 149 | if err != nil { 150 | return fmt.Errorf("failed to parse default version: %w", err) 151 | } 152 | 153 | if unikernelVersion.GreaterThanOrEqual(targetVersion) { 154 | setCurrentArgs() 155 | } else { 156 | setCompatArgs() 157 | } 158 | return nil 159 | } 160 | 161 | func newUnikraft() *Unikraft { 162 | unikraftStruct := new(Unikraft) 163 | return unikraftStruct 164 | } 165 | -------------------------------------------------------------------------------- /pkg/unikontainers/unikernels/utils.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2025, Nubificus LTD 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package unikernels 16 | 17 | import ( 18 | "fmt" 19 | "strconv" 20 | "strings" 21 | ) 22 | 23 | func subnetMaskToCIDR(subnetMask string) (int, error) { 24 | maskParts := strings.Split(subnetMask, ".") 25 | if len(maskParts) != 4 { 26 | return 0, fmt.Errorf("invalid subnet mask format") 27 | } 28 | 29 | var cidr int 30 | for _, part := range maskParts { 31 | val, err := strconv.Atoi(part) 32 | if err != nil || val < 0 || val > 255 { 33 | return 0, fmt.Errorf("invalid subnet mask value: %s", part) 34 | } 35 | 36 | // Convert part to binary and count the number of 1 bits 37 | binary := fmt.Sprintf("%08b", val) 38 | cidr += strings.Count(binary, "1") 39 | } 40 | 41 | return cidr, nil 42 | } 43 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | mkdocs 2 | mkdocs-material 3 | mkdocs-section-index 4 | mkdocs-literate-nav 5 | 6 | mkdocs 7 | mkdocs-material 8 | mkdocs-section-index 9 | mkdocs-awesome-nav 10 | mkdocs-macros-plugin 11 | mkdocs-minify-plugin 12 | pymdown-extensions 13 | mkdocstrings[python] 14 | ruff 15 | mike 16 | -------------------------------------------------------------------------------- /script/dm_create.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright (c) 2023-2025, Nubificus LTD 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | 17 | 18 | DATA_DIR=/var/lib/containerd/io.containerd.snapshotter.v1.devmapper 19 | POOL_NAME=containerd-pool 20 | 21 | mkdir -p /var/lib/containerd/ 22 | mkdir -p ${DATA_DIR} 23 | 24 | # Create data file 25 | touch "${DATA_DIR}/data" 26 | truncate -s 100G "${DATA_DIR}/data" 27 | 28 | # Create metadata file 29 | touch "${DATA_DIR}/meta" 30 | truncate -s 10G "${DATA_DIR}/meta" 31 | 32 | # Allocate loop devices 33 | DATA_DEV=$(losetup --find --show "${DATA_DIR}/data") 34 | META_DEV=$(losetup --find --show "${DATA_DIR}/meta") 35 | 36 | # Define thin-pool parameters. 37 | # See https://www.kernel.org/doc/Documentation/device-mapper/thin-provisioning.txt for details. 38 | SECTOR_SIZE=512 39 | DATA_SIZE="$(blockdev --getsize64 -q ${DATA_DEV})" 40 | LENGTH_IN_SECTORS=$(bc <<<"${DATA_SIZE}/${SECTOR_SIZE}") 41 | DATA_BLOCK_SIZE=128 42 | LOW_WATER_MARK=32768 43 | 44 | # Create a thin-pool device 45 | dmsetup create "${POOL_NAME}" \ 46 | --table "0 ${LENGTH_IN_SECTORS} thin-pool ${META_DEV} ${DATA_DEV} ${DATA_BLOCK_SIZE} ${LOW_WATER_MARK}" 47 | -------------------------------------------------------------------------------- /script/dm_reload.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Devmapper reload script 3 | After=network.target 4 | 5 | [Service] 6 | Type=simple 7 | ExecStart=/usr/local/bin/scripts/dm_reload.sh 8 | User=root 9 | 10 | [Install] 11 | WantedBy=multi-user.target 12 | -------------------------------------------------------------------------------- /script/dm_reload.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright (c) 2023-2025, Nubificus LTD 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | 17 | set -ex 18 | 19 | DATA_DIR=/var/lib/containerd/io.containerd.snapshotter.v1.devmapper 20 | POOL_NAME=containerd-pool 21 | 22 | # Allocate loop devices 23 | DATA_DEV=$(losetup --find --show "${DATA_DIR}/data") 24 | META_DEV=$(losetup --find --show "${DATA_DIR}/meta") 25 | 26 | # Define thin-pool parameters. 27 | # See https://www.kernel.org/doc/Documentation/device-mapper/thin-provisioning.txt for details. 28 | SECTOR_SIZE=512 29 | DATA_SIZE="$(blockdev --getsize64 -q ${DATA_DEV})" 30 | LENGTH_IN_SECTORS=$(bc <<<"${DATA_SIZE}/${SECTOR_SIZE}") 31 | DATA_BLOCK_SIZE=128 32 | LOW_WATER_MARK=32768 33 | 34 | # Create a thin-pool device 35 | dmsetup create "${POOL_NAME}" \ 36 | --table "0 ${LENGTH_IN_SECTORS} thin-pool ${META_DEV} ${DATA_DEV} ${DATA_BLOCK_SIZE} ${LOW_WATER_MARK}" 37 | systemctl restart containerd.service 38 | -------------------------------------------------------------------------------- /script/performance/measure.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023-2025, Nubificus LTD 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from __modules__ import * 16 | from sys import argv 17 | from time import sleep 18 | from pprint import pprint 19 | 20 | LOGFILE = "/tmp/urunc.zlog" 21 | DELAY = 2 22 | 23 | 24 | def main(): 25 | if len(argv) != 2: 26 | print("Error: Iterations not specified!") 27 | print("") 28 | print("Usage:") 29 | print(f"\t{argv[0]} ") 30 | exit(1) 31 | iterations = int(argv[1]) 32 | myprint(f"Collecting timestamps for {iterations} iterations") 33 | sleep(2) 34 | emptyFile(filename=LOGFILE) 35 | containerIDs = [] 36 | for i in range(iterations): 37 | myprint(f"Running iteration {i+1} of {iterations}") 38 | containerID = spawnContainer() 39 | containerIDs.append(containerID) 40 | sleep(DELAY) 41 | success = deleteContainer() 42 | if not success: 43 | print("Error removing container.") 44 | exit(1) 45 | myprint("Done") 46 | timestampDiffs = {} 47 | for containerID in containerIDs: 48 | data = parseSingleContainerTimestamps( 49 | filename=LOGFILE, containerID=containerID) 50 | series = TimestampSeries(data=data) 51 | diffs = series.diffs 52 | for key in diffs: 53 | value = diffs[key] 54 | if key in timestampDiffs: 55 | timestampDiffs[key].append(value) 56 | else: 57 | timestampDiffs[key] = [value] 58 | result = {} 59 | for key in timestampDiffs: 60 | value = timestampDiffs[key] 61 | current = timestampDiffs[key] 62 | durations = [c.duration for c in current] 63 | max_duration = f"{max(durations)} ns" 64 | min_duration = f"{min(durations)} ns" 65 | avg_duration = f"{int(sum(durations)/len(durations))} ns" 66 | result[key] = {"maximum": max_duration, 67 | "minimum": min_duration, "average": avg_duration} 68 | pprint(result) 69 | # emptyFile(filename=LOGFILE) 70 | 71 | 72 | if __name__ == "__main__": 73 | main() 74 | -------------------------------------------------------------------------------- /script/performance/measure_single.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023-2025, Nubificus LTD 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from __modules__ import * 16 | from sys import argv 17 | 18 | LOGFILE = "/tmp/urunc.zlog" 19 | 20 | 21 | def main(): 22 | if len(argv) != 2: 23 | print("Error: Container ID not specified!") 24 | print("") 25 | print("Usage:") 26 | print(f"\t{argv[0]} ") 27 | exit(1) 28 | containerID = argv[1] 29 | data = parseSingleContainerTimestamps( 30 | filename=LOGFILE, containerID=containerID) 31 | series = TimestampSeries(data=data) 32 | print(series.report) 33 | 34 | 35 | if __name__ == "__main__": 36 | main() 37 | -------------------------------------------------------------------------------- /script/performance/measure_to_json.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023-2025, Nubificus LTD 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from __modules__ import * 16 | from sys import argv 17 | from time import sleep 18 | from pprint import pprint 19 | 20 | LOGFILE = "/tmp/urunc.zlog" 21 | DELAY = 2 22 | 23 | 24 | def main(): 25 | if len(argv) != 3: 26 | print("Error: Iterations or output file not specified!") 27 | print("") 28 | print("Usage:") 29 | print(f"\t{argv[0]} ") 30 | exit(1) 31 | iterations = int(argv[1]) 32 | outputFile = argv[2] 33 | myprint(f"Collecting timestamps for {iterations} iterations") 34 | sleep(2) 35 | emptyFile(filename=LOGFILE) 36 | containerIDs = [] 37 | for i in range(iterations): 38 | myprint(f"Running iteration {i+1} of {iterations}") 39 | containerID = spawnContainer() 40 | containerIDs.append(containerID) 41 | sleep(DELAY) 42 | success = deleteContainer() 43 | if not success: 44 | print("Error removing container.") 45 | exit(1) 46 | myprint("Done") 47 | timestampDiffs = {} 48 | for containerID in containerIDs: 49 | data = parseSingleContainerTimestamps( 50 | filename=LOGFILE, containerID=containerID) 51 | series = TimestampSeries(data=data) 52 | diffs = series.diffs 53 | for key in diffs: 54 | value = diffs[key] 55 | if key in timestampDiffs: 56 | timestampDiffs[key].append(value) 57 | else: 58 | timestampDiffs[key] = [value] 59 | result = {} 60 | for key in timestampDiffs: 61 | value = timestampDiffs[key] 62 | current = timestampDiffs[key] 63 | durations = [c.duration for c in current] 64 | max_duration = f"{max(durations)} ns" 65 | min_duration = f"{min(durations)} ns" 66 | avg_duration = f"{int(sum(durations)/len(durations))} ns" 67 | result[key] = {"maximum": max_duration, 68 | "minimum": min_duration, "average": avg_duration} 69 | saveToJsonFile(outputFile, result) 70 | emptyFile(filename=LOGFILE) 71 | 72 | # nerdctl run --name redis-test -d --snapshotter devmapper --runtime io.containerd.urunc.v2 harbor.nbfc.io/nubificus/urunc/redis-hvt-rumprun:latest 73 | # nerdctl rm --force redis-test 74 | 75 | # data = parseSingleContainerTimestamps( 76 | # filename=LOGFILE, containerID=containerID) 77 | # series = TimestampSeries(data=data) 78 | # print(series.report) 79 | 80 | 81 | if __name__ == "__main__": 82 | main() 83 | -------------------------------------------------------------------------------- /tests/benchmarks/benchmark_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2025, Nubificus LTD 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "fmt" 19 | "testing" 20 | 21 | "github.com/nubificus/urunc/internal/constants" 22 | m "github.com/nubificus/urunc/internal/metrics" 23 | ) 24 | 25 | func BenchmarkZerologWriter(b *testing.B) { 26 | var zerologWriter = m.NewZerologMetrics(constants.TimestampTargetFile) 27 | for i := 0; i < b.N; i++ { 28 | for j := 0; j < 20; j++ { 29 | zerologWriter.Capture(fmt.Sprintf("container%02d", i), fmt.Sprintf("TS%02d", j)) 30 | } 31 | } 32 | } 33 | 34 | func BenchmarkMockWriter(b *testing.B) { 35 | var mockWriter = m.NewMockMetrics(constants.TimestampTargetFile) 36 | for i := 0; i < b.N; i++ { 37 | for j := 0; j < 20; j++ { 38 | mockWriter.Capture(fmt.Sprintf("container%02d", i), fmt.Sprintf("TS%02d", j)) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/e2e/ctr.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2025, Nubificus LTD 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package urunce2etesting 16 | 17 | import ( 18 | "fmt" 19 | ) 20 | 21 | const ctrName = "ctr" 22 | 23 | type ctrInfo struct { 24 | testArgs containerTestArgs 25 | containerID string 26 | detached bool 27 | } 28 | 29 | func newCtrTool(args containerTestArgs) *ctrInfo { 30 | return &ctrInfo{ 31 | testArgs: args, 32 | containerID: "", 33 | detached: false, 34 | } 35 | } 36 | 37 | func ctrNewContainerCmd(a containerTestArgs) string { 38 | cmdBase := "--runtime io.containerd.urunc.v2 " 39 | if a.Devmapper { 40 | cmdBase += "--snapshotter devmapper " 41 | } 42 | if a.Seccomp { 43 | cmdBase += "--seccomp " 44 | } 45 | if a.Memory != "" { 46 | cmdBase += fmt.Sprintf("--memory-limit %s ", a.Memory) 47 | } 48 | // ctr does not seem to support additional groups. 49 | // Therefore only set the UID/GID 50 | if a.UID != 0 && a.GID != 0 { 51 | cmdBase += fmt.Sprintf("-u %d:%d ", a.UID, a.GID) 52 | } 53 | cmdBase += a.Image + " " 54 | cmdBase += a.Name + " " 55 | cmdBase += a.Cli 56 | return cmdBase 57 | } 58 | 59 | func (i *ctrInfo) Name() string { 60 | return ctrName 61 | } 62 | 63 | func (i *ctrInfo) getTestArgs() containerTestArgs { 64 | return i.testArgs 65 | } 66 | 67 | func (i *ctrInfo) getPodID() string { 68 | // Not supported by ctr 69 | return "" 70 | } 71 | 72 | func (i *ctrInfo) getContainerID() string { 73 | return i.containerID 74 | } 75 | 76 | func (i *ctrInfo) setPodID(string) { 77 | // Not supported by ctr 78 | } 79 | 80 | func (i *ctrInfo) setContainerID(cID string) { 81 | i.containerID = cID 82 | } 83 | 84 | func (i *ctrInfo) pullImage() error { 85 | return commonPull(ctrName, i.testArgs.Image) 86 | } 87 | 88 | func (i *ctrInfo) rmImage() error { 89 | return commonRmImage(ctrName, i.testArgs.Image) 90 | } 91 | 92 | func (i *ctrInfo) createPod() (string, error) { 93 | // Not supported by ctr 94 | return "", errToolDoesNotSupport 95 | } 96 | 97 | func (i *ctrInfo) createContainer() (string, error) { 98 | cmdBase := ctrName 99 | cmdBase += " c create " 100 | cmdBase += ctrNewContainerCmd(i.testArgs) 101 | return commonCmdExec(cmdBase) 102 | } 103 | 104 | // nolint:unused 105 | func (i *ctrInfo) startPod() (string, error) { 106 | // Not supported by ctr 107 | return "", errToolDoesNotSupport 108 | } 109 | 110 | func (i *ctrInfo) startContainer(detach bool) (string, error) { 111 | if detach { 112 | i.detached = true 113 | } 114 | return commonStart(ctrName+" t", i.containerID, detach) 115 | } 116 | 117 | func (i *ctrInfo) runContainer(detach bool) (string, error) { 118 | cmdBase := ctrName 119 | cmdBase += " run " 120 | if detach { 121 | cmdBase += "-d " 122 | i.detached = true 123 | } 124 | cmdBase += ctrNewContainerCmd(i.testArgs) 125 | return commonCmdExec(cmdBase) 126 | } 127 | 128 | func (i *ctrInfo) stopContainer() error { 129 | if !i.detached { 130 | return nil 131 | } 132 | cmdBase := ctrName 133 | cmdBase += " t kill " 134 | cmdBase += i.containerID 135 | output, err := commonCmdExec(cmdBase) 136 | err = checkExpectedOut("", output, err) 137 | if err != nil { 138 | return fmt.Errorf("Failed to stop %s: %v", i.containerID, err) 139 | } 140 | return nil 141 | } 142 | 143 | func (i *ctrInfo) stopPod() error { 144 | // Not supported by ctr 145 | return errToolDoesNotSupport 146 | } 147 | 148 | func (i *ctrInfo) rmContainer() error { 149 | output, err := commonRmContainer(ctrName+" c", i.containerID) 150 | err = checkExpectedOut("", output, err) 151 | if err != nil { 152 | return fmt.Errorf("Failed to remove %s: %v", i.containerID, err) 153 | } 154 | return nil 155 | } 156 | 157 | func (i *ctrInfo) rmPod() error { 158 | // Not supported by ctr 159 | return errToolDoesNotSupport 160 | } 161 | 162 | func (i *ctrInfo) logContainer() (string, error) { 163 | // Not supported by ctr 164 | // TODO: We need to fix this 165 | // One idea would be to use a go routine for the running container 166 | // and channels for redirecting the output and getting the logs 167 | return "", errToolDoesNotSupport 168 | } 169 | 170 | func (i *ctrInfo) searchContainer(cID string) (bool, error) { 171 | cmd := ctrName + " c ls -q" 172 | 173 | output, err := commonCmdExec(cmd) 174 | if err != nil { 175 | return true, err 176 | } 177 | return searchCID(output, cID), nil 178 | } 179 | 180 | func (i *ctrInfo) searchPod(string) (bool, error) { 181 | // Not supported by ctr 182 | return true, errToolDoesNotSupport 183 | } 184 | 185 | func (i *ctrInfo) inspectCAndGet(key string) (string, error) { 186 | cmdBase := ctrName 187 | cmdBase += " c info " 188 | cmdBase += i.containerID 189 | output, err := commonCmdExec(cmdBase) 190 | if err != nil { 191 | return "", err 192 | } 193 | 194 | return findValOfKey(output, key) 195 | } 196 | 197 | func (i *ctrInfo) inspectPAndGet(string) (string, error) { 198 | // Not supported by ctr 199 | return "", errToolDoesNotSupport 200 | } 201 | -------------------------------------------------------------------------------- /tests/e2e/docker.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2025, Nubificus LTD 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package urunce2etesting 16 | 17 | import ( 18 | "fmt" 19 | ) 20 | 21 | const dockerName = "docker" 22 | 23 | type dockerInfo struct { 24 | testArgs containerTestArgs 25 | containerID string 26 | } 27 | 28 | func newDockerTool(args containerTestArgs) *dockerInfo { 29 | return &dockerInfo{ 30 | testArgs: args, 31 | containerID: "", 32 | } 33 | } 34 | 35 | func (i *dockerInfo) Name() string { 36 | return dockerName 37 | } 38 | 39 | func (i *dockerInfo) getTestArgs() containerTestArgs { 40 | return i.testArgs 41 | } 42 | 43 | func (i *dockerInfo) getPodID() string { 44 | // Not supported by docker 45 | return "" 46 | } 47 | 48 | func (i *dockerInfo) getContainerID() string { 49 | return i.containerID 50 | } 51 | 52 | func (i *dockerInfo) setPodID(string) { 53 | // Not supported by docker 54 | } 55 | 56 | func (i *dockerInfo) setContainerID(cID string) { 57 | i.containerID = cID 58 | } 59 | 60 | func (i *dockerInfo) pullImage() error { 61 | return commonPull(dockerName, i.testArgs.Image) 62 | } 63 | 64 | func (i *dockerInfo) rmImage() error { 65 | return commonRmImage(dockerName, i.testArgs.Image) 66 | } 67 | 68 | func (i *dockerInfo) createPod() (string, error) { 69 | // Not supported by docker 70 | return "", errToolDoesNotSupport 71 | } 72 | 73 | func (i *dockerInfo) createContainer() (string, error) { 74 | return commonCreate(dockerName, i.testArgs) 75 | } 76 | 77 | // nolint:unused 78 | func (i *dockerInfo) startPod() (string, error) { 79 | // Not supported by docker 80 | return "", errToolDoesNotSupport 81 | } 82 | 83 | func (i *dockerInfo) startContainer(detach bool) (string, error) { 84 | return commonStart(dockerName, i.containerID, detach) 85 | } 86 | 87 | func (i *dockerInfo) runContainer(detach bool) (string, error) { 88 | return commonRun(dockerName, i.testArgs, detach) 89 | } 90 | 91 | func (i *dockerInfo) stopContainer() error { 92 | output, err := commonStopContainer(dockerName, i.containerID) 93 | err = checkExpectedOut(i.containerID, output, err) 94 | if err != nil { 95 | return fmt.Errorf("Failed to stop %s: %v", i.containerID, err) 96 | } 97 | return nil 98 | } 99 | 100 | func (i *dockerInfo) stopPod() error { 101 | // Not supported by docker 102 | return errToolDoesNotSupport 103 | } 104 | 105 | func (i *dockerInfo) rmContainer() error { 106 | output, err := commonRmContainer(dockerName, i.containerID) 107 | err = checkExpectedOut(i.containerID, output, err) 108 | if err != nil { 109 | return fmt.Errorf("Failed to stop %s: %v", i.containerID, err) 110 | } 111 | return nil 112 | } 113 | 114 | func (i *dockerInfo) rmPod() error { 115 | // Not supported by docker 116 | return errToolDoesNotSupport 117 | } 118 | 119 | func (i *dockerInfo) logContainer() (string, error) { 120 | return commonLogs(dockerName, i.containerID) 121 | } 122 | 123 | func (i *dockerInfo) searchContainer(cID string) (bool, error) { 124 | return commonSearchContainer(dockerName, cID) 125 | } 126 | 127 | func (i *dockerInfo) searchPod(string) (bool, error) { 128 | // Not supported by docker 129 | return true, errToolDoesNotSupport 130 | } 131 | 132 | func (i *dockerInfo) inspectCAndGet(key string) (string, error) { 133 | return commonInspectCAndGet(dockerName, i.containerID, key) 134 | } 135 | 136 | func (i *dockerInfo) inspectPAndGet(string) (string, error) { 137 | // Not supported by docker 138 | return "", errToolDoesNotSupport 139 | } 140 | -------------------------------------------------------------------------------- /tests/e2e/nerdctl.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2025, Nubificus LTD 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package urunce2etesting 16 | 17 | import ( 18 | "fmt" 19 | ) 20 | 21 | const nerdctlName = "nerdctl" 22 | 23 | type nerdctlInfo struct { 24 | testArgs containerTestArgs 25 | containerID string 26 | } 27 | 28 | func newNerdctlTool(args containerTestArgs) *nerdctlInfo { 29 | return &nerdctlInfo{ 30 | testArgs: args, 31 | containerID: "", 32 | } 33 | } 34 | 35 | func (i *nerdctlInfo) Name() string { 36 | return nerdctlName 37 | } 38 | 39 | func (i *nerdctlInfo) getTestArgs() containerTestArgs { 40 | return i.testArgs 41 | } 42 | 43 | func (i *nerdctlInfo) getPodID() string { 44 | // Not supported by nerdctl 45 | return "" 46 | } 47 | 48 | func (i *nerdctlInfo) getContainerID() string { 49 | return i.containerID 50 | } 51 | 52 | func (i *nerdctlInfo) setPodID(string) { 53 | // Not supported by nerdctl 54 | } 55 | 56 | func (i *nerdctlInfo) setContainerID(cID string) { 57 | i.containerID = cID 58 | } 59 | 60 | func (i *nerdctlInfo) pullImage() error { 61 | return commonPull(nerdctlName, i.testArgs.Image) 62 | } 63 | 64 | func (i *nerdctlInfo) rmImage() error { 65 | return commonRmImage(nerdctlName, i.testArgs.Image) 66 | } 67 | 68 | func (i *nerdctlInfo) createPod() (string, error) { 69 | // Not supported by nerdctl 70 | return "", errToolDoesNotSupport 71 | } 72 | 73 | func (i *nerdctlInfo) createContainer() (string, error) { 74 | return commonCreate(nerdctlName, i.testArgs) 75 | } 76 | 77 | // nolint:unused 78 | func (i *nerdctlInfo) startPod() (string, error) { 79 | // Not supported by nerdctl 80 | return "", errToolDoesNotSupport 81 | } 82 | 83 | func (i *nerdctlInfo) startContainer(detach bool) (string, error) { 84 | return commonStart(nerdctlName, i.containerID, detach) 85 | } 86 | 87 | func (i *nerdctlInfo) runContainer(detach bool) (string, error) { 88 | return commonRun(nerdctlName, i.testArgs, detach) 89 | } 90 | 91 | func (i *nerdctlInfo) stopContainer() error { 92 | output, err := commonStopContainer(nerdctlName, i.containerID) 93 | err = checkExpectedOut(i.containerID, output, err) 94 | if err != nil { 95 | return fmt.Errorf("Failed to stop %s: %v", i.containerID, err) 96 | } 97 | return nil 98 | } 99 | 100 | func (i *nerdctlInfo) stopPod() error { 101 | // Not supported by nerdctl 102 | return errToolDoesNotSupport 103 | } 104 | 105 | func (i *nerdctlInfo) rmContainer() error { 106 | output, err := commonRmContainer(nerdctlName, i.containerID) 107 | err = checkExpectedOut(i.containerID, output, err) 108 | if err != nil { 109 | return fmt.Errorf("Failed to stop %s: %v", i.containerID, err) 110 | } 111 | return nil 112 | } 113 | 114 | func (i *nerdctlInfo) rmPod() error { 115 | // Not supported by nerdctl 116 | return errToolDoesNotSupport 117 | } 118 | 119 | func (i *nerdctlInfo) logContainer() (string, error) { 120 | return commonLogs(nerdctlName, i.containerID) 121 | } 122 | 123 | func (i *nerdctlInfo) searchContainer(cID string) (bool, error) { 124 | return commonSearchContainer(nerdctlName, cID) 125 | } 126 | 127 | func (i *nerdctlInfo) searchPod(string) (bool, error) { 128 | // Not supported by nerdctl 129 | return true, errToolDoesNotSupport 130 | } 131 | 132 | func (i *nerdctlInfo) inspectCAndGet(key string) (string, error) { 133 | return commonInspectCAndGet(nerdctlName, i.containerID, key) 134 | } 135 | 136 | func (i *nerdctlInfo) inspectPAndGet(string) (string, error) { 137 | // Not supported by nerdctl 138 | return "", errToolDoesNotSupport 139 | } 140 | -------------------------------------------------------------------------------- /tests/e2e/tests_skeleton.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2025, Nubificus LTD 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package urunce2etesting 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | "strings" 21 | "testing" 22 | "time" 23 | ) 24 | 25 | type testMethod func(tool testTool) error 26 | 27 | type containerTestArgs struct { 28 | Name string 29 | Image string 30 | Devmapper bool 31 | Seccomp bool 32 | UID int 33 | GID int 34 | Groups []int64 35 | Memory string 36 | Cli string 37 | StaticNet bool 38 | SideContainers []string 39 | Skippable bool 40 | TestFunc testMethod 41 | ExpectOut string 42 | } 43 | 44 | func runTest(tool testTool, t *testing.T) { 45 | cwd, err := os.Getwd() 46 | if err != nil { 47 | t.Fatalf("Could not get CWD: %v", err) 48 | } 49 | testDir := t.TempDir() 50 | err = os.Chdir(testDir) 51 | if err != nil { 52 | t.Fatalf("Could not change directory to %s: %v", testDir, err) 53 | } 54 | t.Cleanup(func() { 55 | err = os.Chdir(cwd) 56 | if err != nil { 57 | t.Errorf("Could not switch back to %s: %v", cwd, err) 58 | } 59 | 60 | }) 61 | cntrArgs := tool.getTestArgs() 62 | err = tool.pullImage() 63 | if err != nil { 64 | t.Fatalf("Failed to pull container image: %s - %v", cntrArgs.Image, err) 65 | } 66 | t.Cleanup(func() { 67 | err = tool.rmImage() 68 | if err != nil { 69 | t.Errorf("Failed to remove container image: %s - %v", cntrArgs.Image, err) 70 | } 71 | 72 | }) 73 | if cntrArgs.TestFunc == nil { 74 | if tool.Name() == "crictl" { 75 | // TODO: Add support for matchTest in crictl 76 | t.Fatalf("Crictl does not support matchTest") 77 | } 78 | output, err := tool.runContainer(false) 79 | if err != nil { 80 | t.Fatalf("Failed to run unikernel container: %s -- %v", output, err) 81 | } 82 | tool.setContainerID(cntrArgs.Name) 83 | if !strings.Contains(string(output), cntrArgs.ExpectOut) { 84 | t.Fatalf("Expected: %s, Got: %s", cntrArgs.ExpectOut, output) 85 | } 86 | err = testCleanup(tool) 87 | if err != nil { 88 | t.Errorf("Cleaning up: %v", err) 89 | } 90 | return 91 | } 92 | podID, err := tool.createPod() 93 | if err != nil && err != errToolDoesNotSupport { 94 | t.Fatalf("Failed to create Pod: %s - %v", podID, err) 95 | } 96 | tool.setPodID(podID) 97 | t.Cleanup(func() { 98 | err = tool.stopPod() 99 | if err != nil && err != errToolDoesNotSupport { 100 | t.Errorf("Failed to stop pod: %s - %v", podID, err) 101 | } 102 | 103 | err = tool.rmPod() 104 | if err != nil && err != errToolDoesNotSupport { 105 | t.Errorf("Failed to remove pod: %s - %v", podID, err) 106 | } 107 | }) 108 | cID, err := tool.createContainer() 109 | if err != nil { 110 | t.Fatalf("Failed to create container: %s - %v", cID, err) 111 | } 112 | tool.setContainerID(cID) 113 | t.Cleanup(func() { 114 | err = tool.rmContainer() 115 | if err != nil { 116 | t.Errorf("Failed to remove container: %s - %v", cntrArgs.Image, err) 117 | } 118 | err = testVerifyRm(tool) 119 | if err != nil { 120 | t.Errorf("Failed to verify container removal: %s - %v", cntrArgs.Image, err) 121 | } 122 | 123 | }) 124 | output, err := tool.startContainer(true) 125 | if err != nil { 126 | t.Fatalf("Failed to start unikernel container: %s - %v", output, err) 127 | } 128 | t.Cleanup(func() { 129 | err = tool.stopContainer() 130 | if err != nil { 131 | t.Errorf("Failed to stop container: %s - %v", cntrArgs.Image, err) 132 | } 133 | 134 | }) 135 | // Give some time till the unikernel is up and running. 136 | // Maybe we need to revisit this in the future. 137 | // MirageOS in Qemu(nested) needs more time to be responsive 138 | // in ping requests 139 | time.Sleep(2 * time.Second) 140 | err = cntrArgs.TestFunc(tool) 141 | if err != nil { 142 | t.Fatalf("Failed test: %v", err) 143 | } 144 | } 145 | 146 | func testVerifyRm(tool testTool) error { 147 | containerID := tool.getContainerID() 148 | exists, err := tool.searchContainer(containerID) 149 | if exists || err != nil { 150 | return fmt.Errorf("Container %s is not removed: %v", containerID, err) 151 | } 152 | err = verifyNoStaleFiles(containerID) 153 | if err != nil { 154 | return fmt.Errorf("Failed to remove all stale files: %v", err) 155 | } 156 | 157 | return nil 158 | } 159 | 160 | func testCleanup(tool testTool) error { 161 | err := tool.stopContainer() 162 | if err != nil { 163 | return fmt.Errorf("Failed to stop container: %v", err) 164 | } 165 | 166 | err = tool.rmContainer() 167 | if err != nil { 168 | return fmt.Errorf("Failed to remove container: %v", err) 169 | } 170 | 171 | return testVerifyRm(tool) 172 | } 173 | --------------------------------------------------------------------------------