COS Volume Plugin (binary)

July 24, 2026 · View on GitHub

Plugin type: binary (each Hook forks a child process; one line of JSON on stdout)
Driver name: cos

Shared COS docs (layout, deps, credentials): ../README.md
rpc example: ../rpc/
Framework: docs/guide/volume-plugin.md

中文文档:README.zh.md


Before you start

StepContent
1../README.md — Prerequisites
2Cubelet node: cosfs + jq — §1
3CubeMaster node: coscmd + jq — §1
4Deploy cube-volume-cos.sh + volume-cos.conf§2
5Configure CubeMaster + Cubelet — §3–§5
6SDK verification — §6–§7

Dependencies: coscmd + jq on CubeMaster; cosfs + jq on Cubelet. No COS Go SDK (see rpc). Install: ../README.md §1.


How it works

(COS bucket layout and Hook semantics: ../README.md.)

The plugin uses cosfs (FUSE) to mount a COS bucket subpath as a local directory. Each volume is volumes/<volume_id>/, managed by its own cosfs process.

COS Bucket
└── volumes/
    ├── vol-aaa/          ← volume A (one cosfs process)
    │   ├── .keep
    │   └── data.bin
    └── vol-bbb/          ← volume B (another cosfs process)
        └── model.pt

Isolation: Each volume has its own cosfs process and mount point. Destroying sandbox A (unmount vol-aaa) does not affect vol-bbb used by sandbox B.

Lifecycle:

PhaseTriggerAction
createVolume.create()coscmd creates volumes/<id>/.keep on COS
attachsandbox createcosfs mounts BUCKET:/volumes/<id> on host; returns host_path
detachsandbox destroyLast reference: fusermount unmounts
destroyVolume.destroy()coscmd deletes volumes/<id>/ recursively

Deploy the plugin

1. Install the script

After one-click install, the binary plugin is at /usr/local/services/cubetoolbox/CubeMaster/plugin/cube-volume-cos (Controller) and /usr/local/services/cubetoolbox/Cubelet/plugin/cube-volume-cos (Node). Credentials: ../README.md §2.

Manual install from source (non one-click):

PREFIX=/usr/local/services/cubetoolbox
sudo install -m 0755 examples/volume/cos/binary/cube-volume-cos.sh \
  "$PREFIX/CubeMaster/plugin/cube-volume-cos"
sudo install -m 0755 examples/volume/cos/binary/cube-volume-cos.sh \
  "$PREFIX/Cubelet/plugin/cube-volume-cos"

Or download from GitHub:

PREFIX=/usr/local/services/cubetoolbox
curl -fsSL "https://raw.githubusercontent.com/TencentCloud/CubeSandbox/master/examples/volume/cos/binary/cube-volume-cos.sh" \
  -o /tmp/cube-volume-cos.sh
sudo install -m 0755 /tmp/cube-volume-cos.sh "$PREFIX/CubeMaster/plugin/cube-volume-cos"
sudo install -m 0755 /tmp/cube-volume-cos.sh "$PREFIX/Cubelet/plugin/cube-volume-cos"

2. Create config file

For Kubernetes / Terraform deployments, configure volume-cos.conf yourself using native cluster mechanisms. The steps below use one-click / bare-metal paths as examples.

Same directory as the plugin binary (one file per node; edit on each host when roles are split):

PREFIX=/usr/local/services/cubetoolbox
sudo install -m 0600 examples/volume/cos/volume-cos.conf.example \
  "$PREFIX/CubeMaster/plugin/volume-cos.conf"
sudo install -m 0600 examples/volume/cos/volume-cos.conf.example \
  "$PREFIX/Cubelet/plugin/volume-cos.conf"
sudo ${EDITOR:-vi} "$PREFIX/CubeMaster/plugin/volume-cos.conf"
sudo ${EDITOR:-vi} "$PREFIX/Cubelet/plugin/volume-cos.conf"

Or write manually (example fields):

PREFIX=/usr/local/services/cubetoolbox
sudo tee "$PREFIX/CubeMaster/plugin/volume-cos.conf" > /dev/null << 'EOF'
SECRET_ID=AKIDxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
SECRET_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
BUCKET=mybucket-1250000000
REGION=ap-guangzhou
# Mount path comes from Cubelet --volume-base-dir (default /data/cube-shared/volume/cos-<id>).
EOF
sudo chmod 600 "$PREFIX/CubeMaster/plugin/volume-cos.conf"
# On Cubelet node, edit $PREFIX/Cubelet/plugin/volume-cos.conf similarly
FieldDescriptionExample
SECRET_IDTencent Cloud API key IDAKIDxxx
SECRET_KEYAPI key secretxxxxx
BUCKETCOS bucket BucketName-APPIDmybucket-1250000000
REGIONRegionap-guangzhou

Mount path is passed as --volume-base-dir (volume_plugin_base_dir, default /data/cube-shared/volume). host_path must be under it, e.g. /data/cube-shared/volume/cos-<id>.

Security: Prefer a sub-account key with bucket read/write only.

CubeMaster / Cubelet config, restart, and verification: ../README.md §3–§5. Copy below for offline reading.

3. Configure Cubelet

Edit /usr/local/services/cubetoolbox/Cubelet/config/config.toml:

[[plugins."io.cubelet.internal.v1.storage".volume_plugins]]
  name        = "cos"
  type        = "binary"
  binary_path = "/usr/local/services/cubetoolbox/Cubelet/plugin/cube-volume-cos"

4. Configure CubeMaster

Edit /usr/local/services/cubetoolbox/CubeMaster/conf.yaml:

volume_plugins:
  - name: cos
    type: binary
    binary_path: /usr/local/services/cubetoolbox/CubeMaster/plugin/cube-volume-cos

5. Restart services

systemctl restart cube-sandbox-cubemaster
systemctl restart cube-sandbox-cubelet

Verify plugins loaded:

grep -aF '[volume] registered' /data/log/CubeMaster/cubemaster-req.log | tail -5
grep -aF '[plugin_volume] initialized' /data/log/Cubelet/Cubelet-req.log | tail -5

Usage

Examples use Python SDK cubesandbox ≥ 0.6.0. See Framework §2.1.

Create Volume

from cubesandbox import Volume

vol = Volume.create("my-vol")
# Same when first volume_plugins entry is cos:
# vol = Volume.create("my-vol", driver="cos")
print(vol.volume_id, vol.name, vol.token)

Creates volumes/my-vol/.keep on COS. Verify with coscmd:

source /usr/local/services/cubetoolbox/CubeMaster/plugin/volume-cos.conf
coscmd -b $BUCKET -r $REGION list volumes/my-vol/

Create sandbox with mount

from cubesandbox import Sandbox, Volume

sb = Sandbox.create(
    volume_mounts={"/mnt/data": vol},
)
try:
    sb.files.write("/mnt/data/hello.txt", "persisted in COS")
    print(sb.files.read("/mnt/data/hello.txt"))
finally:
    sb.kill()

In-sandbox /mnt/data maps to COS BUCKET:/volumes/my-vol/.

Verify mount (Cubelet mntns):

CPID=$(pgrep -f "cubelet --config" | head -1)
nsenter -t $CPID -m -- cat /proc/mounts | grep cosfs

Destroy sandbox (detach)

sb.kill()

When the last reference is released, cosfs exits and the mount is removed. COS data is not deleted.

Delete Volume

Volume.destroy(vol.volume_id)

Runs coscmd delete -r -f volumes/my-vol/irreversible.


Code walkthrough

The plugin is one Shell script implementing CubeMaster (create/destroy) and Cubelet (attach/detach) hooks. Below is what each step does (comments match the source).

Argument parsing

CubeMaster / Cubelet invoke the plugin with --op and flags; the script parses them and dispatches:

while [[ $# -gt 0 ]]; do
    case "\$1" in
        --op)           OP="\$2";           shift 2 ;;  # which hook: create|destroy|attach|detach
        --volume-id)    VOLUME_ID="\$2";    shift 2 ;;  # volume identifier
        --sandbox-id)   SANDBOX_ID="\$2";   shift 2 ;;  # sandbox using the volume (attach/detach)
        --ref-count)    REF_COUNT="\$2";    shift 2 ;;  # sandboxes still using this volume on node
        --metadata)     METADATA="\$2";     shift 2 ;;  # JSON saved at attach (used on detach)
        # ...
    esac
done

create (control plane — user creates a volume)

Creates an empty folder for the volume in COS; no node mount yet.

do_create() {
    local volume_id="\$1" name="\$2"

    load_config                          # Step 1: read SECRET_ID/KEY, BUCKET, REGION
    cos_create_dir "$volume_id"          # Step 2: upload volumes/<id>/.keep via coscmd
    jq -cn --arg pd "volumes/${volume_id}/" \
        '{ token: "", private_data: $pd, error: "" }'  # Step 3: private_data → Attach
}

destroy (control plane — user deletes a volume)

Recursively deletes the volume folder from COS — irreversible.

do_destroy() {
    local volume_id="\$1"

    load_config                          # Step 1: read COS credentials
    cos_remove_dir "$volume_id"          # Step 2: coscmd delete -r volumes/<id>/
    ok_json                              # Step 3: return success
}

attach (data plane — sandbox mounts a volume)

Mounts the COS folder on the node with cosfs and returns host_path for Cubelet to bind-mount into the sandbox.

do_attach() {
    local sandbox_id="\$1" volume_id="\$2" ref_count="\$3"

    load_config                          # Step 1: read COS credentials
    ensure_passwd_file                   # Step 2: write /etc/cube/.passwd-cosfs for cosfs

    volume_lock_acquire "$volume_id"     # Step 3: flock — one attach at a time per volume
    trap 'volume_lock_release' EXIT

    cosfs_mount_volume "$volume_id"      # Step 4: mount BUCKET:/volumes/<id> (skip if up)

    local mnt="$(volume_mountpoint "$volume_id")"

    # Step 5: return host_path; Cubelet bind-mounts into sandbox
    jq -cn --arg path "$mnt" --arg vid "$volume_id" \
        '{ host_path: $path,
           metadata: { mount_dir: $path, volume_id: $vid }, error: "" }'
}

Idempotent attach: cosfs_mount_volume uses mountpoint -q; skips if already mounted. When RefCount > 0, sandboxes share the same host_path.

detach (data plane — sandbox unmounts a volume)

Unmounts cosfs only when no sandbox on this node still uses the volume (ref_count == 0). COS data remains until destroy.

do_detach() {
    local sandbox_id="\$1" volume_id="\$2" ref_count="\$3"

    if [[ "$ref_count" -gt 0 ]]; then     # Step 1: others still mounted — do nothing
        ok_json; return
    fi

    volume_lock_acquire "$volume_id"      # Step 2: flock before unmount
    trap 'volume_lock_release' EXIT

    # Step 3: path from attach metadata, or recompute from volume_id
    local mnt="$(printf '%s' "$metadata_json" | jq -r '.mount_dir // empty')"
    [[ -n "$mnt" ]] || mnt="$(volume_mountpoint "$volume_id")"
    cosfs_unmount_volume "$mnt"           # Step 4: fusermount — last user gone
    ok_json
}

cosfs passwd format

Format: BucketName-APPID:SecretId:SecretKey, mode 600:

ensure_passwd_file() {
    local content="${BUCKET}:${SECRET_ID}:${SECRET_KEY}"   # cosfs credential line
    printf '%s\n' "$content" > /etc/cube/.passwd-cosfs
    chmod 600 /etc/cube/.passwd-cosfs                      # cosfs rejects world-readable files
}

Verification and debugging

Manual attach

/usr/local/services/cubetoolbox/Cubelet/plugin/cube-volume-cos \
  --op attach \
  --sandbox-id test-sandbox-001 \
  --namespace default \
  --volume-id my-vol \
  --ref-count 0 \
  --volume-base-dir /data/cube-shared/volume

CPID=$(pgrep -f "cubelet --config" | head -1)
nsenter -t $CPID -m -- mountpoint /data/cube-shared/volume/cos-my-vol

Manual detach

/usr/local/services/cubetoolbox/Cubelet/plugin/cube-volume-cos \
  --op detach \
  --sandbox-id test-sandbox-001 \
  --namespace default \
  --volume-id my-vol \
  --ref-count 0 \
  --metadata '{"mount_dir":"/data/cube-shared/volume/cos-my-vol"}'

Plugin logs

Logs go to stderr; Cubelet forwards to journald:

journalctl -u cube-sandbox-cubelet --since "5 min ago" | grep cube-volume-cos

FAQ

cosfs mount not visible in host /proc/mounts?

Expected. cosfs runs in Cubelet mntns. Use nsenter as above.

libcrypto.so.1.1: cannot open shared object file

TencentOS 4.x uses OpenSSL 3; cosfs needs 1.1:

yum install -y compat-openssl11

coscmd: command not found

python3 -m venv /opt/coscmd-venv
/opt/coscmd-venv/bin/pip install coscmd
ln -sf /opt/coscmd-venv/bin/coscmd /usr/local/bin/coscmd

Duplicate entry 'vol-xxx' for key 'uniq_volume_id'

Volume still exists. Volume.destroy("<id>") first, then recreate.


References