diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a941d14b3..233696a98 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,7 +27,7 @@ jobs: run: sudo apt-get update && sudo apt-get install -y gcc-aarch64-linux-gnu gcc-arm-linux-gnueabihf - name: Install goreleaser - run: go install github.com/goreleaser/goreleaser/v2@latest + run: go install github.com/goreleaser/goreleaser/v2@v2.17.1 - name: Release env: diff --git a/CHANGELOG.md b/CHANGELOG.md index 339e0f964..6cb0663ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,62 @@ All notable changes to potctl / iofogctl are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [v3.9.0-rc.4] - 22-09-2026 + +### Added + +- Fleet Knowledge: `deploy -f`, attach/detach, get/describe/delete +- Microservice `spec.knowledge`; `deploy --patch-knowledge` (combinable with `--patch-model`) +- Describe agent knowledge fields (`knowledgeStatus`, `activeKnowledge`, `knowledgeLastUpdate`); `get all` knowledge table + +### Fixed + +- Describe agent `modelLastUpdate` treated as Unix milliseconds (same RFC3339 conversion as `lastStatusTime`) + +### Changed + +- Pin **iofog-go-sdk** to **v3.9.0-rc.6** +- Describe agent status uses a stable field order and prints host fog metrics (`systemCpus`, `systemTotalMemory`, `systemTotalDisk`, OS/kernel). `cpuUsage` is cores (100 = 1 core) for agent and microservice; `systemTotalCPU` stays host utilization %. + +## [v3.9.0-rc.3] - 20-09-2026 + +### Added + +- Microservice volume mapping `scope` (`private` default, or `shared`) when `type` is `volume`. + `deploy -f` accepts it; `describe` prints Controller’s value. Omit on write → Controller stores `private`. + +### Changed + +- Bump **iofog-go-sdk** to **v3.9.0-rc.4** +- Component pins: controller **3.9.0-rc.5**, edgelet binary **v1.1.0-rc.3** / image **1.1.0-rc.3** + +## [v3.9.0-rc.2] - 18-09-2026 + +### Added + +- `get` / `describe` microservice: print Controller `lastError`, `lastErrorAt`, and `restartCount`. `get` adds a compact crash summary when those fields are set; `describe` always prints them (`lastErrorAt` as a timestamp). Empty extras stay quiet on `get` for older Controllers. Distinct from current `errorMessage`. + +## [v3.9.0-rc.1] - 17-09-2026 + +### Breaking + +- Remove agent HAL/BLE fields `deviceScanFrequency`, `bluetoothEnabled`, and `abstractedHardwareEnabled`. Keep `edgeGuardFrequency`. +- Registry YAML: drop `isSecure`, `requiresCert`, and `certificate`. Use `type` (`oci` or `hf`), `ca`, and `insecure`. Email is optional. + +### Added + +- `deploy -f` for `kind: Model`, `RuntimeClass`, and `MicroserviceTemplate` +- `attach` / `detach` model and runtimeclass to agents by name +- `get` / `describe` / `delete` for model, runtimeclass, and microservice-template +- Microservice `spec.template` and `spec.models`; `deploy --patch-model` patches the catalog only (Controller decides rebuild) +- Describe microservice: models catalog, `podId`, and container 3.9 fields (`runAsGroup`, `entrypoint`, `tmpfs`, `devices`, …) +- Describe agent: parsed `runtimeClasses`, `availableCdiDevices`, `modelStatus`, `activeModels`, and `modelLastUpdate` +- `get all` includes a models table + +### Fixed + +- `get controllers` lists Running/Pending Kubernetes controller pods only (skips Failed, Succeeded, and terminating) and no longer persists those pods in namespace config + ## [v3.8.3-rc.2] — September 2026 ### Fixed diff --git a/assets/edgelet/scripts/install.sh b/assets/edgelet/scripts/install.sh index dd2daf93b..f28011505 100644 --- a/assets/edgelet/scripts/install.sh +++ b/assets/edgelet/scripts/install.sh @@ -139,22 +139,20 @@ if [ "$ACTION" = "rollback" ]; then else curl -fsSL -o "$_staged" "$_purl" || die "Failed to download rollback binary" fi - _old_embed="" - if [ "$OS" = "linux" ]; then - _old_embed=$(installed_embed_hash) - fi - [ "$OS" = "linux" ] && stop_edgelet_service "$INIT" - stop_edgelet_daemon_desktop "$OS" - install_binary_file "$_staged" "$BINARY_PATH" + replace_staged_edgelet_binary "$_staged" install_dirs_for_os "$OS" if [ "$FORCE_CONFIG" != true ] && [ -f "$_cfgbak" ]; then maybe_sudo install -m 640 "$_cfgbak" "$CONFIG_FILE" fi - record_restart_data_plane_decision "$_old_embed" _sha=$(sha256_file "$BINARY_PATH") - write_install_receipt "$EDGELET_VERSION" "$_pos" "$_parch" "$CONTAINER_ENGINE" "$_purl" "$_sha" "rollback" "$SCRIPT_DIR/bundled.sh" || true + stage_pending_install_receipt "$EDGELET_VERSION" "$_pos" "$_parch" "$CONTAINER_ENGINE" "$_purl" "$_sha" "rollback" + if [ "$SKIP_START" = true ]; then + info "Skipping daemon start (--skip-start); use start_edgelet.sh" + exit 0 + fi _run_post_install + commit_pending_install_receipt info "Rollback to ${EDGELET_VERSION} complete." exit 0 fi @@ -177,29 +175,23 @@ if [ "$ACTION" = "upgrade" ]; then _cfg_backup="${BACKUP_DIR}/config.yaml.$(date +%Y%m%d%H%M%S 2>/dev/null || date +%s)" cp "$CONFIG_FILE" "$_cfg_backup" 2>/dev/null || true cache_binary "$_cur_ver" "$_cur_os" "$_cur_arch" "$BINARY_PATH" - write_previous_release "$_cur_ver" "$_cur_os" "$_cur_arch" "$_cur_eng" "$_cur_src" "$_cur_sha" "$_cfg_backup" - _old_embed="" - if [ "$OS" = "linux" ]; then - _old_embed=$(installed_embed_hash) - fi - [ "$OS" = "linux" ] && stop_edgelet_service "$INIT" - stop_edgelet_daemon_desktop "$OS" _staged="${TMPDIR}/edgelet-bin" download_or_stage_binary "$_staged" verify_binary_checksum "$_staged" - install_binary_file "$_staged" "$BINARY_PATH" + replace_staged_edgelet_binary "$_staged" + write_previous_release "$_cur_ver" "$_cur_os" "$_cur_arch" "$_cur_eng" "$_cur_src" "$_cur_sha" "$_cfg_backup" install_dirs_for_os "$OS" _sha=$(sha256_file "$BINARY_PATH") _method="upgrade" [ "$AIRGAP" = true ] && _method="upgrade-airgap" - write_install_receipt "$EDGELET_VERSION" "$OS" "$ARCH" "$CONTAINER_ENGINE" "$(compute_source_url)" "$_sha" "$_method" "$SCRIPT_DIR/bundled.sh" || true - record_restart_data_plane_decision "$_old_embed" + stage_pending_install_receipt "$EDGELET_VERSION" "$OS" "$ARCH" "$CONTAINER_ENGINE" "$(compute_source_url)" "$_sha" "$_method" if [ "$SKIP_START" = true ]; then info "Skipping daemon start (--skip-start); use start_edgelet.sh" exit 0 fi _run_post_install + commit_pending_install_receipt info "Upgrade to ${EDGELET_VERSION} complete." exit 0 fi diff --git a/assets/edgelet/scripts/install_init_units.sh b/assets/edgelet/scripts/install_init_units.sh index 1859065c7..a44b88759 100755 --- a/assets/edgelet/scripts/install_init_units.sh +++ b/assets/edgelet/scripts/install_init_units.sh @@ -94,6 +94,7 @@ StartLimitBurst=5 Type=simple ExecStartPre=/bin/sh -c 'mountpoint -q /sys/fs/bpf || mount -t bpf bpf /sys/fs/bpf 2>/dev/null || true' ExecStart=/usr/local/bin/edgelet runtime-bootstrap +# Reap only after a verified data-plane drain (receipt under /run/edgelet). ExecStopPost=-/usr/local/bin/edgelet runtime reap-orphans Restart=always RestartSec=5s @@ -315,6 +316,7 @@ stop() { fi fi rm -f "${pidfile}" 2>/dev/null || true + # Reap only after a verified data-plane drain (receipt under /run/edgelet). /usr/local/bin/edgelet runtime reap-orphans || ewarn "orphan reap exited non-zero" eend 0 } diff --git a/assets/edgelet/scripts/lib/embed.sh b/assets/edgelet/scripts/lib/embed.sh index 6509d4bc0..6719ea9d5 100644 --- a/assets/edgelet/scripts/lib/embed.sh +++ b/assets/edgelet/scripts/lib/embed.sh @@ -1,5 +1,5 @@ #!/bin/sh -# Embed-hash helpers for conditional edgelet-containerd restart (upstream install.sh 9a9ded8 parity). +# Embed-hash helpers for fat OTA drain before binary replace. # Requires lib/common.sh (maybe_sudo, info) when sourced. restart_data_plane_marker_path() { @@ -42,11 +42,44 @@ consume_restart_data_plane_marker() { [ "$_requested" = true ] } -# installed_embed_hash returns the basename of data/current (embed SHA256 prefix), or empty. +edgelet_data_root() { + if [ -n "${EDGELET_DATA_DIR:-}" ]; then + echo "${EDGELET_DATA_DIR}" + return 0 + fi + echo "/var/lib/edgelet" +} + +# embed_bundle_ready is true when dir has an executable fat runtime and the +# same companion files the extract path requires before it will run. +embed_bundle_ready() { + _dir="$1" + [ -n "$_dir" ] || return 1 + _fat="${_dir}/bin/edgelet" + [ -f "$_fat" ] && [ -x "$_fat" ] || return 1 + _magic=$(od -An -tx1 -N 4 "$_fat" 2>/dev/null | tr -d ' \n' || true) + [ "$_magic" = "7f454c46" ] || return 1 + [ -f "${_dir}/bin/containerd-shim-runc-v2" ] || return 1 + [ -f "${_dir}/bin/aux/xtables-legacy-multi" ] || return 1 + [ -f "${_dir}/bin/ip" ] || return 1 + [ -f "${_dir}/bin/busybox" ] || return 1 + [ -L "${_dir}/bin/aux/iptables" ] || return 1 + [ "$(readlink "${_dir}/bin/aux/iptables" 2>/dev/null || true)" = "xtables-legacy-multi" ] +} + +# installed_embed_hash prints the ready data/current bundle hash, or nothing +# when the symlink is missing or the fat runtime is not ready. installed_embed_hash() { - _link="/var/lib/edgelet/data/current" + _link="$(edgelet_data_root)/data/current" [ -L "$_link" ] || return 0 - basename "$(readlink -f "$_link" 2>/dev/null || readlink "$_link")" + _target=$(readlink "$_link" 2>/dev/null || true) + [ -n "$_target" ] || return 0 + case "$_target" in + /*) ;; + *) _target="$(CDPATH= cd -- "$(dirname "$_link")" && pwd)/${_target}" ;; + esac + embed_bundle_ready "$_target" || return 0 + basename "$_target" } # binary_embed_hash reads embed hash from a linux thin binary (edgelet version --verbose). @@ -58,8 +91,9 @@ binary_embed_hash() { | head -1 } -# should_restart_data_plane is true when containerEngine=edgelet and the embed hash changed -# (or data/current is not yet set). Returns false for docker/podman or lite builds without embed. +# should_restart_data_plane is true when containerEngine=edgelet, the new embed +# hash is set, and the ready current bundle is missing or different. +# Returns false for docker/podman or a thin binary with no embed hash. should_restart_data_plane() { _eng="$1" _old="$2" @@ -70,6 +104,53 @@ should_restart_data_plane() { [ "$_old" != "$_new" ] } +# quiesce_data_plane_for_replace drains labeled workloads through CRI using the +# staged binary. Control stays up. It does not use edgelet.sock and does not +# fall back to the installed binary. Non-zero leaves that binary in place. +quiesce_data_plane_for_replace() { + _bin="$1" + if [ -z "$_bin" ] || [ ! -x "$_bin" ]; then + return 1 + fi + info "Draining data plane before embed replace" + "$_bin" --quiet runtime drain --direct +} + +# replace_staged_edgelet_binary installs a staged thin binary. +# Fat OTA drains and verifies while control is still up, stops the data plane, +# then replaces the binary. A matching ready hash, or docker/podman, stops +# control only and does not stop edgelet-containerd. +# Verify failure exits non-zero and does not replace the binary. +# Sets OTA_RESTART_DATA_PLANE=true or false and writes the start-layer marker. +replace_staged_edgelet_binary() { + _staged="$1" + OTA_RESTART_DATA_PLANE=false + [ -f "$_staged" ] || die "Staged binary missing: ${_staged}" + chmod 755 "$_staged" || die "Cannot prepare staged binary: ${_staged}" + if [ "$OS" = "linux" ]; then + _old_embed=$(installed_embed_hash) + _new_embed=$(binary_embed_hash "$_staged") + if should_restart_data_plane "$CONTAINER_ENGINE" "$_old_embed" "$_new_embed"; then + OTA_RESTART_DATA_PLANE=true + info "Embed hash: ${_old_embed:-} -> ${_new_embed}; data-plane restart required" + if quiesce_data_plane_for_replace "$_staged"; then + stop_edgelet_containerd_unit "$INIT" + elif ! edgelet_containerd_unit_active \ + && [ "$(binary_embed_hash "$BINARY_PATH")" = "$_new_embed" ]; then + info "Data plane already stopped after a previous verified drain; binary replace continues" + else + die "Data-plane drain did not verify; binary was not replaced" + fi + else + stop_edgelet_service "$INIT" + fi + else + stop_edgelet_daemon_desktop "$OS" + fi + install_binary_file "$_staged" "$BINARY_PATH" + write_restart_data_plane_marker "$OTA_RESTART_DATA_PLANE" +} + start_edgelet_containerd_unit() { _init="$1" _restart="$2" @@ -77,8 +158,7 @@ start_edgelet_containerd_unit() { systemd) maybe_sudo systemctl enable edgelet-containerd 2>/dev/null || true if [ "$_restart" = true ]; then - info "Embedded bundle hash changed; restarting edgelet-containerd (data plane)" - maybe_sudo systemctl stop edgelet-containerd 2>/dev/null || true + info "Embedded bundle hash changed; starting edgelet-containerd (data plane)" maybe_sudo systemctl reset-failed edgelet-containerd 2>/dev/null || true maybe_sudo systemctl start edgelet-containerd else @@ -88,8 +168,7 @@ start_edgelet_containerd_unit() { openrc) maybe_sudo rc-update add edgelet-containerd default 2>/dev/null || true if [ "$_restart" = true ]; then - info "Embedded bundle hash changed; restarting edgelet-containerd (data plane)" - maybe_sudo rc-service edgelet-containerd stop 2>/dev/null || true + info "Embedded bundle hash changed; starting edgelet-containerd (data plane)" maybe_sudo rc-service edgelet-containerd start 2>/dev/null || true else maybe_sudo rc-service edgelet-containerd start 2>/dev/null || true @@ -98,11 +177,15 @@ start_edgelet_containerd_unit() { esac } +# record_restart_data_plane_decision compares a ready embed hash to a binary. +# Pass the staged binary as $2. The default is the installed path, which is +# only valid before that path has been replaced. record_restart_data_plane_decision() { _old_embed="$1" + _bin="${2:-$BINARY_PATH}" _restart_dp=false if [ "$OS" = "linux" ]; then - _new_embed=$(binary_embed_hash "$BINARY_PATH") + _new_embed=$(binary_embed_hash "$_bin") if should_restart_data_plane "$CONTAINER_ENGINE" "$_old_embed" "$_new_embed"; then _restart_dp=true info "Embed hash: ${_old_embed:-} -> ${_new_embed}; data-plane restart required" diff --git a/assets/edgelet/scripts/lib/receipt.sh b/assets/edgelet/scripts/lib/receipt.sh index e7ca5ca59..ea587896f 100644 --- a/assets/edgelet/scripts/lib/receipt.sh +++ b/assets/edgelet/scripts/lib/receipt.sh @@ -19,14 +19,17 @@ kv_get() { echo "$_line" | sed "s/^${_key}=//" } -write_install_receipt() { - _ver="$1" - _os="$2" - _arch="$3" - _eng="$4" - _url="$5" - _sha="$6" - _method="$7" +PENDING_RECEIPT_FILE="${PENDING_RECEIPT_FILE:-${BACKUP_DIR}/pending-install-receipt}" + +_write_receipt_body() { + _dest="$1" + _ver="$2" + _os="$3" + _arch="$4" + _eng="$5" + _url="$6" + _sha="$7" + _method="$8" maybe_sudo mkdir -p "$BACKUP_DIR" _ts=$(date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || date -u) { @@ -38,8 +41,27 @@ write_install_receipt() { printf 'installed_at=%s\n' "$_ts" printf 'install_method=%s\n' "$_method" printf 'binary_sha256=%s\n' "$_sha" - } | maybe_sudo tee "$RECEIPT_FILE" >/dev/null + } | maybe_sudo tee "$_dest" >/dev/null + maybe_sudo chmod 600 "$_dest" 2>/dev/null || true +} + +write_install_receipt() { + _write_receipt_body "$RECEIPT_FILE" "$@" +} + +# stage_pending_install_receipt records a receipt that becomes current only +# after start_edgelet.sh brings the units back up. +stage_pending_install_receipt() { + _write_receipt_body "$PENDING_RECEIPT_FILE" "$@" +} + +commit_pending_install_receipt() { + [ -f "$PENDING_RECEIPT_FILE" ] || return 0 + maybe_sudo mkdir -p "$BACKUP_DIR" + maybe_sudo cp "$PENDING_RECEIPT_FILE" "$RECEIPT_FILE" maybe_sudo chmod 600 "$RECEIPT_FILE" 2>/dev/null || true + maybe_sudo rm -f "$PENDING_RECEIPT_FILE" + info "Install receipt written after services started" } write_previous_release() { diff --git a/assets/edgelet/scripts/lib/service.sh b/assets/edgelet/scripts/lib/service.sh index 30f5e36d4..abfee4dd4 100644 --- a/assets/edgelet/scripts/lib/service.sh +++ b/assets/edgelet/scripts/lib/service.sh @@ -1,6 +1,82 @@ #!/bin/sh # Init-system service stop/restart helpers (OTA parity with upstream edgelet install.sh). +# stop_edgelet_dataplane_processes stops runtime-bootstrap and its containerd +# child. It does not stop the control daemon. +stop_edgelet_dataplane_processes() { + _pids=$(pgrep -f '[e]dgelet runtime-bootstrap' 2>/dev/null || true) + _pids="${_pids} $(pgrep -f '[e]dgelet-containerd-child' 2>/dev/null || true)" + _pids=$(echo "${_pids}" | tr ' ' '\n' | awk 'NF && !seen[$0]++') + [ -n "${_pids}" ] || return 0 + for _p in ${_pids}; do + maybe_sudo kill -TERM "${_p}" 2>/dev/null || true + done + sleep 1 + for _p in ${_pids}; do + kill -0 "${_p}" 2>/dev/null || continue + maybe_sudo kill -KILL "${_p}" 2>/dev/null || true + done +} + +# stop_edgelet_containerd_unit stops the data plane on every init. +# Call it only after runtime drain --direct has verified. +stop_edgelet_containerd_unit() { + _init="${1:-${INIT_SYSTEM:-unknown}}" + info "Stopping edgelet-containerd (data plane)" + case "${_init}" in + systemd) + maybe_sudo systemctl stop edgelet-containerd 2>/dev/null || true + maybe_sudo systemctl reset-failed edgelet-containerd 2>/dev/null || true + ;; + openrc) + maybe_sudo rc-service edgelet-containerd stop 2>/dev/null || true + ;; + *) + stop_edgelet_dataplane_processes + ;; + esac +} + +start_edgelet_dataplane_process() { + if pgrep -f '[e]dgelet runtime-bootstrap' >/dev/null 2>&1; then + return 0 + fi + if pgrep -f '[e]dgelet-containerd-child' >/dev/null 2>&1; then + return 0 + fi + maybe_sudo mkdir -p /var/log/edgelet + maybe_sudo sh -c '/usr/local/bin/edgelet runtime-bootstrap >>/var/log/edgelet/containerd.log 2>&1 &' +} + +# start_edgelet_containerd_after_drain starts a data plane that install.sh +# already stopped after a verified drain. It does not stop or restart the unit. +start_edgelet_containerd_after_drain() { + _init="${1:-${INIT_SYSTEM:-unknown}}" + info "Starting edgelet-containerd after verified drain" + case "${_init}" in + systemd) + maybe_sudo systemctl enable edgelet-containerd 2>/dev/null || true + maybe_sudo systemctl reset-failed edgelet-containerd 2>/dev/null || true + maybe_sudo systemctl start edgelet-containerd + ;; + openrc) + maybe_sudo rc-update add edgelet-containerd default 2>/dev/null || true + maybe_sudo rc-service edgelet-containerd start + ;; + *) + start_edgelet_dataplane_process + ;; + esac + case "${_init}" in + systemd|openrc) + if [ "${EDGELET_START_NO_WAIT:-0}" = "1" ]; then + return 0 + fi + wait_edgelet_containerd_ready + ;; + esac +} + stop_edgelet_service() { _init="${1:-${INIT_SYSTEM:-unknown}}" case "${_init}" in @@ -182,15 +258,16 @@ restart_edgelet_daemon_service() { esac } -# restart_edgelet_services stops then starts edgelet (and edgelet-containerd when embedded). +# restart_edgelet_services starts the data plane when a verified fat OTA stopped +# it, then restarts control. Thin OTA and docker/podman restart control only. restart_edgelet_services() { _eng="${1:-${CONTAINER_ENGINE:-edgelet}}" if [ "$_eng" = "edgelet" ]; then if consume_containerd_restarted_marker; then info "edgelet-containerd already restarted; restarting edgelet only (OTA)" elif consume_restart_data_plane_marker; then - info "Restarting edgelet-containerd and edgelet (embedded bundle OTA)" - restart_edgelet_containerd_service + info "Starting edgelet-containerd after verified drain, then edgelet" + start_edgelet_containerd_after_drain "${INIT_SYSTEM:-unknown}" else info "Thin OTA (embed hash unchanged); restarting edgelet only" start_edgelet_containerd_unit "${INIT_SYSTEM:-unknown}" false diff --git a/assets/edgelet/scripts/start_edgelet.sh b/assets/edgelet/scripts/start_edgelet.sh index 7e04a5ae1..c66fc6bbd 100755 --- a/assets/edgelet/scripts/start_edgelet.sh +++ b/assets/edgelet/scripts/start_edgelet.sh @@ -5,6 +5,7 @@ set -x SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" . "$SCRIPT_DIR/lib/common.sh" +. "$SCRIPT_DIR/lib/receipt.sh" . "$SCRIPT_DIR/lib/container_engine.sh" . "$SCRIPT_DIR/lib/container_mounts.sh" . "$SCRIPT_DIR/lib/embed.sh" @@ -48,7 +49,7 @@ redeploy_native_linux() { if consume_containerd_restarted_marker; then info "edgelet-containerd already restarted; restarting edgelet only (redeploy)" elif consume_restart_data_plane_marker; then - restart_edgelet_containerd_service + start_edgelet_containerd_after_drain "$INIT_SYSTEM" else start_edgelet_containerd_unit "$INIT_SYSTEM" false fi @@ -63,7 +64,7 @@ redeploy_native_linux() { if consume_containerd_restarted_marker; then : elif consume_restart_data_plane_marker; then - restart_edgelet_containerd_service + start_edgelet_containerd_after_drain "$INIT_SYSTEM" else start_edgelet_containerd_unit "$INIT_SYSTEM" false fi @@ -279,3 +280,5 @@ case "$EDGELET_INSTALL_MODE" in exit 1 ;; esac + +commit_pending_install_receipt diff --git a/docs/iofogctl_md/iofogctl_attach.md b/docs/iofogctl_md/iofogctl_attach.md index 64737aa91..db7c5ff72 100644 --- a/docs/iofogctl_md/iofogctl_attach.md +++ b/docs/iofogctl_md/iofogctl_attach.md @@ -31,6 +31,9 @@ iofogctl attach * [iofogctl](iofogctl.md) - * [iofogctl attach agent](iofogctl_attach_agent.md) - Attach an Agent to an existing Namespace * [iofogctl attach exec](iofogctl_attach_exec.md) - Provision fog debug exec on an Agent +* [iofogctl attach knowledge](iofogctl_attach_knowledge.md) - Attach Knowledge to existing Agents +* [iofogctl attach model](iofogctl_attach_model.md) - Attach a Model to existing Agents +* [iofogctl attach runtimeclass](iofogctl_attach_runtimeclass.md) - Attach a RuntimeClass to existing Agents * [iofogctl attach volume-mount](iofogctl_attach_volume-mount.md) - Attach a Volume Mount to existing Agents diff --git a/docs/iofogctl_md/iofogctl_attach_knowledge.md b/docs/iofogctl_md/iofogctl_attach_knowledge.md new file mode 100644 index 000000000..7065bbb6c --- /dev/null +++ b/docs/iofogctl_md/iofogctl_attach_knowledge.md @@ -0,0 +1,37 @@ +## iofogctl attach knowledge + +Attach Knowledge to existing Agents + +### Synopsis + +Attach Knowledge to existing Agents. + +``` +iofogctl attach knowledge NAME AGENT_NAME1 AGENT_NAME2 [flags] +``` + +### Examples + +``` +iofogctl attach knowledge NAME AGENT_NAME1 AGENT_NAME2 +``` + +### Options + +``` + -h, --help help for knowledge +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of iofogctl +``` + +### SEE ALSO + +* [iofogctl attach](iofogctl_attach.md) - Attach one ioFog resource to another + + diff --git a/docs/iofogctl_md/iofogctl_attach_model.md b/docs/iofogctl_md/iofogctl_attach_model.md new file mode 100644 index 000000000..0ce49911d --- /dev/null +++ b/docs/iofogctl_md/iofogctl_attach_model.md @@ -0,0 +1,37 @@ +## iofogctl attach model + +Attach a Model to existing Agents + +### Synopsis + +Attach a Model to existing Agents. + +``` +iofogctl attach model NAME AGENT_NAME1 AGENT_NAME2 [flags] +``` + +### Examples + +``` +iofogctl attach model NAME AGENT_NAME1 AGENT_NAME2 +``` + +### Options + +``` + -h, --help help for model +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of iofogctl +``` + +### SEE ALSO + +* [iofogctl attach](iofogctl_attach.md) - Attach one ioFog resource to another + + diff --git a/docs/iofogctl_md/iofogctl_attach_runtimeclass.md b/docs/iofogctl_md/iofogctl_attach_runtimeclass.md new file mode 100644 index 000000000..d22083029 --- /dev/null +++ b/docs/iofogctl_md/iofogctl_attach_runtimeclass.md @@ -0,0 +1,37 @@ +## iofogctl attach runtimeclass + +Attach a RuntimeClass to existing Agents + +### Synopsis + +Attach a RuntimeClass to existing Agents. RuntimeClass can only be linked to edgelet agents. + +``` +iofogctl attach runtimeclass NAME AGENT_NAME1 AGENT_NAME2 [flags] +``` + +### Examples + +``` +iofogctl attach runtimeclass NAME AGENT_NAME1 AGENT_NAME2 +``` + +### Options + +``` + -h, --help help for runtimeclass +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of iofogctl +``` + +### SEE ALSO + +* [iofogctl attach](iofogctl_attach.md) - Attach one ioFog resource to another + + diff --git a/docs/iofogctl_md/iofogctl_delete.md b/docs/iofogctl_md/iofogctl_delete.md index 499ba5ee6..c46d4896c 100644 --- a/docs/iofogctl_md/iofogctl_delete.md +++ b/docs/iofogctl_md/iofogctl_delete.md @@ -38,13 +38,17 @@ iofogctl delete [flags] * [iofogctl delete certificate](iofogctl_delete_certificate.md) - Delete a Certificate * [iofogctl delete configmap](iofogctl_delete_configmap.md) - Delete a ConfigMap * [iofogctl delete controller](iofogctl_delete_controller.md) - Delete a Controller +* [iofogctl delete knowledge](iofogctl_delete_knowledge.md) - Delete Knowledge * [iofogctl delete microservice](iofogctl_delete_microservice.md) - Delete a Microservice +* [iofogctl delete microservice-template](iofogctl_delete_microservice-template.md) - Delete a Microservice Template +* [iofogctl delete model](iofogctl_delete_model.md) - Delete a Model * [iofogctl delete namespace](iofogctl_delete_namespace.md) - Delete a Namespace * [iofogctl delete nats-account-rule](iofogctl_delete_nats-account-rule.md) - Delete a NATS account rule * [iofogctl delete nats-user-rule](iofogctl_delete_nats-user-rule.md) - Delete a NATS user rule * [iofogctl delete registry](iofogctl_delete_registry.md) - Delete a Registry * [iofogctl delete role](iofogctl_delete_role.md) - Delete a Role * [iofogctl delete rolebinding](iofogctl_delete_rolebinding.md) - Delete a RoleBinding +* [iofogctl delete runtimeclass](iofogctl_delete_runtimeclass.md) - Delete a RuntimeClass * [iofogctl delete secret](iofogctl_delete_secret.md) - Delete a Secret * [iofogctl delete service](iofogctl_delete_service.md) - Delete a Service * [iofogctl delete serviceaccount](iofogctl_delete_serviceaccount.md) - Delete a ServiceAccount diff --git a/docs/iofogctl_md/iofogctl_delete_knowledge.md b/docs/iofogctl_md/iofogctl_delete_knowledge.md new file mode 100644 index 000000000..61577bf54 --- /dev/null +++ b/docs/iofogctl_md/iofogctl_delete_knowledge.md @@ -0,0 +1,38 @@ +## iofogctl delete knowledge + +Delete Knowledge + +### Synopsis + +Delete Knowledge from the Controller. + +``` +iofogctl delete knowledge NAME [flags] +``` + +### Examples + +``` +iofogctl delete knowledge NAME +``` + +### Options + +``` + -h, --help help for knowledge +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of iofogctl +``` + +### SEE ALSO + +* [iofogctl delete](iofogctl_delete.md) - Delete an existing ioFog resource + + diff --git a/docs/iofogctl_md/iofogctl_delete_microservice-template.md b/docs/iofogctl_md/iofogctl_delete_microservice-template.md new file mode 100644 index 000000000..f4bd8ba87 --- /dev/null +++ b/docs/iofogctl_md/iofogctl_delete_microservice-template.md @@ -0,0 +1,38 @@ +## iofogctl delete microservice-template + +Delete a Microservice Template + +### Synopsis + +Delete a Microservice Template from the Controller. + +``` +iofogctl delete microservice-template NAME [flags] +``` + +### Examples + +``` +iofogctl delete microservice-template NAME +``` + +### Options + +``` + -h, --help help for microservice-template +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of iofogctl +``` + +### SEE ALSO + +* [iofogctl delete](iofogctl_delete.md) - Delete an existing ioFog resource + + diff --git a/docs/iofogctl_md/iofogctl_delete_model.md b/docs/iofogctl_md/iofogctl_delete_model.md new file mode 100644 index 000000000..cb15eb78e --- /dev/null +++ b/docs/iofogctl_md/iofogctl_delete_model.md @@ -0,0 +1,38 @@ +## iofogctl delete model + +Delete a Model + +### Synopsis + +Delete a Model from the Controller. + +``` +iofogctl delete model NAME [flags] +``` + +### Examples + +``` +iofogctl delete model NAME +``` + +### Options + +``` + -h, --help help for model +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of iofogctl +``` + +### SEE ALSO + +* [iofogctl delete](iofogctl_delete.md) - Delete an existing ioFog resource + + diff --git a/docs/iofogctl_md/iofogctl_delete_runtimeclass.md b/docs/iofogctl_md/iofogctl_delete_runtimeclass.md new file mode 100644 index 000000000..1d4ba572a --- /dev/null +++ b/docs/iofogctl_md/iofogctl_delete_runtimeclass.md @@ -0,0 +1,38 @@ +## iofogctl delete runtimeclass + +Delete a RuntimeClass + +### Synopsis + +Delete a RuntimeClass from the Controller. + +``` +iofogctl delete runtimeclass NAME [flags] +``` + +### Examples + +``` +iofogctl delete runtimeclass NAME +``` + +### Options + +``` + -h, --help help for runtimeclass +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of iofogctl +``` + +### SEE ALSO + +* [iofogctl delete](iofogctl_delete.md) - Delete an existing ioFog resource + + diff --git a/docs/iofogctl_md/iofogctl_deploy.md b/docs/iofogctl_md/iofogctl_deploy.md index 3bff7c977..d612fc3b5 100644 --- a/docs/iofogctl_md/iofogctl_deploy.md +++ b/docs/iofogctl_md/iofogctl_deploy.md @@ -19,6 +19,10 @@ iofogctl deploy -f ecn.yaml application.yaml microservice.yaml catalog.yaml + model.yaml + knowledge.yaml + runtimeclass.yaml + microservice-template.yaml volume.yaml route.yaml secret.yaml @@ -33,6 +37,8 @@ iofogctl deploy -f ecn.yaml -f, --file string YAML file containing specifications for ioFog resources to deploy -h, --help help for deploy --no-cache Disable caching for OfflineImage images after download + --patch-knowledge Patch only spec.knowledge on Microservice YAML (catalog PATCH). Controller rebuilds on empty↔non-empty catalog or bindPath/permissions change; item-only changes do not rebuild. Combinable with --patch-model. + --patch-model Patch only spec.models on Microservice YAML (catalog PATCH). Controller decides whether the workload is rebuilt. --transfer-pool int Maximum number of concurrent OfflineImage transfers (default 2) ``` diff --git a/docs/iofogctl_md/iofogctl_describe.md b/docs/iofogctl_md/iofogctl_describe.md index 14e6cbaa6..9641012b2 100644 --- a/docs/iofogctl_md/iofogctl_describe.md +++ b/docs/iofogctl_md/iofogctl_describe.md @@ -34,7 +34,10 @@ Most resources require a working Controller in the Namespace in order to be desc * [iofogctl describe configmap](iofogctl_describe_configmap.md) - Get detailed information about a ConfigMap * [iofogctl describe controller](iofogctl_describe_controller.md) - Get detailed information about a Controller * [iofogctl describe controlplane](iofogctl_describe_controlplane.md) - Get detailed information about a Control Plane +* [iofogctl describe knowledge](iofogctl_describe_knowledge.md) - Get detailed information about Knowledge * [iofogctl describe microservice](iofogctl_describe_microservice.md) - Get detailed information about a Microservice +* [iofogctl describe microservice-template](iofogctl_describe_microservice-template.md) - Get detailed information about a Microservice Template +* [iofogctl describe model](iofogctl_describe_model.md) - Get detailed information about a Model * [iofogctl describe namespace](iofogctl_describe_namespace.md) - Get detailed information about a Namespace * [iofogctl describe nats-account](iofogctl_describe_nats-account.md) - Get detailed information about a NATS account * [iofogctl describe nats-account-rule](iofogctl_describe_nats-account-rule.md) - Get detailed information about a NATS account rule @@ -43,6 +46,7 @@ Most resources require a working Controller in the Namespace in order to be desc * [iofogctl describe registry](iofogctl_describe_registry.md) - Get detailed information about a Microservice Registry * [iofogctl describe role](iofogctl_describe_role.md) - Get detailed information about a Role * [iofogctl describe rolebinding](iofogctl_describe_rolebinding.md) - Get detailed information about a RoleBinding +* [iofogctl describe runtimeclass](iofogctl_describe_runtimeclass.md) - Get detailed information about a RuntimeClass * [iofogctl describe secret](iofogctl_describe_secret.md) - Get detailed information about a Secret * [iofogctl describe service](iofogctl_describe_service.md) - Get detailed information about a Service * [iofogctl describe serviceaccount](iofogctl_describe_serviceaccount.md) - Get detailed information about a ServiceAccount diff --git a/docs/iofogctl_md/iofogctl_describe_knowledge.md b/docs/iofogctl_md/iofogctl_describe_knowledge.md new file mode 100644 index 000000000..4d10d2747 --- /dev/null +++ b/docs/iofogctl_md/iofogctl_describe_knowledge.md @@ -0,0 +1,38 @@ +## iofogctl describe knowledge + +Get detailed information about Knowledge + +### Synopsis + +Get detailed information about Knowledge. + +``` +iofogctl describe knowledge NAME [flags] +``` + +### Examples + +``` +iofogctl describe knowledge NAME +``` + +### Options + +``` + -h, --help help for knowledge + -o, --output-file string YAML output file +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of iofogctl +``` + +### SEE ALSO + +* [iofogctl describe](iofogctl_describe.md) - Get detailed information of an existing resources + + diff --git a/docs/iofogctl_md/iofogctl_describe_microservice-template.md b/docs/iofogctl_md/iofogctl_describe_microservice-template.md new file mode 100644 index 000000000..4b4ab75f5 --- /dev/null +++ b/docs/iofogctl_md/iofogctl_describe_microservice-template.md @@ -0,0 +1,38 @@ +## iofogctl describe microservice-template + +Get detailed information about a Microservice Template + +### Synopsis + +Get detailed information about a Microservice Template. + +``` +iofogctl describe microservice-template NAME [flags] +``` + +### Examples + +``` +iofogctl describe microservice-template NAME +``` + +### Options + +``` + -h, --help help for microservice-template + -o, --output-file string YAML output file +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of iofogctl +``` + +### SEE ALSO + +* [iofogctl describe](iofogctl_describe.md) - Get detailed information of an existing resources + + diff --git a/docs/iofogctl_md/iofogctl_describe_model.md b/docs/iofogctl_md/iofogctl_describe_model.md new file mode 100644 index 000000000..8f9cf82b7 --- /dev/null +++ b/docs/iofogctl_md/iofogctl_describe_model.md @@ -0,0 +1,38 @@ +## iofogctl describe model + +Get detailed information about a Model + +### Synopsis + +Get detailed information about a Model. + +``` +iofogctl describe model NAME [flags] +``` + +### Examples + +``` +iofogctl describe model NAME +``` + +### Options + +``` + -h, --help help for model + -o, --output-file string YAML output file +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of iofogctl +``` + +### SEE ALSO + +* [iofogctl describe](iofogctl_describe.md) - Get detailed information of an existing resources + + diff --git a/docs/iofogctl_md/iofogctl_describe_runtimeclass.md b/docs/iofogctl_md/iofogctl_describe_runtimeclass.md new file mode 100644 index 000000000..becceb7b4 --- /dev/null +++ b/docs/iofogctl_md/iofogctl_describe_runtimeclass.md @@ -0,0 +1,38 @@ +## iofogctl describe runtimeclass + +Get detailed information about a RuntimeClass + +### Synopsis + +Get detailed information about a RuntimeClass. + +``` +iofogctl describe runtimeclass NAME [flags] +``` + +### Examples + +``` +iofogctl describe runtimeclass NAME +``` + +### Options + +``` + -h, --help help for runtimeclass + -o, --output-file string YAML output file +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of iofogctl +``` + +### SEE ALSO + +* [iofogctl describe](iofogctl_describe.md) - Get detailed information of an existing resources + + diff --git a/docs/iofogctl_md/iofogctl_detach.md b/docs/iofogctl_md/iofogctl_detach.md index 0e367f78c..d22e918a5 100644 --- a/docs/iofogctl_md/iofogctl_detach.md +++ b/docs/iofogctl_md/iofogctl_detach.md @@ -31,6 +31,9 @@ iofogctl detach * [iofogctl](iofogctl.md) - * [iofogctl detach agent](iofogctl_detach_agent.md) - Detaches an Agent * [iofogctl detach exec](iofogctl_detach_exec.md) - Remove fog debug exec from an Agent +* [iofogctl detach knowledge](iofogctl_detach_knowledge.md) - Detach Knowledge from existing Agents +* [iofogctl detach model](iofogctl_detach_model.md) - Detach a Model from existing Agents +* [iofogctl detach runtimeclass](iofogctl_detach_runtimeclass.md) - Detach a RuntimeClass from existing Agents * [iofogctl detach volume-mount](iofogctl_detach_volume-mount.md) - Detach a Volume Mount from existing Agents diff --git a/docs/iofogctl_md/iofogctl_detach_knowledge.md b/docs/iofogctl_md/iofogctl_detach_knowledge.md new file mode 100644 index 000000000..0227a39d4 --- /dev/null +++ b/docs/iofogctl_md/iofogctl_detach_knowledge.md @@ -0,0 +1,37 @@ +## iofogctl detach knowledge + +Detach Knowledge from existing Agents + +### Synopsis + +Detach Knowledge from existing Agents. + +``` +iofogctl detach knowledge NAME AGENT_NAME1 AGENT_NAME2 [flags] +``` + +### Examples + +``` +iofogctl detach knowledge NAME AGENT_NAME1 AGENT_NAME2 +``` + +### Options + +``` + -h, --help help for knowledge +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of iofogctl +``` + +### SEE ALSO + +* [iofogctl detach](iofogctl_detach.md) - Detach one ioFog resource from another + + diff --git a/docs/iofogctl_md/iofogctl_detach_model.md b/docs/iofogctl_md/iofogctl_detach_model.md new file mode 100644 index 000000000..5faf40b2f --- /dev/null +++ b/docs/iofogctl_md/iofogctl_detach_model.md @@ -0,0 +1,37 @@ +## iofogctl detach model + +Detach a Model from existing Agents + +### Synopsis + +Detach a Model from existing Agents. + +``` +iofogctl detach model NAME AGENT_NAME1 AGENT_NAME2 [flags] +``` + +### Examples + +``` +iofogctl detach model NAME AGENT_NAME1 AGENT_NAME2 +``` + +### Options + +``` + -h, --help help for model +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of iofogctl +``` + +### SEE ALSO + +* [iofogctl detach](iofogctl_detach.md) - Detach one ioFog resource from another + + diff --git a/docs/iofogctl_md/iofogctl_detach_runtimeclass.md b/docs/iofogctl_md/iofogctl_detach_runtimeclass.md new file mode 100644 index 000000000..e432c2edf --- /dev/null +++ b/docs/iofogctl_md/iofogctl_detach_runtimeclass.md @@ -0,0 +1,37 @@ +## iofogctl detach runtimeclass + +Detach a RuntimeClass from existing Agents + +### Synopsis + +Detach a RuntimeClass from existing Agents. + +``` +iofogctl detach runtimeclass NAME AGENT_NAME1 AGENT_NAME2 [flags] +``` + +### Examples + +``` +iofogctl detach runtimeclass NAME AGENT_NAME1 AGENT_NAME2 +``` + +### Options + +``` + -h, --help help for runtimeclass +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of iofogctl +``` + +### SEE ALSO + +* [iofogctl detach](iofogctl_detach.md) - Detach one ioFog resource from another + + diff --git a/docs/iofogctl_md/iofogctl_get.md b/docs/iofogctl_md/iofogctl_get.md index 5870f4f98..b35db6da2 100644 --- a/docs/iofogctl_md/iofogctl_get.md +++ b/docs/iofogctl_md/iofogctl_get.md @@ -41,6 +41,10 @@ iofogctl get all nats-user-rules auth-groups auth-group NAME + knowledge + models + runtimeclass + microservice-templates ``` ### Options diff --git a/docs/potctl_md/potctl_attach.md b/docs/potctl_md/potctl_attach.md index 3dc8ad7dd..fceba12ed 100644 --- a/docs/potctl_md/potctl_attach.md +++ b/docs/potctl_md/potctl_attach.md @@ -31,6 +31,9 @@ potctl attach * [potctl](potctl.md) - * [potctl attach agent](potctl_attach_agent.md) - Attach an Agent to an existing Namespace * [potctl attach exec](potctl_attach_exec.md) - Provision fog debug exec on an Agent +* [potctl attach knowledge](potctl_attach_knowledge.md) - Attach Knowledge to existing Agents +* [potctl attach model](potctl_attach_model.md) - Attach a Model to existing Agents +* [potctl attach runtimeclass](potctl_attach_runtimeclass.md) - Attach a RuntimeClass to existing Agents * [potctl attach volume-mount](potctl_attach_volume-mount.md) - Attach a Volume Mount to existing Agents diff --git a/docs/potctl_md/potctl_attach_knowledge.md b/docs/potctl_md/potctl_attach_knowledge.md new file mode 100644 index 000000000..8bd4a517b --- /dev/null +++ b/docs/potctl_md/potctl_attach_knowledge.md @@ -0,0 +1,37 @@ +## potctl attach knowledge + +Attach Knowledge to existing Agents + +### Synopsis + +Attach Knowledge to existing Agents. + +``` +potctl attach knowledge NAME AGENT_NAME1 AGENT_NAME2 [flags] +``` + +### Examples + +``` +potctl attach knowledge NAME AGENT_NAME1 AGENT_NAME2 +``` + +### Options + +``` + -h, --help help for knowledge +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl attach](potctl_attach.md) - Attach one ioFog resource to another + + diff --git a/docs/potctl_md/potctl_attach_model.md b/docs/potctl_md/potctl_attach_model.md new file mode 100644 index 000000000..dbd1a8b54 --- /dev/null +++ b/docs/potctl_md/potctl_attach_model.md @@ -0,0 +1,37 @@ +## potctl attach model + +Attach a Model to existing Agents + +### Synopsis + +Attach a Model to existing Agents. + +``` +potctl attach model NAME AGENT_NAME1 AGENT_NAME2 [flags] +``` + +### Examples + +``` +potctl attach model NAME AGENT_NAME1 AGENT_NAME2 +``` + +### Options + +``` + -h, --help help for model +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl attach](potctl_attach.md) - Attach one ioFog resource to another + + diff --git a/docs/potctl_md/potctl_attach_runtimeclass.md b/docs/potctl_md/potctl_attach_runtimeclass.md new file mode 100644 index 000000000..4048be441 --- /dev/null +++ b/docs/potctl_md/potctl_attach_runtimeclass.md @@ -0,0 +1,37 @@ +## potctl attach runtimeclass + +Attach a RuntimeClass to existing Agents + +### Synopsis + +Attach a RuntimeClass to existing Agents. RuntimeClass can only be linked to edgelet agents. + +``` +potctl attach runtimeclass NAME AGENT_NAME1 AGENT_NAME2 [flags] +``` + +### Examples + +``` +potctl attach runtimeclass NAME AGENT_NAME1 AGENT_NAME2 +``` + +### Options + +``` + -h, --help help for runtimeclass +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl attach](potctl_attach.md) - Attach one ioFog resource to another + + diff --git a/docs/potctl_md/potctl_delete.md b/docs/potctl_md/potctl_delete.md index 5863b217c..705fe572a 100644 --- a/docs/potctl_md/potctl_delete.md +++ b/docs/potctl_md/potctl_delete.md @@ -38,13 +38,17 @@ potctl delete [flags] * [potctl delete certificate](potctl_delete_certificate.md) - Delete a Certificate * [potctl delete configmap](potctl_delete_configmap.md) - Delete a ConfigMap * [potctl delete controller](potctl_delete_controller.md) - Delete a Controller +* [potctl delete knowledge](potctl_delete_knowledge.md) - Delete Knowledge * [potctl delete microservice](potctl_delete_microservice.md) - Delete a Microservice +* [potctl delete microservice-template](potctl_delete_microservice-template.md) - Delete a Microservice Template +* [potctl delete model](potctl_delete_model.md) - Delete a Model * [potctl delete namespace](potctl_delete_namespace.md) - Delete a Namespace * [potctl delete nats-account-rule](potctl_delete_nats-account-rule.md) - Delete a NATS account rule * [potctl delete nats-user-rule](potctl_delete_nats-user-rule.md) - Delete a NATS user rule * [potctl delete registry](potctl_delete_registry.md) - Delete a Registry * [potctl delete role](potctl_delete_role.md) - Delete a Role * [potctl delete rolebinding](potctl_delete_rolebinding.md) - Delete a RoleBinding +* [potctl delete runtimeclass](potctl_delete_runtimeclass.md) - Delete a RuntimeClass * [potctl delete secret](potctl_delete_secret.md) - Delete a Secret * [potctl delete service](potctl_delete_service.md) - Delete a Service * [potctl delete serviceaccount](potctl_delete_serviceaccount.md) - Delete a ServiceAccount diff --git a/docs/potctl_md/potctl_delete_knowledge.md b/docs/potctl_md/potctl_delete_knowledge.md new file mode 100644 index 000000000..cb47df895 --- /dev/null +++ b/docs/potctl_md/potctl_delete_knowledge.md @@ -0,0 +1,38 @@ +## potctl delete knowledge + +Delete Knowledge + +### Synopsis + +Delete Knowledge from the Controller. + +``` +potctl delete knowledge NAME [flags] +``` + +### Examples + +``` +potctl delete knowledge NAME +``` + +### Options + +``` + -h, --help help for knowledge +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl delete](potctl_delete.md) - Delete an existing ioFog resource + + diff --git a/docs/potctl_md/potctl_delete_microservice-template.md b/docs/potctl_md/potctl_delete_microservice-template.md new file mode 100644 index 000000000..bedf42312 --- /dev/null +++ b/docs/potctl_md/potctl_delete_microservice-template.md @@ -0,0 +1,38 @@ +## potctl delete microservice-template + +Delete a Microservice Template + +### Synopsis + +Delete a Microservice Template from the Controller. + +``` +potctl delete microservice-template NAME [flags] +``` + +### Examples + +``` +potctl delete microservice-template NAME +``` + +### Options + +``` + -h, --help help for microservice-template +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl delete](potctl_delete.md) - Delete an existing ioFog resource + + diff --git a/docs/potctl_md/potctl_delete_model.md b/docs/potctl_md/potctl_delete_model.md new file mode 100644 index 000000000..c57255c06 --- /dev/null +++ b/docs/potctl_md/potctl_delete_model.md @@ -0,0 +1,38 @@ +## potctl delete model + +Delete a Model + +### Synopsis + +Delete a Model from the Controller. + +``` +potctl delete model NAME [flags] +``` + +### Examples + +``` +potctl delete model NAME +``` + +### Options + +``` + -h, --help help for model +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl delete](potctl_delete.md) - Delete an existing ioFog resource + + diff --git a/docs/potctl_md/potctl_delete_runtimeclass.md b/docs/potctl_md/potctl_delete_runtimeclass.md new file mode 100644 index 000000000..fe86cf1a2 --- /dev/null +++ b/docs/potctl_md/potctl_delete_runtimeclass.md @@ -0,0 +1,38 @@ +## potctl delete runtimeclass + +Delete a RuntimeClass + +### Synopsis + +Delete a RuntimeClass from the Controller. + +``` +potctl delete runtimeclass NAME [flags] +``` + +### Examples + +``` +potctl delete runtimeclass NAME +``` + +### Options + +``` + -h, --help help for runtimeclass +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + --delete-namespace Also delete the Kubernetes namespace (never deletes "default") + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl delete](potctl_delete.md) - Delete an existing ioFog resource + + diff --git a/docs/potctl_md/potctl_deploy.md b/docs/potctl_md/potctl_deploy.md index 5aa945ae7..7e6c28bf3 100644 --- a/docs/potctl_md/potctl_deploy.md +++ b/docs/potctl_md/potctl_deploy.md @@ -19,6 +19,10 @@ potctl deploy -f ecn.yaml application.yaml microservice.yaml catalog.yaml + model.yaml + knowledge.yaml + runtimeclass.yaml + microservice-template.yaml volume.yaml route.yaml secret.yaml @@ -33,6 +37,8 @@ potctl deploy -f ecn.yaml -f, --file string YAML file containing specifications for ioFog resources to deploy -h, --help help for deploy --no-cache Disable caching for OfflineImage images after download + --patch-knowledge Patch only spec.knowledge on Microservice YAML (catalog PATCH). Controller rebuilds on empty↔non-empty catalog or bindPath/permissions change; item-only changes do not rebuild. Combinable with --patch-model. + --patch-model Patch only spec.models on Microservice YAML (catalog PATCH). Controller decides whether the workload is rebuilt. --transfer-pool int Maximum number of concurrent OfflineImage transfers (default 2) ``` diff --git a/docs/potctl_md/potctl_describe.md b/docs/potctl_md/potctl_describe.md index b4014a45a..7a4c63e4e 100644 --- a/docs/potctl_md/potctl_describe.md +++ b/docs/potctl_md/potctl_describe.md @@ -34,7 +34,10 @@ Most resources require a working Controller in the Namespace in order to be desc * [potctl describe configmap](potctl_describe_configmap.md) - Get detailed information about a ConfigMap * [potctl describe controller](potctl_describe_controller.md) - Get detailed information about a Controller * [potctl describe controlplane](potctl_describe_controlplane.md) - Get detailed information about a Control Plane +* [potctl describe knowledge](potctl_describe_knowledge.md) - Get detailed information about Knowledge * [potctl describe microservice](potctl_describe_microservice.md) - Get detailed information about a Microservice +* [potctl describe microservice-template](potctl_describe_microservice-template.md) - Get detailed information about a Microservice Template +* [potctl describe model](potctl_describe_model.md) - Get detailed information about a Model * [potctl describe namespace](potctl_describe_namespace.md) - Get detailed information about a Namespace * [potctl describe nats-account](potctl_describe_nats-account.md) - Get detailed information about a NATS account * [potctl describe nats-account-rule](potctl_describe_nats-account-rule.md) - Get detailed information about a NATS account rule @@ -43,6 +46,7 @@ Most resources require a working Controller in the Namespace in order to be desc * [potctl describe registry](potctl_describe_registry.md) - Get detailed information about a Microservice Registry * [potctl describe role](potctl_describe_role.md) - Get detailed information about a Role * [potctl describe rolebinding](potctl_describe_rolebinding.md) - Get detailed information about a RoleBinding +* [potctl describe runtimeclass](potctl_describe_runtimeclass.md) - Get detailed information about a RuntimeClass * [potctl describe secret](potctl_describe_secret.md) - Get detailed information about a Secret * [potctl describe service](potctl_describe_service.md) - Get detailed information about a Service * [potctl describe serviceaccount](potctl_describe_serviceaccount.md) - Get detailed information about a ServiceAccount diff --git a/docs/potctl_md/potctl_describe_knowledge.md b/docs/potctl_md/potctl_describe_knowledge.md new file mode 100644 index 000000000..e2af7220f --- /dev/null +++ b/docs/potctl_md/potctl_describe_knowledge.md @@ -0,0 +1,38 @@ +## potctl describe knowledge + +Get detailed information about Knowledge + +### Synopsis + +Get detailed information about Knowledge. + +``` +potctl describe knowledge NAME [flags] +``` + +### Examples + +``` +potctl describe knowledge NAME +``` + +### Options + +``` + -h, --help help for knowledge + -o, --output-file string YAML output file +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl describe](potctl_describe.md) - Get detailed information of an existing resources + + diff --git a/docs/potctl_md/potctl_describe_microservice-template.md b/docs/potctl_md/potctl_describe_microservice-template.md new file mode 100644 index 000000000..dd2dd5e3f --- /dev/null +++ b/docs/potctl_md/potctl_describe_microservice-template.md @@ -0,0 +1,38 @@ +## potctl describe microservice-template + +Get detailed information about a Microservice Template + +### Synopsis + +Get detailed information about a Microservice Template. + +``` +potctl describe microservice-template NAME [flags] +``` + +### Examples + +``` +potctl describe microservice-template NAME +``` + +### Options + +``` + -h, --help help for microservice-template + -o, --output-file string YAML output file +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl describe](potctl_describe.md) - Get detailed information of an existing resources + + diff --git a/docs/potctl_md/potctl_describe_model.md b/docs/potctl_md/potctl_describe_model.md new file mode 100644 index 000000000..5c06c2e8b --- /dev/null +++ b/docs/potctl_md/potctl_describe_model.md @@ -0,0 +1,38 @@ +## potctl describe model + +Get detailed information about a Model + +### Synopsis + +Get detailed information about a Model. + +``` +potctl describe model NAME [flags] +``` + +### Examples + +``` +potctl describe model NAME +``` + +### Options + +``` + -h, --help help for model + -o, --output-file string YAML output file +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl describe](potctl_describe.md) - Get detailed information of an existing resources + + diff --git a/docs/potctl_md/potctl_describe_runtimeclass.md b/docs/potctl_md/potctl_describe_runtimeclass.md new file mode 100644 index 000000000..ec528dbc9 --- /dev/null +++ b/docs/potctl_md/potctl_describe_runtimeclass.md @@ -0,0 +1,38 @@ +## potctl describe runtimeclass + +Get detailed information about a RuntimeClass + +### Synopsis + +Get detailed information about a RuntimeClass. + +``` +potctl describe runtimeclass NAME [flags] +``` + +### Examples + +``` +potctl describe runtimeclass NAME +``` + +### Options + +``` + -h, --help help for runtimeclass + -o, --output-file string YAML output file +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl describe](potctl_describe.md) - Get detailed information of an existing resources + + diff --git a/docs/potctl_md/potctl_detach.md b/docs/potctl_md/potctl_detach.md index 0fcb2dac6..3f5879c2d 100644 --- a/docs/potctl_md/potctl_detach.md +++ b/docs/potctl_md/potctl_detach.md @@ -31,6 +31,9 @@ potctl detach * [potctl](potctl.md) - * [potctl detach agent](potctl_detach_agent.md) - Detaches an Agent * [potctl detach exec](potctl_detach_exec.md) - Remove fog debug exec from an Agent +* [potctl detach knowledge](potctl_detach_knowledge.md) - Detach Knowledge from existing Agents +* [potctl detach model](potctl_detach_model.md) - Detach a Model from existing Agents +* [potctl detach runtimeclass](potctl_detach_runtimeclass.md) - Detach a RuntimeClass from existing Agents * [potctl detach volume-mount](potctl_detach_volume-mount.md) - Detach a Volume Mount from existing Agents diff --git a/docs/potctl_md/potctl_detach_knowledge.md b/docs/potctl_md/potctl_detach_knowledge.md new file mode 100644 index 000000000..378c71691 --- /dev/null +++ b/docs/potctl_md/potctl_detach_knowledge.md @@ -0,0 +1,37 @@ +## potctl detach knowledge + +Detach Knowledge from existing Agents + +### Synopsis + +Detach Knowledge from existing Agents. + +``` +potctl detach knowledge NAME AGENT_NAME1 AGENT_NAME2 [flags] +``` + +### Examples + +``` +potctl detach knowledge NAME AGENT_NAME1 AGENT_NAME2 +``` + +### Options + +``` + -h, --help help for knowledge +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl detach](potctl_detach.md) - Detach one ioFog resource from another + + diff --git a/docs/potctl_md/potctl_detach_model.md b/docs/potctl_md/potctl_detach_model.md new file mode 100644 index 000000000..7f38bce91 --- /dev/null +++ b/docs/potctl_md/potctl_detach_model.md @@ -0,0 +1,37 @@ +## potctl detach model + +Detach a Model from existing Agents + +### Synopsis + +Detach a Model from existing Agents. + +``` +potctl detach model NAME AGENT_NAME1 AGENT_NAME2 [flags] +``` + +### Examples + +``` +potctl detach model NAME AGENT_NAME1 AGENT_NAME2 +``` + +### Options + +``` + -h, --help help for model +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl detach](potctl_detach.md) - Detach one ioFog resource from another + + diff --git a/docs/potctl_md/potctl_detach_runtimeclass.md b/docs/potctl_md/potctl_detach_runtimeclass.md new file mode 100644 index 000000000..af730f025 --- /dev/null +++ b/docs/potctl_md/potctl_detach_runtimeclass.md @@ -0,0 +1,37 @@ +## potctl detach runtimeclass + +Detach a RuntimeClass from existing Agents + +### Synopsis + +Detach a RuntimeClass from existing Agents. + +``` +potctl detach runtimeclass NAME AGENT_NAME1 AGENT_NAME2 [flags] +``` + +### Examples + +``` +potctl detach runtimeclass NAME AGENT_NAME1 AGENT_NAME2 +``` + +### Options + +``` + -h, --help help for runtimeclass +``` + +### Options inherited from parent commands + +``` + --debug Toggle for displaying verbose output of API clients (HTTP and SSH) + -n, --namespace string Namespace to execute respective command within (default "default") + -v, --verbose Toggle for displaying verbose output of potctl +``` + +### SEE ALSO + +* [potctl detach](potctl_detach.md) - Detach one ioFog resource from another + + diff --git a/docs/potctl_md/potctl_get.md b/docs/potctl_md/potctl_get.md index 24893891c..42b1e21cb 100644 --- a/docs/potctl_md/potctl_get.md +++ b/docs/potctl_md/potctl_get.md @@ -41,6 +41,10 @@ potctl get all nats-user-rules auth-groups auth-group NAME + knowledge + models + runtimeclass + microservice-templates ``` ### Options diff --git a/go.mod b/go.mod index 621fadf09..5f7b6381d 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,8 @@ go 1.26.6 require ( github.com/briandowns/spinner v1.23.1 - github.com/eclipse-iofog/iofog-go-sdk/v3 v3.8.3-rc.1 - github.com/eclipse-iofog/iofog-operator/v3 v3.8.3-rc.1 + github.com/eclipse-iofog/iofog-go-sdk/v3 v3.9.0-rc.6 + github.com/eclipse-iofog/iofog-operator/v3 v3.9.0-rc.1 github.com/mitchellh/go-homedir v1.1.0 github.com/moby/moby/api v1.55.0 github.com/moby/moby/client v0.5.1 @@ -133,7 +133,7 @@ require ( golang.org/x/time v0.15.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect - google.golang.org/grpc v1.82.1 // indirect + google.golang.org/grpc v1.83.1 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/go.sum b/go.sum index 16e015fde..209d2e941 100644 --- a/go.sum +++ b/go.sum @@ -53,10 +53,10 @@ github.com/docker/go-connections v0.8.1 h1:JibmG5hULs5qXSr/cp/w3Pw5fZuStt4MOHMUE github.com/docker/go-connections v0.8.1/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/eclipse-iofog/iofog-go-sdk/v3 v3.8.3-rc.1 h1:3D3eklSjFqQ/0G4/BDiQAz0X8jORrm/glcAvqIsW7gc= -github.com/eclipse-iofog/iofog-go-sdk/v3 v3.8.3-rc.1/go.mod h1:16CjC/B5xwOc76nzCsoXS3EdD/MHeusxbsUGS/Wi1UI= -github.com/eclipse-iofog/iofog-operator/v3 v3.8.3-rc.1 h1:f21l63I7lU8IbfUwhHfSEjHH2gp5bqn404ZEZxR2p7I= -github.com/eclipse-iofog/iofog-operator/v3 v3.8.3-rc.1/go.mod h1:+nU3Kal2NDJTlVF7Po8PZ95ovV/Mm/RO05CYSTJub6k= +github.com/eclipse-iofog/iofog-go-sdk/v3 v3.9.0-rc.6 h1:k2cGTiKGrJ4iXfQOw3keg7kZ5zTy6QlAfwKb0vtlY38= +github.com/eclipse-iofog/iofog-go-sdk/v3 v3.9.0-rc.6/go.mod h1:E8MrwKuJpQJpaMLpNtb3GO3/uLB9Y6kmsv1Cd8eUwLU= +github.com/eclipse-iofog/iofog-operator/v3 v3.9.0-rc.1 h1:FaaiQ4duA3MqK99rfcYElWY5jrzlBAxBPFMo62BdYRA= +github.com/eclipse-iofog/iofog-operator/v3 v3.9.0-rc.1/go.mod h1:6t1FKLQt8f9Y5jtyq/7p//6v6Yy/Dpeiz9ex4EvU/fQ= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= @@ -374,8 +374,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1: google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY= google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= -google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= +google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y= +google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/attach/knowledge/execute.go b/internal/attach/knowledge/execute.go new file mode 100644 index 000000000..76c56d47f --- /dev/null +++ b/internal/attach/knowledge/execute.go @@ -0,0 +1,49 @@ +package attachknowledge + +import ( + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + "github.com/eclipse-iofog/iofogctl/internal/execute" + clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +type Options struct { + Name string + Agents []string + Namespace string +} + +type executor struct { + name string + agents []string + namespace string +} + +func NewExecutor(opt Options) execute.Executor { + return &executor{ + name: opt.Name, + agents: opt.Agents, + namespace: opt.Namespace, + } +} + +func (exe *executor) GetName() string { + return exe.name +} + +func (exe *executor) Execute() error { + util.SpinStart("Attaching Knowledge") + + clt, err := clientutil.NewControllerClient(exe.namespace) + if err != nil { + return err + } + + agentUUIDs, err := clientutil.AgentUUIDsFromNames(clt, exe.agents) + if err != nil { + return err + } + + req := client.FogLinkRequest{FogUUIDs: agentUUIDs} + return clt.LinkKnowledge(exe.name, &req) +} diff --git a/internal/attach/model/execute.go b/internal/attach/model/execute.go new file mode 100644 index 000000000..9c61bf047 --- /dev/null +++ b/internal/attach/model/execute.go @@ -0,0 +1,49 @@ +package attachmodel + +import ( + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + "github.com/eclipse-iofog/iofogctl/internal/execute" + clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +type Options struct { + Name string + Agents []string + Namespace string +} + +type executor struct { + name string + agents []string + namespace string +} + +func NewExecutor(opt Options) execute.Executor { + return &executor{ + name: opt.Name, + agents: opt.Agents, + namespace: opt.Namespace, + } +} + +func (exe *executor) GetName() string { + return exe.name +} + +func (exe *executor) Execute() error { + util.SpinStart("Attaching Model") + + clt, err := clientutil.NewControllerClient(exe.namespace) + if err != nil { + return err + } + + agentUUIDs, err := clientutil.AgentUUIDsFromNames(clt, exe.agents) + if err != nil { + return err + } + + req := client.FogLinkRequest{FogUUIDs: agentUUIDs} + return clt.LinkModel(exe.name, &req) +} diff --git a/internal/attach/runtimeclass/execute.go b/internal/attach/runtimeclass/execute.go new file mode 100644 index 000000000..8942690b9 --- /dev/null +++ b/internal/attach/runtimeclass/execute.go @@ -0,0 +1,49 @@ +package attachruntimeclass + +import ( + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + "github.com/eclipse-iofog/iofogctl/internal/execute" + clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +type Options struct { + Name string + Agents []string + Namespace string +} + +type executor struct { + name string + agents []string + namespace string +} + +func NewExecutor(opt Options) execute.Executor { + return &executor{ + name: opt.Name, + agents: opt.Agents, + namespace: opt.Namespace, + } +} + +func (exe *executor) GetName() string { + return exe.name +} + +func (exe *executor) Execute() error { + util.SpinStart("Attaching RuntimeClass") + + clt, err := clientutil.NewControllerClient(exe.namespace) + if err != nil { + return err + } + + agentUUIDs, err := clientutil.AgentUUIDsFromNames(clt, exe.agents) + if err != nil { + return err + } + + req := client.FogLinkRequest{FogUUIDs: agentUUIDs} + return clientutil.PrefixRuntimeClassLink400(clt.LinkRuntimeClass(exe.name, &req)) +} diff --git a/internal/cmd/attach.go b/internal/cmd/attach.go index 57fcd029d..044ca92d2 100644 --- a/internal/cmd/attach.go +++ b/internal/cmd/attach.go @@ -17,6 +17,9 @@ func newAttachCommand() *cobra.Command { newAttachAgentCommand(), newAttachVolumeMountCommand(), newAttachExecCommand(), + newAttachKnowledgeCommand(), + newAttachModelCommand(), + newAttachRuntimeClassCommand(), ) return cmd diff --git a/internal/cmd/attach_knowledge.go b/internal/cmd/attach_knowledge.go new file mode 100644 index 000000000..b264c0717 --- /dev/null +++ b/internal/cmd/attach_knowledge.go @@ -0,0 +1,37 @@ +package cmd + +import ( + "fmt" + "strings" + + attach "github.com/eclipse-iofog/iofogctl/internal/attach/knowledge" + "github.com/eclipse-iofog/iofogctl/pkg/util" + "github.com/spf13/cobra" +) + +func newAttachKnowledgeCommand() *cobra.Command { + opt := attach.Options{} + cmd := &cobra.Command{ + Use: "knowledge NAME AGENT_NAME1 AGENT_NAME2", + Short: "Attach Knowledge to existing Agents", + Long: `Attach Knowledge to existing Agents.`, + Example: ex(`%[1]s attach knowledge NAME AGENT_NAME1 AGENT_NAME2`), + Args: cobra.MinimumNArgs(2), + Run: func(cmd *cobra.Command, args []string) { + opt.Name = args[0] + opt.Agents = args[1:] + var err error + opt.Namespace, err = cmd.Flags().GetString("namespace") + util.Check(err) + + exe := attach.NewExecutor(opt) + err = exe.Execute() + util.Check(err) + + msg := fmt.Sprintf("Successfully attached Knowledge %s to Agents %s", opt.Name, strings.Join(opt.Agents, ", ")) + util.PrintSuccess(msg) + }, + } + + return cmd +} diff --git a/internal/cmd/attach_model.go b/internal/cmd/attach_model.go new file mode 100644 index 000000000..e7a8bd99d --- /dev/null +++ b/internal/cmd/attach_model.go @@ -0,0 +1,37 @@ +package cmd + +import ( + "fmt" + "strings" + + attach "github.com/eclipse-iofog/iofogctl/internal/attach/model" + "github.com/eclipse-iofog/iofogctl/pkg/util" + "github.com/spf13/cobra" +) + +func newAttachModelCommand() *cobra.Command { + opt := attach.Options{} + cmd := &cobra.Command{ + Use: "model NAME AGENT_NAME1 AGENT_NAME2", + Short: "Attach a Model to existing Agents", + Long: `Attach a Model to existing Agents.`, + Example: ex(`%[1]s attach model NAME AGENT_NAME1 AGENT_NAME2`), + Args: cobra.MinimumNArgs(2), + Run: func(cmd *cobra.Command, args []string) { + opt.Name = args[0] + opt.Agents = args[1:] + var err error + opt.Namespace, err = cmd.Flags().GetString("namespace") + util.Check(err) + + exe := attach.NewExecutor(opt) + err = exe.Execute() + util.Check(err) + + msg := fmt.Sprintf("Successfully attached Model %s to Agents %s", opt.Name, strings.Join(opt.Agents, ", ")) + util.PrintSuccess(msg) + }, + } + + return cmd +} diff --git a/internal/cmd/attach_runtimeclass.go b/internal/cmd/attach_runtimeclass.go new file mode 100644 index 000000000..7912fad35 --- /dev/null +++ b/internal/cmd/attach_runtimeclass.go @@ -0,0 +1,37 @@ +package cmd + +import ( + "fmt" + "strings" + + attach "github.com/eclipse-iofog/iofogctl/internal/attach/runtimeclass" + "github.com/eclipse-iofog/iofogctl/pkg/util" + "github.com/spf13/cobra" +) + +func newAttachRuntimeClassCommand() *cobra.Command { + opt := attach.Options{} + cmd := &cobra.Command{ + Use: "runtimeclass NAME AGENT_NAME1 AGENT_NAME2", + Short: "Attach a RuntimeClass to existing Agents", + Long: `Attach a RuntimeClass to existing Agents. RuntimeClass can only be linked to edgelet agents.`, + Example: ex(`%[1]s attach runtimeclass NAME AGENT_NAME1 AGENT_NAME2`), + Args: cobra.MinimumNArgs(2), + Run: func(cmd *cobra.Command, args []string) { + opt.Name = args[0] + opt.Agents = args[1:] + var err error + opt.Namespace, err = cmd.Flags().GetString("namespace") + util.Check(err) + + exe := attach.NewExecutor(opt) + err = exe.Execute() + util.Check(err) + + msg := fmt.Sprintf("Successfully attached RuntimeClass %s to Agents %s", opt.Name, strings.Join(opt.Agents, ", ")) + util.PrintSuccess(msg) + }, + } + + return cmd +} diff --git a/internal/cmd/delete.go b/internal/cmd/delete.go index d70b49448..036778573 100644 --- a/internal/cmd/delete.go +++ b/internal/cmd/delete.go @@ -60,6 +60,10 @@ func newDeleteCommand() *cobra.Command { newDeleteVolumeMountCommand(), newDeleteCertificateCommand(), newDeleteAuthGroupCommand(), + newDeleteKnowledgeCommand(), + newDeleteModelCommand(), + newDeleteRuntimeClassCommand(), + newDeleteMicroserviceTemplateCommand(), ) // Register flags diff --git a/internal/cmd/delete_knowledge.go b/internal/cmd/delete_knowledge.go new file mode 100644 index 000000000..d65cfc4f0 --- /dev/null +++ b/internal/cmd/delete_knowledge.go @@ -0,0 +1,31 @@ +package cmd + +import ( + deleteknowledge "github.com/eclipse-iofog/iofogctl/internal/delete/knowledge" + "github.com/eclipse-iofog/iofogctl/pkg/util" + "github.com/spf13/cobra" +) + +func newDeleteKnowledgeCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "knowledge NAME", + Short: "Delete Knowledge", + Long: `Delete Knowledge from the Controller.`, + Example: ex(`%[1]s delete knowledge NAME`), + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + name := args[0] + namespace, err := cmd.Flags().GetString("namespace") + util.Check(err) + + exe, err := deleteknowledge.NewExecutor(namespace, name) + util.Check(err) + err = exe.Execute() + util.Check(err) + + util.PrintSuccess("Successfully deleted knowledge " + name) + }, + } + + return cmd +} diff --git a/internal/cmd/delete_microservice_template.go b/internal/cmd/delete_microservice_template.go new file mode 100644 index 000000000..dfdd91395 --- /dev/null +++ b/internal/cmd/delete_microservice_template.go @@ -0,0 +1,31 @@ +package cmd + +import ( + deletemicroservicetemplate "github.com/eclipse-iofog/iofogctl/internal/delete/microservicetemplate" + "github.com/eclipse-iofog/iofogctl/pkg/util" + "github.com/spf13/cobra" +) + +func newDeleteMicroserviceTemplateCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "microservice-template NAME", + Short: "Delete a Microservice Template", + Long: `Delete a Microservice Template from the Controller.`, + Example: ex(`%[1]s delete microservice-template NAME`), + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + name := args[0] + namespace, err := cmd.Flags().GetString("namespace") + util.Check(err) + + exe, err := deletemicroservicetemplate.NewExecutor(namespace, name) + util.Check(err) + err = exe.Execute() + util.Check(err) + + util.PrintSuccess("Successfully deleted microservice-template " + name) + }, + } + + return cmd +} diff --git a/internal/cmd/delete_model.go b/internal/cmd/delete_model.go new file mode 100644 index 000000000..500e12364 --- /dev/null +++ b/internal/cmd/delete_model.go @@ -0,0 +1,31 @@ +package cmd + +import ( + deletemodel "github.com/eclipse-iofog/iofogctl/internal/delete/model" + "github.com/eclipse-iofog/iofogctl/pkg/util" + "github.com/spf13/cobra" +) + +func newDeleteModelCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "model NAME", + Short: "Delete a Model", + Long: `Delete a Model from the Controller.`, + Example: ex(`%[1]s delete model NAME`), + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + name := args[0] + namespace, err := cmd.Flags().GetString("namespace") + util.Check(err) + + exe, err := deletemodel.NewExecutor(namespace, name) + util.Check(err) + err = exe.Execute() + util.Check(err) + + util.PrintSuccess("Successfully deleted model " + name) + }, + } + + return cmd +} diff --git a/internal/cmd/delete_runtimeclass.go b/internal/cmd/delete_runtimeclass.go new file mode 100644 index 000000000..332c0add6 --- /dev/null +++ b/internal/cmd/delete_runtimeclass.go @@ -0,0 +1,31 @@ +package cmd + +import ( + deleteruntimeclass "github.com/eclipse-iofog/iofogctl/internal/delete/runtimeclass" + "github.com/eclipse-iofog/iofogctl/pkg/util" + "github.com/spf13/cobra" +) + +func newDeleteRuntimeClassCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "runtimeclass NAME", + Short: "Delete a RuntimeClass", + Long: `Delete a RuntimeClass from the Controller.`, + Example: ex(`%[1]s delete runtimeclass NAME`), + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + name := args[0] + namespace, err := cmd.Flags().GetString("namespace") + util.Check(err) + + exe, err := deleteruntimeclass.NewExecutor(namespace, name) + util.Check(err) + err = exe.Execute() + util.Check(err) + + util.PrintSuccess("Successfully deleted runtimeclass " + name) + }, + } + + return cmd +} diff --git a/internal/cmd/deploy.go b/internal/cmd/deploy.go index f684ba4ae..80f2edebc 100644 --- a/internal/cmd/deploy.go +++ b/internal/cmd/deploy.go @@ -20,6 +20,10 @@ func newDeployCommand() *cobra.Command { application.yaml microservice.yaml catalog.yaml + model.yaml + knowledge.yaml + runtimeclass.yaml + microservice-template.yaml volume.yaml route.yaml secret.yaml @@ -53,6 +57,8 @@ Visit %[2]s to view all YAML specifications usable with this command.`, util.Get cmd.Flags().StringVarP(&opt.InputFile, "file", "f", "", pkg.flagDescYaml) cmd.Flags().BoolVar(&opt.NoCache, "no-cache", false, "Disable caching for OfflineImage images after download") cmd.Flags().IntVar(&opt.TransferPool, "transfer-pool", 2, "Maximum number of concurrent OfflineImage transfers") + cmd.Flags().BoolVar(&opt.PatchModel, "patch-model", false, "Patch only spec.models on Microservice YAML (catalog PATCH). Controller decides whether the workload is rebuilt.") + cmd.Flags().BoolVar(&opt.PatchKnowledge, "patch-knowledge", false, "Patch only spec.knowledge on Microservice YAML (catalog PATCH). Controller rebuilds on empty↔non-empty catalog or bindPath/permissions change; item-only changes do not rebuild. Combinable with --patch-model.") return cmd } diff --git a/internal/cmd/describe.go b/internal/cmd/describe.go index 78139d44c..326e51763 100644 --- a/internal/cmd/describe.go +++ b/internal/cmd/describe.go @@ -40,6 +40,10 @@ Most resources require a working Controller in the Namespace in order to be desc newDescribeNatsUserCommand(), newDescribeNatsAccountRuleCommand(), newDescribeNatsUserRuleCommand(), + newDescribeKnowledgeCommand(), + newDescribeModelCommand(), + newDescribeRuntimeClassCommand(), + newDescribeMicroserviceTemplateCommand(), ) // Register Flags diff --git a/internal/cmd/describe_knowledge.go b/internal/cmd/describe_knowledge.go new file mode 100644 index 000000000..1b4be78c7 --- /dev/null +++ b/internal/cmd/describe_knowledge.go @@ -0,0 +1,36 @@ +package cmd + +import ( + "github.com/eclipse-iofog/iofogctl/internal/describe" + "github.com/eclipse-iofog/iofogctl/pkg/util" + "github.com/spf13/cobra" +) + +func newDescribeKnowledgeCommand() *cobra.Command { + opt := describe.Options{ + Resource: "knowledge", + } + + cmd := &cobra.Command{ + Use: "knowledge NAME", + Short: "Get detailed information about Knowledge", + Long: `Get detailed information about Knowledge.`, + Example: ex(`%[1]s describe knowledge NAME`), + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + var err error + opt.Name = args[0] + opt.Namespace, err = cmd.Flags().GetString("namespace") + util.Check(err) + + exe, err := describe.NewExecutor(&opt) + util.Check(err) + + err = exe.Execute() + util.Check(err) + }, + } + cmd.Flags().StringVarP(&opt.Filename, "output-file", "o", "", "YAML output file") + + return cmd +} diff --git a/internal/cmd/describe_microservice_template.go b/internal/cmd/describe_microservice_template.go new file mode 100644 index 000000000..a84387ef7 --- /dev/null +++ b/internal/cmd/describe_microservice_template.go @@ -0,0 +1,36 @@ +package cmd + +import ( + "github.com/eclipse-iofog/iofogctl/internal/describe" + "github.com/eclipse-iofog/iofogctl/pkg/util" + "github.com/spf13/cobra" +) + +func newDescribeMicroserviceTemplateCommand() *cobra.Command { + opt := describe.Options{ + Resource: "microservice-template", + } + + cmd := &cobra.Command{ + Use: "microservice-template NAME", + Short: "Get detailed information about a Microservice Template", + Long: `Get detailed information about a Microservice Template.`, + Example: ex(`%[1]s describe microservice-template NAME`), + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + var err error + opt.Name = args[0] + opt.Namespace, err = cmd.Flags().GetString("namespace") + util.Check(err) + + exe, err := describe.NewExecutor(&opt) + util.Check(err) + + err = exe.Execute() + util.Check(err) + }, + } + cmd.Flags().StringVarP(&opt.Filename, "output-file", "o", "", "YAML output file") + + return cmd +} diff --git a/internal/cmd/describe_model.go b/internal/cmd/describe_model.go new file mode 100644 index 000000000..b82c139c1 --- /dev/null +++ b/internal/cmd/describe_model.go @@ -0,0 +1,36 @@ +package cmd + +import ( + "github.com/eclipse-iofog/iofogctl/internal/describe" + "github.com/eclipse-iofog/iofogctl/pkg/util" + "github.com/spf13/cobra" +) + +func newDescribeModelCommand() *cobra.Command { + opt := describe.Options{ + Resource: "model", + } + + cmd := &cobra.Command{ + Use: "model NAME", + Short: "Get detailed information about a Model", + Long: `Get detailed information about a Model.`, + Example: ex(`%[1]s describe model NAME`), + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + var err error + opt.Name = args[0] + opt.Namespace, err = cmd.Flags().GetString("namespace") + util.Check(err) + + exe, err := describe.NewExecutor(&opt) + util.Check(err) + + err = exe.Execute() + util.Check(err) + }, + } + cmd.Flags().StringVarP(&opt.Filename, "output-file", "o", "", "YAML output file") + + return cmd +} diff --git a/internal/cmd/describe_runtimeclass.go b/internal/cmd/describe_runtimeclass.go new file mode 100644 index 000000000..95db9eb70 --- /dev/null +++ b/internal/cmd/describe_runtimeclass.go @@ -0,0 +1,36 @@ +package cmd + +import ( + "github.com/eclipse-iofog/iofogctl/internal/describe" + "github.com/eclipse-iofog/iofogctl/pkg/util" + "github.com/spf13/cobra" +) + +func newDescribeRuntimeClassCommand() *cobra.Command { + opt := describe.Options{ + Resource: "runtimeclass", + } + + cmd := &cobra.Command{ + Use: "runtimeclass NAME", + Short: "Get detailed information about a RuntimeClass", + Long: `Get detailed information about a RuntimeClass.`, + Example: ex(`%[1]s describe runtimeclass NAME`), + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + var err error + opt.Name = args[0] + opt.Namespace, err = cmd.Flags().GetString("namespace") + util.Check(err) + + exe, err := describe.NewExecutor(&opt) + util.Check(err) + + err = exe.Execute() + util.Check(err) + }, + } + cmd.Flags().StringVarP(&opt.Filename, "output-file", "o", "", "YAML output file") + + return cmd +} diff --git a/internal/cmd/detach.go b/internal/cmd/detach.go index 1a74f3ecb..cbd585b3b 100644 --- a/internal/cmd/detach.go +++ b/internal/cmd/detach.go @@ -17,6 +17,9 @@ func newDetachCommand() *cobra.Command { newDetachAgentCommand(), newDetachVolumeMountCommand(), newDetachExecCommand(), + newDetachKnowledgeCommand(), + newDetachModelCommand(), + newDetachRuntimeClassCommand(), ) return cmd diff --git a/internal/cmd/detach_knowledge.go b/internal/cmd/detach_knowledge.go new file mode 100644 index 000000000..ab3462406 --- /dev/null +++ b/internal/cmd/detach_knowledge.go @@ -0,0 +1,37 @@ +package cmd + +import ( + "fmt" + "strings" + + detach "github.com/eclipse-iofog/iofogctl/internal/detach/knowledge" + "github.com/eclipse-iofog/iofogctl/pkg/util" + "github.com/spf13/cobra" +) + +func newDetachKnowledgeCommand() *cobra.Command { + opt := detach.Options{} + cmd := &cobra.Command{ + Use: "knowledge NAME AGENT_NAME1 AGENT_NAME2", + Short: "Detach Knowledge from existing Agents", + Long: `Detach Knowledge from existing Agents.`, + Example: ex(`%[1]s detach knowledge NAME AGENT_NAME1 AGENT_NAME2`), + Args: cobra.MinimumNArgs(2), + Run: func(cmd *cobra.Command, args []string) { + opt.Name = args[0] + opt.Agents = args[1:] + var err error + opt.Namespace, err = cmd.Flags().GetString("namespace") + util.Check(err) + + exe := detach.NewExecutor(opt) + err = exe.Execute() + util.Check(err) + + msg := fmt.Sprintf("Successfully detached Knowledge %s from Agents %s", opt.Name, strings.Join(opt.Agents, ", ")) + util.PrintSuccess(msg) + }, + } + + return cmd +} diff --git a/internal/cmd/detach_model.go b/internal/cmd/detach_model.go new file mode 100644 index 000000000..4584e2847 --- /dev/null +++ b/internal/cmd/detach_model.go @@ -0,0 +1,37 @@ +package cmd + +import ( + "fmt" + "strings" + + detach "github.com/eclipse-iofog/iofogctl/internal/detach/model" + "github.com/eclipse-iofog/iofogctl/pkg/util" + "github.com/spf13/cobra" +) + +func newDetachModelCommand() *cobra.Command { + opt := detach.Options{} + cmd := &cobra.Command{ + Use: "model NAME AGENT_NAME1 AGENT_NAME2", + Short: "Detach a Model from existing Agents", + Long: `Detach a Model from existing Agents.`, + Example: ex(`%[1]s detach model NAME AGENT_NAME1 AGENT_NAME2`), + Args: cobra.MinimumNArgs(2), + Run: func(cmd *cobra.Command, args []string) { + opt.Name = args[0] + opt.Agents = args[1:] + var err error + opt.Namespace, err = cmd.Flags().GetString("namespace") + util.Check(err) + + exe := detach.NewExecutor(opt) + err = exe.Execute() + util.Check(err) + + msg := fmt.Sprintf("Successfully detached Model %s from Agents %s", opt.Name, strings.Join(opt.Agents, ", ")) + util.PrintSuccess(msg) + }, + } + + return cmd +} diff --git a/internal/cmd/detach_runtimeclass.go b/internal/cmd/detach_runtimeclass.go new file mode 100644 index 000000000..53b618f3f --- /dev/null +++ b/internal/cmd/detach_runtimeclass.go @@ -0,0 +1,37 @@ +package cmd + +import ( + "fmt" + "strings" + + detach "github.com/eclipse-iofog/iofogctl/internal/detach/runtimeclass" + "github.com/eclipse-iofog/iofogctl/pkg/util" + "github.com/spf13/cobra" +) + +func newDetachRuntimeClassCommand() *cobra.Command { + opt := detach.Options{} + cmd := &cobra.Command{ + Use: "runtimeclass NAME AGENT_NAME1 AGENT_NAME2", + Short: "Detach a RuntimeClass from existing Agents", + Long: `Detach a RuntimeClass from existing Agents.`, + Example: ex(`%[1]s detach runtimeclass NAME AGENT_NAME1 AGENT_NAME2`), + Args: cobra.MinimumNArgs(2), + Run: func(cmd *cobra.Command, args []string) { + opt.Name = args[0] + opt.Agents = args[1:] + var err error + opt.Namespace, err = cmd.Flags().GetString("namespace") + util.Check(err) + + exe := detach.NewExecutor(opt) + err = exe.Execute() + util.Check(err) + + msg := fmt.Sprintf("Successfully detached RuntimeClass %s from Agents %s", opt.Name, strings.Join(opt.Agents, ", ")) + util.PrintSuccess(msg) + }, + } + + return cmd +} diff --git a/internal/cmd/get.go b/internal/cmd/get.go index 05a137328..73943ea73 100644 --- a/internal/cmd/get.go +++ b/internal/cmd/get.go @@ -37,6 +37,10 @@ func newGetCommand() *cobra.Command { "nats-user-rules", "auth-groups", "auth-group", + "knowledge", + "models", + "runtimeclass", + "microservice-templates", } cmd := &cobra.Command{ Use: "get RESOURCE [NAME]", @@ -69,7 +73,11 @@ Resources like Agents will require a working Controller in the namespace to disp nats-account-rules nats-user-rules auth-groups - auth-group NAME`), + auth-group NAME + knowledge + models + runtimeclass + microservice-templates`), ValidArgs: validResources, Args: func(cmd *cobra.Command, args []string) error { if len(args) < 1 || len(args) > 2 { diff --git a/internal/config/types.go b/internal/config/types.go index d5fbc6d1e..5dce8f142 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -24,6 +24,10 @@ const ( MicroserviceKind Kind = Kind(apps.MicroserviceKind) ApplicationKind Kind = Kind(apps.ApplicationKind) ApplicationTemplateKind Kind = Kind(apps.ApplicationTemplateKind) + ModelKind Kind = Kind(apps.ModelKind) + KnowledgeKind Kind = Kind(apps.KnowledgeKind) + RuntimeClassKind Kind = Kind(apps.RuntimeClassKind) + MicroserviceTemplateKind Kind = Kind(apps.MicroserviceTemplateKind) NatsAccountRuleKind Kind = "NatsAccountRule" NatsUserRuleKind Kind = "NatsUserRule" SecretKind Kind = "Secret" @@ -43,6 +47,7 @@ type Header struct { APIVersion string `yaml:"apiVersion" json:"apiVersion"` Kind Kind `yaml:"kind" json:"kind"` Metadata HeaderMetadata `yaml:"metadata" json:"metadata"` + Handler string `yaml:"handler,omitempty" json:"handler,omitempty"` // RuntimeClass root field Spec interface{} `yaml:"spec,omitempty" json:"spec,omitempty"` Data interface{} `yaml:"data,omitempty" json:"data,omitempty"` Status interface{} `yaml:"status,omitempty" json:"status,omitempty"` diff --git a/internal/delete/knowledge/knowledge.go b/internal/delete/knowledge/knowledge.go new file mode 100644 index 000000000..18fdf10f6 --- /dev/null +++ b/internal/delete/knowledge/knowledge.go @@ -0,0 +1,35 @@ +package deleteknowledge + +import ( + "github.com/eclipse-iofog/iofogctl/internal/execute" + clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +type Executor struct { + namespace string + name string +} + +func NewExecutor(namespace, name string) (execute.Executor, error) { + exe := &Executor{ + namespace: namespace, + name: name, + } + + return exe, nil +} + +func (exe *Executor) GetName() string { + return exe.name +} + +func (exe *Executor) Execute() error { + util.SpinStart("Deleting Knowledge") + clt, err := clientutil.NewControllerClient(exe.namespace) + if err != nil { + return err + } + + return clt.DeleteKnowledge(exe.name) +} diff --git a/internal/delete/microservicetemplate/microservice_template.go b/internal/delete/microservicetemplate/microservice_template.go new file mode 100644 index 000000000..3a6b36da9 --- /dev/null +++ b/internal/delete/microservicetemplate/microservice_template.go @@ -0,0 +1,35 @@ +package deletemicroservicetemplate + +import ( + "github.com/eclipse-iofog/iofogctl/internal/execute" + clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +type Executor struct { + namespace string + name string +} + +func NewExecutor(namespace, name string) (execute.Executor, error) { + exe := &Executor{ + namespace: namespace, + name: name, + } + + return exe, nil +} + +func (exe *Executor) GetName() string { + return exe.name +} + +func (exe *Executor) Execute() error { + util.SpinStart("Deleting Microservice Template") + clt, err := clientutil.NewControllerClient(exe.namespace) + if err != nil { + return err + } + + return clt.DeleteMicroserviceTemplate(exe.name) +} diff --git a/internal/delete/model/model.go b/internal/delete/model/model.go new file mode 100644 index 000000000..e4f64deba --- /dev/null +++ b/internal/delete/model/model.go @@ -0,0 +1,35 @@ +package deletemodel + +import ( + "github.com/eclipse-iofog/iofogctl/internal/execute" + clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +type Executor struct { + namespace string + name string +} + +func NewExecutor(namespace, name string) (execute.Executor, error) { + exe := &Executor{ + namespace: namespace, + name: name, + } + + return exe, nil +} + +func (exe *Executor) GetName() string { + return exe.name +} + +func (exe *Executor) Execute() error { + util.SpinStart("Deleting Model") + clt, err := clientutil.NewControllerClient(exe.namespace) + if err != nil { + return err + } + + return clt.DeleteModel(exe.name) +} diff --git a/internal/delete/runtimeclass/runtimeclass.go b/internal/delete/runtimeclass/runtimeclass.go new file mode 100644 index 000000000..a886b6359 --- /dev/null +++ b/internal/delete/runtimeclass/runtimeclass.go @@ -0,0 +1,35 @@ +package deleteruntimeclass + +import ( + "github.com/eclipse-iofog/iofogctl/internal/execute" + clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +type Executor struct { + namespace string + name string +} + +func NewExecutor(namespace, name string) (execute.Executor, error) { + exe := &Executor{ + namespace: namespace, + name: name, + } + + return exe, nil +} + +func (exe *Executor) GetName() string { + return exe.name +} + +func (exe *Executor) Execute() error { + util.SpinStart("Deleting RuntimeClass") + clt, err := clientutil.NewControllerClient(exe.namespace) + if err != nil { + return err + } + + return clt.DeleteRuntimeClass(exe.name) +} diff --git a/internal/deploy/catalogitem/catalog_item.go b/internal/deploy/catalogitem/catalog_item.go index 4276624ef..412fddf51 100644 --- a/internal/deploy/catalogitem/catalog_item.go +++ b/internal/deploy/catalogitem/catalog_item.go @@ -64,12 +64,7 @@ func (exe *remoteExecutor) updateCatalogItem(clt *client.Client, existing *clien Description: exe.catalogItem.Description, } - if exe.catalogItem.Registry != "" { - request.RegistryID, err = clientutil.ResolveRegistryID(exe.catalogItem.Registry) - if err != nil { - return err - } - } + request.RegistryID = exe.catalogItem.Registry.Int() if _, err = clt.UpdateCatalogItem(&request); err != nil { return err @@ -79,15 +74,10 @@ func (exe *remoteExecutor) updateCatalogItem(clt *client.Client, existing *clien } func (exe *remoteExecutor) createCatalogItem(clt *client.Client) (err error) { - registryID, err := clientutil.ResolveRegistryID(exe.catalogItem.Registry) - if err != nil { - return err - } - if _, err = clt.CreateCatalogItem(&client.CatalogItemCreateRequest{ Name: exe.catalogItem.Name, Images: buildCatalogImages(exe.catalogItem), - RegistryID: registryID, + RegistryID: exe.catalogItem.Registry.Int(), Description: exe.catalogItem.Description, }); err != nil { return err @@ -154,9 +144,5 @@ func validate(opt *apps.CatalogItem) error { return util.NewInputError("At least one image must be specified") } - if _, err := clientutil.ResolveRegistryID(opt.Registry); err != nil { - return err - } - return nil } diff --git a/internal/deploy/execute.go b/internal/deploy/execute.go index cb6b1cfcc..452c990c5 100644 --- a/internal/deploy/execute.go +++ b/internal/deploy/execute.go @@ -18,13 +18,17 @@ import ( deployk8scontrolplane "github.com/eclipse-iofog/iofogctl/internal/deploy/controlplane/k8s" deploylocalcontrolplane "github.com/eclipse-iofog/iofogctl/internal/deploy/controlplane/local" deployremotecontrolplane "github.com/eclipse-iofog/iofogctl/internal/deploy/controlplane/remote" + deployknowledge "github.com/eclipse-iofog/iofogctl/internal/deploy/knowledge" deploymicroservice "github.com/eclipse-iofog/iofogctl/internal/deploy/microservice" + deploymicroservicetemplate "github.com/eclipse-iofog/iofogctl/internal/deploy/microservicetemplate" + deploymodel "github.com/eclipse-iofog/iofogctl/internal/deploy/model" deploynatsaccountrule "github.com/eclipse-iofog/iofogctl/internal/deploy/natsaccountrule" deploynatsuserrule "github.com/eclipse-iofog/iofogctl/internal/deploy/natsuserrule" deployofflineimage "github.com/eclipse-iofog/iofogctl/internal/deploy/offlineimage" deployregistry "github.com/eclipse-iofog/iofogctl/internal/deploy/registry" deployrole "github.com/eclipse-iofog/iofogctl/internal/deploy/role" deployrolebinding "github.com/eclipse-iofog/iofogctl/internal/deploy/rolebinding" + deployruntimeclass "github.com/eclipse-iofog/iofogctl/internal/deploy/runtimeclass" deploysecret "github.com/eclipse-iofog/iofogctl/internal/deploy/secret" deployservice "github.com/eclipse-iofog/iofogctl/internal/deploy/service" deployserviceaccount "github.com/eclipse-iofog/iofogctl/internal/deploy/serviceaccount" @@ -56,6 +60,10 @@ var kindOrder = []config.Kind{ config.OfflineImageKind, config.VolumeMountKind, config.RegistryKind, + config.ModelKind, + config.KnowledgeKind, + config.RuntimeClassKind, + config.MicroserviceTemplateKind, config.CatalogItemKind, config.ApplicationKind, config.MicroserviceKind, @@ -63,10 +71,12 @@ var kindOrder = []config.Kind{ } type Options struct { - Namespace string - InputFile string - NoCache bool - TransferPool int + Namespace string + InputFile string + NoCache bool + TransferPool int + PatchModel bool + PatchKnowledge bool } func deployCatalogItem(opt *execute.KindHandlerOpt) (exe execute.Executor, err error) { @@ -81,8 +91,16 @@ func deployApplication(opt *execute.KindHandlerOpt) (exe execute.Executor, err e return deployapplication.NewExecutor(deployapplication.Options{Namespace: opt.Namespace, Yaml: opt.YAML, Name: opt.Name}) } -func deployMicroservice(opt *execute.KindHandlerOpt) (exe execute.Executor, err error) { - return deploymicroservice.NewExecutor(deploymicroservice.Options{Namespace: opt.Namespace, Yaml: opt.YAML, Name: opt.Name}) +func deployMicroservice(patchModel, patchKnowledge bool) func(*execute.KindHandlerOpt) (execute.Executor, error) { + return func(opt *execute.KindHandlerOpt) (execute.Executor, error) { + return deploymicroservice.NewExecutor(deploymicroservice.Options{ + Namespace: opt.Namespace, + Yaml: opt.YAML, + Name: opt.Name, + PatchModel: patchModel, + PatchKnowledge: patchKnowledge, + }) + } } func deployKubernetesControlPlane(opt *execute.KindHandlerOpt) (exe execute.Executor, err error) { @@ -121,6 +139,27 @@ func deployRegistry(opt *execute.KindHandlerOpt) (exe execute.Executor, err erro return deployregistry.NewExecutor(deployregistry.Options{Namespace: opt.Namespace, Yaml: opt.YAML, Name: opt.Name}) } +func deployModel(opt *execute.KindHandlerOpt) (exe execute.Executor, err error) { + return deploymodel.NewExecutor(deploymodel.Options{Namespace: opt.Namespace, Yaml: opt.YAML, Name: opt.Name}) +} + +func deployKnowledge(opt *execute.KindHandlerOpt) (exe execute.Executor, err error) { + return deployknowledge.NewExecutor(deployknowledge.Options{Namespace: opt.Namespace, Yaml: opt.YAML, Name: opt.Name}) +} + +func deployRuntimeClass(opt *execute.KindHandlerOpt) (exe execute.Executor, err error) { + return deployruntimeclass.NewExecutor(deployruntimeclass.Options{ + Namespace: opt.Namespace, + Yaml: opt.YAML, + FullYAML: opt.FullYAML, + Name: opt.Name, + }) +} + +func deployMicroserviceTemplate(opt *execute.KindHandlerOpt) (exe execute.Executor, err error) { + return deploymicroservicetemplate.NewExecutor(deploymicroservicetemplate.Options{Namespace: opt.Namespace, Yaml: opt.YAML, Name: opt.Name}) +} + func deployVolume(opt *execute.KindHandlerOpt) (exe execute.Executor, err error) { return deployvolume.NewExecutor(deployvolume.Options{Namespace: opt.Namespace, Yaml: opt.YAML, Name: opt.Name}) } @@ -177,11 +216,16 @@ func deployNatsUserRule(opt *execute.KindHandlerOpt) (exe execute.Executor, err // Execute deploy from yaml file func Execute(opt *Options) (err error) { - kindHandlers := buildKindHandlers(opt.NoCache, opt.TransferPool) + kindHandlers := buildKindHandlers(opt.NoCache, opt.TransferPool, opt.PatchModel, opt.PatchKnowledge) executorsMap, err := execute.GetExecutorsFromYAML(opt.InputFile, opt.Namespace, kindHandlers, false) if err != nil { return err } + if opt.PatchModel || opt.PatchKnowledge { + if err := validatePatchCatalogExecutors(executorsMap, opt.PatchModel, opt.PatchKnowledge); err != nil { + return err + } + } // Create any AgentConfig executor missing. // Each Agent requires a corresponding Agent Config to be created with Controller. @@ -304,11 +348,11 @@ func Execute(opt *Options) (err error) { return nil } -func buildKindHandlers(noCache bool, transferPool int) map[config.Kind]func(*execute.KindHandlerOpt) (execute.Executor, error) { +func buildKindHandlers(noCache bool, transferPool int, patchModel, patchKnowledge bool) map[config.Kind]func(*execute.KindHandlerOpt) (execute.Executor, error) { handlers := map[config.Kind]func(*execute.KindHandlerOpt) (execute.Executor, error){ config.ApplicationKind: deployApplication, config.ApplicationTemplateKind: deployApplicationTemplate, - config.MicroserviceKind: deployMicroservice, + config.MicroserviceKind: deployMicroservice(patchModel, patchKnowledge), config.CatalogItemKind: deployCatalogItem, config.KubernetesControlPlaneKind: deployKubernetesControlPlane, config.RemoteControlPlaneKind: deployRemoteControlPlane, @@ -319,6 +363,10 @@ func buildKindHandlers(noCache bool, transferPool int) map[config.Kind]func(*exe config.LocalAgentKind: deployLocalAgent, config.AgentConfigKind: deployAgentConfig, config.RegistryKind: deployRegistry, + config.ModelKind: deployModel, + config.KnowledgeKind: deployKnowledge, + config.RuntimeClassKind: deployRuntimeClass, + config.MicroserviceTemplateKind: deployMicroserviceTemplate, config.VolumeKind: deployVolume, config.SecretKind: deploySecret, config.ConfigMapKind: deployConfigMap, @@ -344,6 +392,34 @@ func buildKindHandlers(noCache bool, transferPool int) map[config.Kind]func(*exe return handlers } +func validatePatchCatalogExecutors(executorsMap map[config.Kind][]execute.Executor, patchModel, patchKnowledge bool) error { + if !patchModel && !patchKnowledge { + return nil + } + flags := patchCatalogFlagNames(patchModel, patchKnowledge) + hasMicroservice := len(executorsMap[config.MicroserviceKind]) > 0 + if !hasMicroservice { + return util.NewInputError(fmt.Sprintf("%s requires a Microservice YAML file", flags)) + } + for kind, exes := range executorsMap { + if kind != config.MicroserviceKind && len(exes) > 0 { + return util.NewInputError(fmt.Sprintf("%s requires a Microservice-only YAML file", flags)) + } + } + return nil +} + +func patchCatalogFlagNames(patchModel, patchKnowledge bool) string { + switch { + case patchModel && patchKnowledge: + return "--patch-model / --patch-knowledge" + case patchKnowledge: + return "--patch-knowledge" + default: + return "--patch-model" + } +} + func deployAgentConfiguration(executors []execute.Executor) (err error) { if len(executors) == 0 { return nil diff --git a/internal/deploy/execute_kind_order_test.go b/internal/deploy/execute_kind_order_test.go new file mode 100644 index 000000000..fe6ad60ee --- /dev/null +++ b/internal/deploy/execute_kind_order_test.go @@ -0,0 +1,70 @@ +package deploy + +import ( + "testing" + + "github.com/eclipse-iofog/iofogctl/internal/config" + "github.com/eclipse-iofog/iofogctl/internal/execute" + "github.com/stretchr/testify/require" +) + +func TestKindOrderPlacesV39KindsAfterRegistryBeforeApplication(t *testing.T) { + idx := map[config.Kind]int{} + for i, k := range kindOrder { + idx[k] = i + } + require.Contains(t, idx, config.RegistryKind) + require.Contains(t, idx, config.ModelKind) + require.Contains(t, idx, config.KnowledgeKind) + require.Contains(t, idx, config.RuntimeClassKind) + require.Contains(t, idx, config.MicroserviceTemplateKind) + require.Contains(t, idx, config.ApplicationKind) + require.Contains(t, idx, config.MicroserviceKind) + + require.Less(t, idx[config.RegistryKind], idx[config.ModelKind]) + require.Less(t, idx[config.ModelKind], idx[config.KnowledgeKind]) + require.Less(t, idx[config.KnowledgeKind], idx[config.RuntimeClassKind]) + require.Less(t, idx[config.RuntimeClassKind], idx[config.MicroserviceTemplateKind]) + require.Less(t, idx[config.MicroserviceTemplateKind], idx[config.ApplicationKind]) + require.Less(t, idx[config.ApplicationKind], idx[config.MicroserviceKind]) +} + +func TestBuildKindHandlersIncludesV39Kinds(t *testing.T) { + handlers := buildKindHandlers(false, 2, false, false) + require.Contains(t, handlers, config.ModelKind) + require.Contains(t, handlers, config.KnowledgeKind) + require.Contains(t, handlers, config.RuntimeClassKind) + require.Contains(t, handlers, config.MicroserviceTemplateKind) +} + +func TestValidatePatchCatalogExecutorsMicroserviceOnly(t *testing.T) { + require.NoError(t, validatePatchCatalogExecutors(map[config.Kind][]execute.Executor{ + config.MicroserviceKind: {execute.NewEmptyExecutor("app/ms")}, + }, true, false)) + require.NoError(t, validatePatchCatalogExecutors(map[config.Kind][]execute.Executor{ + config.MicroserviceKind: {execute.NewEmptyExecutor("app/ms")}, + }, false, true)) + require.NoError(t, validatePatchCatalogExecutors(map[config.Kind][]execute.Executor{ + config.MicroserviceKind: {execute.NewEmptyExecutor("app/ms")}, + }, true, true)) +} + +func TestValidatePatchCatalogExecutorsRejectsOtherKinds(t *testing.T) { + err := validatePatchCatalogExecutors(map[config.Kind][]execute.Executor{ + config.MicroserviceKind: {execute.NewEmptyExecutor("app/ms")}, + config.ModelKind: {execute.NewEmptyExecutor("llama")}, + }, true, true) + require.Error(t, err) + require.Contains(t, err.Error(), "Microservice-only") + require.Contains(t, err.Error(), "--patch-model") + require.Contains(t, err.Error(), "--patch-knowledge") +} + +func TestValidatePatchCatalogExecutorsRequiresMicroservice(t *testing.T) { + err := validatePatchCatalogExecutors(map[config.Kind][]execute.Executor{ + config.ModelKind: {execute.NewEmptyExecutor("llama")}, + }, false, true) + require.Error(t, err) + require.Contains(t, err.Error(), "Microservice YAML") + require.Contains(t, err.Error(), "--patch-knowledge") +} diff --git a/internal/deploy/knowledge/factory.go b/internal/deploy/knowledge/factory.go new file mode 100644 index 000000000..f5f2e92a2 --- /dev/null +++ b/internal/deploy/knowledge/factory.go @@ -0,0 +1,160 @@ +package deployknowledge + +import ( + "fmt" + "strings" + + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + "github.com/eclipse-iofog/iofogctl/internal/config" + "github.com/eclipse-iofog/iofogctl/internal/execute" + clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" + "github.com/eclipse-iofog/iofogctl/pkg/util" + "gopkg.in/yaml.v2" +) + +type Options struct { + Namespace string + Yaml []byte + Name string +} + +// knowledgeSpec is the kind: Knowledge spec. registry is an alias of registryId (P11-YAML-1). +type knowledgeSpec struct { + Repo string `yaml:"repo"` + Revision string `yaml:"revision,omitempty"` + RegistryID int `yaml:"registryId,omitempty"` + Registry string `yaml:"registry,omitempty"` + Files []string `yaml:"files,omitempty"` + Format string `yaml:"format,omitempty"` +} + +type remoteExecutor struct { + namespace string + name string + spec knowledgeSpec + registryID int +} + +func (exe *remoteExecutor) GetName() string { + return exe.name +} + +func (exe *remoteExecutor) Execute() error { + util.SpinStart(fmt.Sprintf("Deploying knowledge %s", exe.GetName())) + clt, err := clientutil.NewControllerClient(exe.namespace) + if err != nil { + return err + } + + _, err = clt.GetKnowledge(exe.name) + if err != nil { + if !clientutil.IsClientNotFoundError(err) { + return err + } + return exe.createKnowledge(clt) + } + return exe.updateKnowledge(clt) +} + +func (exe *remoteExecutor) createKnowledge(clt *client.Client) error { + _, err := clt.CreateKnowledge(&client.KnowledgeCreateRequest{ + Name: exe.name, + Repo: exe.spec.Repo, + RegistryID: exe.registryID, + Revision: exe.spec.Revision, + Files: exe.spec.Files, + Format: exe.spec.Format, + }) + return err +} + +func (exe *remoteExecutor) updateKnowledge(clt *client.Client) error { + repo := exe.spec.Repo + revision := exe.spec.Revision + format := exe.spec.Format + registryID := exe.registryID + _, err := clt.UpdateKnowledge(exe.name, &client.KnowledgeUpdateRequest{ + Repo: &repo, + Revision: &revision, + RegistryID: ®istryID, + Files: exe.spec.Files, + Format: &format, + }) + return err +} + +func NewExecutor(opt Options) (exe execute.Executor, err error) { + ns, err := config.GetNamespace(opt.Namespace) + if err != nil { + return exe, err + } + if len(ns.GetControllers()) == 0 { + return exe, util.NewInputError("This namespace does not have a Controller. You must first deploy a Controller before deploying Knowledge") + } + + spec, err := parseKnowledgeSpec(opt.Yaml) + if err != nil { + return nil, err + } + if len(opt.Name) > 0 { + if err := util.IsLowerAlphanumeric("Knowledge", opt.Name); err != nil { + return nil, err + } + } else { + return nil, util.NewInputError("Name must be specified") + } + + registryID, err := resolveKnowledgeRegistryID(spec) + if err != nil { + return nil, err + } + if err := validateKnowledgeSpec(spec); err != nil { + return nil, err + } + + return &remoteExecutor{ + namespace: opt.Namespace, + name: opt.Name, + spec: spec, + registryID: registryID, + }, nil +} + +func parseKnowledgeSpec(raw []byte) (knowledgeSpec, error) { + var spec knowledgeSpec + if err := yaml.UnmarshalStrict(raw, &spec); err != nil { + return spec, util.NewUnmarshalError(err.Error()) + } + return spec, nil +} + +func resolveKnowledgeRegistryID(spec knowledgeSpec) (int, error) { + alias := strings.TrimSpace(spec.Registry) + var fromAlias int + var hasAlias bool + if alias != "" { + id, err := clientutil.ResolveRegistryID(alias) + if err != nil { + return 0, err + } + fromAlias = id + hasAlias = true + } + if spec.RegistryID > 0 && hasAlias && spec.RegistryID != fromAlias { + return 0, util.NewInputError("spec.registry and spec.registryId must refer to the same registry") + } + if hasAlias { + return fromAlias, nil + } + if spec.RegistryID > 0 { + return spec.RegistryID, nil + } + return 0, util.NewInputError("spec.registry or spec.registryId must be specified") +} + +func validateKnowledgeSpec(spec knowledgeSpec) error { + if strings.TrimSpace(spec.Repo) == "" { + return util.NewInputError("repo must be specified") + } + return nil +} diff --git a/internal/deploy/knowledge/factory_test.go b/internal/deploy/knowledge/factory_test.go new file mode 100644 index 000000000..1141e8efc --- /dev/null +++ b/internal/deploy/knowledge/factory_test.go @@ -0,0 +1,78 @@ +package deployknowledge + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestParseKnowledgeSpecRegistryAlias(t *testing.T) { + raw := []byte(` +repo: acme/product-manuals +revision: 9f3c111122223333444455556666777788889999 +registry: 1 +files: [] +format: jsonl +`) + spec, err := parseKnowledgeSpec(raw) + require.NoError(t, err) + id, err := resolveKnowledgeRegistryID(spec) + require.NoError(t, err) + require.Equal(t, 1, id) + require.Equal(t, "acme/product-manuals", spec.Repo) + require.Equal(t, "jsonl", spec.Format) +} + +func TestParseKnowledgeSpecRegistryID(t *testing.T) { + raw := []byte(` +repo: org/docs +registryId: 2 +`) + spec, err := parseKnowledgeSpec(raw) + require.NoError(t, err) + id, err := resolveKnowledgeRegistryID(spec) + require.NoError(t, err) + require.Equal(t, 2, id) +} + +func TestParseKnowledgeSpecRegistryNamedAlias(t *testing.T) { + raw := []byte(` +repo: org/docs +registry: remote +`) + spec, err := parseKnowledgeSpec(raw) + require.NoError(t, err) + id, err := resolveKnowledgeRegistryID(spec) + require.NoError(t, err) + require.Equal(t, 1, id) +} + +func TestParseKnowledgeSpecConflictingRegistry(t *testing.T) { + raw := []byte(` +repo: org/docs +registry: 1 +registryId: 2 +`) + spec, err := parseKnowledgeSpec(raw) + require.NoError(t, err) + _, err = resolveKnowledgeRegistryID(spec) + require.Error(t, err) + require.Contains(t, err.Error(), "same registry") +} + +func TestParseKnowledgeSpecMissingRegistry(t *testing.T) { + raw := []byte(` +repo: org/docs +`) + spec, err := parseKnowledgeSpec(raw) + require.NoError(t, err) + _, err = resolveKnowledgeRegistryID(spec) + require.Error(t, err) + require.Contains(t, err.Error(), "registry") +} + +func TestValidateKnowledgeSpecRequiresRepo(t *testing.T) { + err := validateKnowledgeSpec(knowledgeSpec{RegistryID: 1}) + require.Error(t, err) + require.Contains(t, err.Error(), "repo") +} diff --git a/internal/deploy/microservice/factory.go b/internal/deploy/microservice/factory.go index 51b5dd6af..80e84a39c 100644 --- a/internal/deploy/microservice/factory.go +++ b/internal/deploy/microservice/factory.go @@ -5,6 +5,7 @@ import ( "strings" apps "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/apps" + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" "github.com/eclipse-iofog/iofogctl/internal/config" "github.com/eclipse-iofog/iofogctl/internal/execute" clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" @@ -13,15 +14,21 @@ import ( ) type Options struct { - Namespace string - Yaml []byte - Name string + Namespace string + Yaml []byte + Name string + PatchModel bool + PatchKnowledge bool } type remoteExecutor struct { - namespace string - microservice interface{} - name string + namespace string + microservice apps.Microservice + name string + patchModel bool + patchKnowledge bool + catalog client.MicroserviceCatalog + knowledgeCatalog client.KnowledgeCatalog } func (exe *remoteExecutor) GetName() string { @@ -29,6 +36,10 @@ func (exe *remoteExecutor) GetName() string { } func (exe *remoteExecutor) Execute() error { + if exe.patchModel || exe.patchKnowledge { + return exe.patchCatalogs() + } + util.SpinStart(fmt.Sprintf("Deploying microservice %s", exe.GetName())) ns, err := config.GetNamespace(exe.namespace) if err != nil { @@ -66,7 +77,52 @@ func (exe *remoteExecutor) Execute() error { return err } - return apps.DeployMicroservice(controller, exe.microservice, appName, msvcName, apps.WithAPIVersion(util.GetCliApiVersion())) + return apps.DeployMicroservice(controller, &exe.microservice, appName, msvcName, apps.WithAPIVersion(util.GetCliApiVersion())) +} + +func (exe *remoteExecutor) patchCatalogs() error { + clt, uuid, err := exe.lookupMicroservice() + if err != nil { + return err + } + // Models first to match --patch-model then --patch-knowledge flag order. + if exe.patchModel { + if err := exe.patchModels(clt, uuid); err != nil { + return err + } + } + if exe.patchKnowledge { + if err := exe.patchKnowledgeCatalog(clt, uuid); err != nil { + return err + } + } + return nil +} + +func (exe *remoteExecutor) lookupMicroservice() (*client.Client, string, error) { + clt, err := clientutil.NewControllerClient(exe.namespace) + if err != nil { + return nil, "", err + } + appName, msvcName, err := clientutil.ParseFQName(exe.name, "Microservice") + if err != nil { + return nil, "", err + } + msvc, err := clt.GetMicroserviceByName(appName, msvcName) + if err != nil { + return nil, "", err + } + return clt, msvc.UUID, nil +} + +func (exe *remoteExecutor) patchModels(clt *client.Client, uuid string) error { + util.SpinStart(fmt.Sprintf("Patching models for microservice %s", exe.GetName())) + return clt.PatchMicroserviceModels(uuid, exe.catalog) +} + +func (exe *remoteExecutor) patchKnowledgeCatalog(clt *client.Client, uuid string) error { + util.SpinStart(fmt.Sprintf("Patching knowledge for microservice %s", exe.GetName())) + return clt.PatchMicroserviceKnowledge(uuid, exe.knowledgeCatalog) } func NewExecutor(opt Options) (exe execute.Executor, err error) { @@ -75,24 +131,115 @@ func NewExecutor(opt Options) (exe execute.Executor, err error) { return exe, err } // Unmarshal file - var microservice interface{} + var microservice apps.Microservice if err = yaml.UnmarshalStrict(opt.Yaml, µservice); err != nil { err = util.NewUnmarshalError(err.Error()) return } - name := opt.Name - if !strings.Contains(name, "/") { - if m, ok := microservice.(map[interface{}]interface{}); ok { - if app, ok := m["application"].(string); ok && app != "" { - name = app + "/" + name + name := resolveMicroserviceName(opt.Name, opt.Yaml) + + if opt.PatchModel || opt.PatchKnowledge { + remote := &remoteExecutor{ + namespace: opt.Namespace, + microservice: microservice, + name: name, + patchModel: opt.PatchModel, + patchKnowledge: opt.PatchKnowledge, + } + if opt.PatchModel { + catalog, err := parseMicroserviceModels(opt.Yaml) + if err != nil { + return nil, err + } + if catalog == nil { + return nil, util.NewInputError("--patch-model requires spec.models") } + remote.catalog = toClientCatalog(*catalog) } + if opt.PatchKnowledge { + catalog, err := parseMicroserviceKnowledge(opt.Yaml) + if err != nil { + return nil, err + } + if catalog == nil { + return nil, util.NewInputError("--patch-knowledge requires spec.knowledge") + } + remote.knowledgeCatalog = toClientKnowledgeCatalog(*catalog) + } + return remote, nil } return &remoteExecutor{ namespace: opt.Namespace, - microservice: µservice, + microservice: microservice, name: name, }, nil } + +func resolveMicroserviceName(optName string, yamlBytes []byte) string { + name := optName + if strings.Contains(name, "/") { + return name + } + var spec struct { + Application string `yaml:"application,omitempty"` + } + if err := yaml.Unmarshal(yamlBytes, &spec); err != nil { + return name + } + if spec.Application != "" { + return spec.Application + "/" + name + } + return name +} + +func parseMicroserviceModels(raw []byte) (*apps.MicroserviceCatalog, error) { + var spec struct { + Models *apps.MicroserviceCatalog `yaml:"models"` + } + if err := yaml.Unmarshal(raw, &spec); err != nil { + return nil, util.NewUnmarshalError(err.Error()) + } + return spec.Models, nil +} + +func parseMicroserviceKnowledge(raw []byte) (*apps.KnowledgeCatalog, error) { + var spec struct { + Knowledge *apps.KnowledgeCatalog `yaml:"knowledge"` + } + if err := yaml.Unmarshal(raw, &spec); err != nil { + return nil, util.NewUnmarshalError(err.Error()) + } + return spec.Knowledge, nil +} + +func toClientCatalog(in apps.MicroserviceCatalog) client.MicroserviceCatalog { + out := client.MicroserviceCatalog{ + BindPath: in.BindPath, + Permissions: in.Permissions, + } + if len(in.Items) == 0 { + return out + } + out.Items = make([]client.MicroserviceCatalogItem, len(in.Items)) + for i, item := range in.Items { + out.Items[i] = client.MicroserviceCatalogItem{Name: item.Name} + } + return out +} + +func toClientKnowledgeCatalog(in apps.KnowledgeCatalog) client.KnowledgeCatalog { + out := client.KnowledgeCatalog{ + BindPath: in.BindPath, + Permissions: in.Permissions, + } + if len(in.Items) == 0 { + return out + } + out.Items = make([]client.KnowledgeCatalogItem, len(in.Items)) + for i, item := range in.Items { + out.Items[i] = client.KnowledgeCatalogItem{Name: item.Name} + } + return out +} diff --git a/internal/deploy/microservice/factory_test.go b/internal/deploy/microservice/factory_test.go new file mode 100644 index 000000000..379d2d238 --- /dev/null +++ b/internal/deploy/microservice/factory_test.go @@ -0,0 +1,164 @@ +package deploymicroservice + +import ( + "testing" + + apps "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/apps" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v2" +) + +func TestUnmarshalMicroserviceSpecKeepsTemplateAndModels(t *testing.T) { + raw := []byte(` +template: + name: ms-template + variables: + - key: agent-name + value: lima + - key: model1 + value: tiny-gpt2 + - key: application + value: test-app +models: + bindPath: /models + permissions: ro + items: + - name: tiny-gpt2 +application: test-app +`) + var microservice apps.Microservice + require.NoError(t, yaml.UnmarshalStrict(raw, µservice)) + require.NotNil(t, microservice.Template) + require.Equal(t, "ms-template", microservice.Template.Name) + require.NotNil(t, microservice.Models) + require.Equal(t, "/models", microservice.Models.BindPath) +} + +func TestUnmarshalMicroserviceSpecKeepsServiceAccount(t *testing.T) { + raw := []byte(` +images: + registry: 1 + amd64: ghcr.io/example/app:latest +serviceAccount: + roleRef: + kind: Role + name: microservice +application: test-app +`) + var microservice apps.Microservice + require.NoError(t, yaml.UnmarshalStrict(raw, µservice)) + require.NotNil(t, microservice.ServiceAccount) + require.Equal(t, "Role", microservice.ServiceAccount.RoleRef.Kind) + require.Equal(t, "microservice", microservice.ServiceAccount.RoleRef.Name) + require.Equal(t, apps.RegistryRef(1), microservice.Images.Registry) +} + +func TestParseMicroserviceModels(t *testing.T) { + raw := []byte(` +application: test-app +models: + bindPath: /models + permissions: ro + items: + - name: tiny-gpt2 +`) + catalog, err := parseMicroserviceModels(raw) + require.NoError(t, err) + require.NotNil(t, catalog) + require.Equal(t, "/models", catalog.BindPath) + require.Equal(t, "ro", catalog.Permissions) + require.Len(t, catalog.Items, 1) + require.Equal(t, "tiny-gpt2", catalog.Items[0].Name) + + clientCatalog := toClientCatalog(*catalog) + require.Equal(t, "/models", clientCatalog.BindPath) + require.Equal(t, "ro", clientCatalog.Permissions) + require.Equal(t, "tiny-gpt2", clientCatalog.Items[0].Name) +} + +func TestParseMicroserviceModelsMissing(t *testing.T) { + catalog, err := parseMicroserviceModels([]byte("application: test-app\n")) + require.NoError(t, err) + require.Nil(t, catalog) +} + +func TestParseMicroserviceModelsEmptyObjectPresent(t *testing.T) { + catalog, err := parseMicroserviceModels([]byte("models: {}\n")) + require.NoError(t, err) + require.NotNil(t, catalog) + require.Empty(t, catalog.BindPath) + require.Empty(t, catalog.Items) +} + +func TestUnmarshalMicroserviceSpecKeepsModelsAndKnowledge(t *testing.T) { + raw := []byte(` +application: my-app +models: + bindPath: /models + permissions: ro + items: + - name: smollm2-135m +knowledge: + bindPath: /knowledge + permissions: ro + items: + - name: product-docs +`) + var microservice apps.Microservice + require.NoError(t, yaml.UnmarshalStrict(raw, µservice)) + require.NotNil(t, microservice.Models) + require.Equal(t, "/models", microservice.Models.BindPath) + require.Equal(t, "smollm2-135m", microservice.Models.Items[0].Name) + require.NotNil(t, microservice.Knowledge) + require.Equal(t, "/knowledge", microservice.Knowledge.BindPath) + require.Equal(t, "ro", microservice.Knowledge.Permissions) + require.Equal(t, "product-docs", microservice.Knowledge.Items[0].Name) +} + +func TestParseMicroserviceKnowledge(t *testing.T) { + raw := []byte(` +application: my-app +models: + bindPath: /models + permissions: ro + items: + - name: smollm2-135m +knowledge: + bindPath: /knowledge + permissions: ro + items: + - name: product-docs +`) + catalog, err := parseMicroserviceKnowledge(raw) + require.NoError(t, err) + require.NotNil(t, catalog) + require.Equal(t, "/knowledge", catalog.BindPath) + require.Equal(t, "ro", catalog.Permissions) + require.Len(t, catalog.Items, 1) + require.Equal(t, "product-docs", catalog.Items[0].Name) + + clientCatalog := toClientKnowledgeCatalog(*catalog) + require.Equal(t, "/knowledge", clientCatalog.BindPath) + require.Equal(t, "ro", clientCatalog.Permissions) + require.Equal(t, "product-docs", clientCatalog.Items[0].Name) +} + +func TestParseMicroserviceKnowledgeMissing(t *testing.T) { + catalog, err := parseMicroserviceKnowledge([]byte("application: test-app\n")) + require.NoError(t, err) + require.Nil(t, catalog) +} + +func TestParseMicroserviceKnowledgeEmptyObjectPresent(t *testing.T) { + catalog, err := parseMicroserviceKnowledge([]byte("knowledge: {}\n")) + require.NoError(t, err) + require.NotNil(t, catalog) + require.Empty(t, catalog.BindPath) + require.Empty(t, catalog.Items) +} + +func TestResolveMicroserviceNameFromApplication(t *testing.T) { + require.Equal(t, "test-app/tiny-gpt2-ms", resolveMicroserviceName("tiny-gpt2-ms", []byte("application: test-app\n"))) + require.Equal(t, "other/ms", resolveMicroserviceName("other/ms", []byte("application: test-app\n"))) + require.Equal(t, "tiny-gpt2-ms", resolveMicroserviceName("tiny-gpt2-ms", []byte("template:\n name: ms-template\n"))) +} diff --git a/internal/deploy/microservicetemplate/factory.go b/internal/deploy/microservicetemplate/factory.go new file mode 100644 index 000000000..87ac7efce --- /dev/null +++ b/internal/deploy/microservicetemplate/factory.go @@ -0,0 +1,85 @@ +package deploymicroservicetemplate + +import ( + "fmt" + + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/apps" + "github.com/eclipse-iofog/iofogctl/internal/config" + "github.com/eclipse-iofog/iofogctl/internal/execute" + clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" + "github.com/eclipse-iofog/iofogctl/pkg/util" + "gopkg.in/yaml.v2" +) + +type Options struct { + Namespace string + Yaml []byte + Name string +} + +type remoteExecutor struct { + namespace string + template interface{} + name string +} + +func (exe *remoteExecutor) GetName() string { + return exe.name +} + +func (exe *remoteExecutor) Execute() error { + util.SpinStart(fmt.Sprintf("Deploying Microservice Template %s", exe.GetName())) + + ns, err := config.GetNamespace(exe.namespace) + if err != nil { + return err + } + controlPlane, err := ns.GetControlPlane() + if err != nil { + return err + } + + if len(controlPlane.GetControllers()) == 0 { + return util.NewInputError("This namespace does not have a Controller. You must first deploy a Controller before deploying Microservice Templates") + } + + endpoint, err := controlPlane.GetEndpoint() + if err != nil { + return err + } + + clt, err := clientutil.NewControllerClient(exe.namespace) + if err != nil { + return err + } + + controller := apps.IofogController{ + Endpoint: endpoint, + Email: controlPlane.GetUser().Email, + Password: controlPlane.GetUser().Password, + Token: clt.GetAccessToken(), + RefreshToken: clt.GetRefreshToken(), + } + baseURL, err := util.GetBaseURL(controller.Endpoint) + if err != nil { + return err + } + return apps.DeployMicroserviceTemplate(controller, baseURL, exe.template, exe.name, apps.WithAPIVersion(util.GetCliApiVersion())) +} + +func NewExecutor(opt Options) (exe execute.Executor, err error) { + if _, err = config.GetNamespace(opt.Namespace); err != nil { + return exe, err + } + var template interface{} + if err = yaml.Unmarshal(opt.Yaml, &template); err != nil { + err = util.NewUnmarshalError(err.Error()) + return + } + + return &remoteExecutor{ + namespace: opt.Namespace, + template: template, + name: opt.Name, + }, nil +} diff --git a/internal/deploy/model/factory.go b/internal/deploy/model/factory.go new file mode 100644 index 000000000..3c4304f6a --- /dev/null +++ b/internal/deploy/model/factory.go @@ -0,0 +1,160 @@ +package deploymodel + +import ( + "fmt" + "strings" + + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + "github.com/eclipse-iofog/iofogctl/internal/config" + "github.com/eclipse-iofog/iofogctl/internal/execute" + clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" + "github.com/eclipse-iofog/iofogctl/pkg/util" + "gopkg.in/yaml.v2" +) + +type Options struct { + Namespace string + Yaml []byte + Name string +} + +// modelSpec is the kind: Model spec. registry is an alias of registryId (P10-MODEL-YAML-1). +type modelSpec struct { + Repo string `yaml:"repo"` + Revision string `yaml:"revision,omitempty"` + RegistryID int `yaml:"registryId,omitempty"` + Registry string `yaml:"registry,omitempty"` + Files []string `yaml:"files,omitempty"` + Format string `yaml:"format,omitempty"` +} + +type remoteExecutor struct { + namespace string + name string + spec modelSpec + registryID int +} + +func (exe *remoteExecutor) GetName() string { + return exe.name +} + +func (exe *remoteExecutor) Execute() error { + util.SpinStart(fmt.Sprintf("Deploying model %s", exe.GetName())) + clt, err := clientutil.NewControllerClient(exe.namespace) + if err != nil { + return err + } + + _, err = clt.GetModel(exe.name) + if err != nil { + if !clientutil.IsClientNotFoundError(err) { + return err + } + return exe.createModel(clt) + } + return exe.updateModel(clt) +} + +func (exe *remoteExecutor) createModel(clt *client.Client) error { + _, err := clt.CreateModel(&client.ModelCreateRequest{ + Name: exe.name, + Repo: exe.spec.Repo, + RegistryID: exe.registryID, + Revision: exe.spec.Revision, + Files: exe.spec.Files, + Format: exe.spec.Format, + }) + return err +} + +func (exe *remoteExecutor) updateModel(clt *client.Client) error { + repo := exe.spec.Repo + revision := exe.spec.Revision + format := exe.spec.Format + registryID := exe.registryID + _, err := clt.UpdateModel(exe.name, &client.ModelUpdateRequest{ + Repo: &repo, + Revision: &revision, + RegistryID: ®istryID, + Files: exe.spec.Files, + Format: &format, + }) + return err +} + +func NewExecutor(opt Options) (exe execute.Executor, err error) { + ns, err := config.GetNamespace(opt.Namespace) + if err != nil { + return exe, err + } + if len(ns.GetControllers()) == 0 { + return exe, util.NewInputError("This namespace does not have a Controller. You must first deploy a Controller before deploying Models") + } + + spec, err := parseModelSpec(opt.Yaml) + if err != nil { + return nil, err + } + if len(opt.Name) > 0 { + if err := util.IsLowerAlphanumeric("Model", opt.Name); err != nil { + return nil, err + } + } else { + return nil, util.NewInputError("Name must be specified") + } + + registryID, err := resolveModelRegistryID(spec) + if err != nil { + return nil, err + } + if err := validateModelSpec(spec); err != nil { + return nil, err + } + + return &remoteExecutor{ + namespace: opt.Namespace, + name: opt.Name, + spec: spec, + registryID: registryID, + }, nil +} + +func parseModelSpec(raw []byte) (modelSpec, error) { + var spec modelSpec + if err := yaml.UnmarshalStrict(raw, &spec); err != nil { + return spec, util.NewUnmarshalError(err.Error()) + } + return spec, nil +} + +func resolveModelRegistryID(spec modelSpec) (int, error) { + alias := strings.TrimSpace(spec.Registry) + var fromAlias int + var hasAlias bool + if alias != "" { + id, err := clientutil.ResolveRegistryID(alias) + if err != nil { + return 0, err + } + fromAlias = id + hasAlias = true + } + if spec.RegistryID > 0 && hasAlias && spec.RegistryID != fromAlias { + return 0, util.NewInputError("spec.registry and spec.registryId must refer to the same registry") + } + if hasAlias { + return fromAlias, nil + } + if spec.RegistryID > 0 { + return spec.RegistryID, nil + } + return 0, util.NewInputError("spec.registry or spec.registryId must be specified") +} + +func validateModelSpec(spec modelSpec) error { + if strings.TrimSpace(spec.Repo) == "" { + return util.NewInputError("repo must be specified") + } + return nil +} diff --git a/internal/deploy/model/factory_test.go b/internal/deploy/model/factory_test.go new file mode 100644 index 000000000..43977f70b --- /dev/null +++ b/internal/deploy/model/factory_test.go @@ -0,0 +1,78 @@ +package deploymodel + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestParseModelSpecRegistryAlias(t *testing.T) { + raw := []byte(` +repo: ai/smollm2 +revision: sha256:eb3483481647229668c7625b080c2d6a632bf49535e0714198de5c026d4f41a5 +registry: 1 +files: [] +format: gguf +`) + spec, err := parseModelSpec(raw) + require.NoError(t, err) + id, err := resolveModelRegistryID(spec) + require.NoError(t, err) + require.Equal(t, 1, id) + require.Equal(t, "ai/smollm2", spec.Repo) + require.Equal(t, "gguf", spec.Format) +} + +func TestParseModelSpecRegistryID(t *testing.T) { + raw := []byte(` +repo: org/model +registryId: 2 +`) + spec, err := parseModelSpec(raw) + require.NoError(t, err) + id, err := resolveModelRegistryID(spec) + require.NoError(t, err) + require.Equal(t, 2, id) +} + +func TestParseModelSpecRegistryNamedAlias(t *testing.T) { + raw := []byte(` +repo: org/model +registry: remote +`) + spec, err := parseModelSpec(raw) + require.NoError(t, err) + id, err := resolveModelRegistryID(spec) + require.NoError(t, err) + require.Equal(t, 1, id) +} + +func TestParseModelSpecConflictingRegistry(t *testing.T) { + raw := []byte(` +repo: org/model +registry: 1 +registryId: 2 +`) + spec, err := parseModelSpec(raw) + require.NoError(t, err) + _, err = resolveModelRegistryID(spec) + require.Error(t, err) + require.Contains(t, err.Error(), "same registry") +} + +func TestParseModelSpecMissingRegistry(t *testing.T) { + raw := []byte(` +repo: org/model +`) + spec, err := parseModelSpec(raw) + require.NoError(t, err) + _, err = resolveModelRegistryID(spec) + require.Error(t, err) + require.Contains(t, err.Error(), "registry") +} + +func TestValidateModelSpecRequiresRepo(t *testing.T) { + err := validateModelSpec(modelSpec{RegistryID: 1}) + require.Error(t, err) + require.Contains(t, err.Error(), "repo") +} diff --git a/internal/deploy/registry/factory.go b/internal/deploy/registry/factory.go index 462c0098c..9dfb2251f 100644 --- a/internal/deploy/registry/factory.go +++ b/internal/deploy/registry/factory.go @@ -2,6 +2,7 @@ package deployregistry import ( "fmt" + "strings" "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" "github.com/eclipse-iofog/iofogctl/internal/config" @@ -12,6 +13,11 @@ import ( "gopkg.in/yaml.v2" ) +const ( + registryTypeOCI = "oci" + registryTypeHF = "hf" +) + type Options struct { Namespace string Yaml []byte @@ -32,53 +38,20 @@ func (exe *remoteExecutor) GetName() string { func (exe *remoteExecutor) Execute() error { util.SpinStart(fmt.Sprintf("Deploying registry %s", exe.GetName())) - // Init remote resources clt, err := clientutil.NewControllerClient(exe.namespace) if err != nil { return err } if exe.registry.ID > 0 { - var publicPtr *bool - if exe.registry.Private != nil { - public := !*exe.registry.Private - publicPtr = &public - } - return clt.UpdateRegistry(client.RegistryUpdateRequest{ - URL: exe.registry.URL, - IsPublic: publicPtr, - Username: exe.registry.Username, - Email: exe.registry.Email, - Password: exe.registry.Password, - ID: exe.registry.ID, - }) + return clt.UpdateRegistry(toUpdateRequest(exe.registry)) } - createRequest := &client.RegistryCreateRequest{} - if exe.registry.URL != nil { - createRequest.URL = *exe.registry.URL - } - if exe.registry.Private != nil { - createRequest.IsPublic = !*exe.registry.Private - } - if exe.registry.Username != nil { - createRequest.Username = *exe.registry.Username - } - if exe.registry.Password != nil { - createRequest.Password = *exe.registry.Password - } - if exe.registry.Email != nil { - createRequest.Email = *exe.registry.Email - } - if _, err = clt.CreateRegistry(createRequest); err != nil { - return err - } - - return nil + _, err = clt.CreateRegistry(toCreateRequest(exe.registry)) + return err } func NewExecutor(opt Options) (exe execute.Executor, err error) { - // Check the namespace exists ns, err := config.GetNamespace(opt.Namespace) if err != nil { return @@ -88,12 +61,10 @@ func NewExecutor(opt Options) (exe execute.Executor, err error) { return } - // Check Controller exists if len(controlPlane.GetControllers()) == 0 { return exe, util.NewInputError("This namespace does not have a Controller. You must first deploy a Controller before deploying Applications") } - // Unmarshal file var registry rsc.Registry if err = yaml.UnmarshalStrict(opt.Yaml, ®istry); err != nil { err = util.NewUnmarshalError(err.Error()) @@ -101,11 +72,18 @@ func NewExecutor(opt Options) (exe execute.Executor, err error) { } if registry.Private == nil { - Private := false - registry.Private = &Private + private := false + registry.Private = &private + } + if registry.Type == nil || strings.TrimSpace(*registry.Type) == "" { + t := registryTypeOCI + registry.Type = &t + } else { + t := strings.ToLower(strings.TrimSpace(*registry.Type)) + registry.Type = &t } - if err := validate(registry, true); err != nil { + if err := validate(registry); err != nil { return nil, err } @@ -115,20 +93,82 @@ func NewExecutor(opt Options) (exe execute.Executor, err error) { }, nil } -func validate(opt rsc.Registry, create bool) error { - if create { - if opt.URL == nil || *opt.URL == "" { - return util.NewInputError("URL cannot be empty") - } - if opt.Email == nil || *opt.Email == "" { - return util.NewInputError("Email cannot be empty") - } +func toCreateRequest(reg rsc.Registry) *client.RegistryCreateRequest { + req := &client.RegistryCreateRequest{ + Type: derefString(reg.Type), + CA: derefString(reg.CA), + } + if reg.URL != nil { + req.URL = *reg.URL + } + if reg.Private != nil { + req.IsPublic = !*reg.Private + } + if reg.Username != nil { + req.Username = *reg.Username } - if opt.RequiresCert != nil && *opt.RequiresCert && opt.Certificate != nil && *opt.Certificate == "" { - return util.NewInputError("Certificate cannot be empty if requiresCertificate is set to true") + if reg.Password != nil { + req.Password = *reg.Password } - if !*opt.Private && ((opt.Password == nil || *opt.Password == "") || (opt.Username == nil || *opt.Username == "")) { - return util.NewInputError("Password and/or Username cannot be empty if Private is set to false") + if reg.Email != nil { + req.Email = *reg.Email + } + if reg.Insecure != nil { + req.Insecure = *reg.Insecure + } + return req +} + +func toUpdateRequest(reg rsc.Registry) client.RegistryUpdateRequest { + return client.RegistryUpdateRequest{ + URL: reg.URL, + IsPublic: invertBool(reg.Private), + Username: reg.Username, + Email: reg.Email, + Password: reg.Password, + Type: reg.Type, + CA: reg.CA, + Insecure: reg.Insecure, + ID: reg.ID, + } +} + +func invertBool(p *bool) *bool { + if p == nil { + return nil + } + v := !*p + return &v +} + +func derefString(p *string) string { + if p == nil { + return "" + } + return *p +} + +func validate(opt rsc.Registry) error { + if opt.URL == nil || *opt.URL == "" { + return util.NewInputError("URL cannot be empty") + } + + regType := derefString(opt.Type) + if regType != registryTypeOCI && regType != registryTypeHF { + return util.NewInputError("type must be oci or hf") + } + + if opt.Private != nil && *opt.Private { + switch regType { + case registryTypeHF: + if opt.Password == nil || *opt.Password == "" { + return util.NewInputError("Password cannot be empty for private hf registry") + } + default: + if opt.Username == nil || *opt.Username == "" || opt.Password == nil || *opt.Password == "" { + return util.NewInputError("Username and password are required for private oci registry") + } + } } return nil diff --git a/internal/deploy/registry/factory_test.go b/internal/deploy/registry/factory_test.go new file mode 100644 index 000000000..f465ed64d --- /dev/null +++ b/internal/deploy/registry/factory_test.go @@ -0,0 +1,129 @@ +package deployregistry + +import ( + "strings" + "testing" + + rsc "github.com/eclipse-iofog/iofogctl/internal/resource" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v2" +) + +func strPtr(v string) *string { return &v } +func boolPtr(v bool) *bool { return &v } + +func TestValidatePrivateOCIRequiresCredentials(t *testing.T) { + err := validate(rsc.Registry{ + URL: strPtr("registry.example.com"), + Private: boolPtr(true), + Type: strPtr(registryTypeOCI), + Username: strPtr("user"), + }) + require.Error(t, err) + require.Contains(t, err.Error(), "Username and password") + + err = validate(rsc.Registry{ + URL: strPtr("registry.example.com"), + Private: boolPtr(true), + Type: strPtr(registryTypeOCI), + Username: strPtr("user"), + Password: strPtr("secret"), + }) + require.NoError(t, err) +} + +func TestValidatePrivateHFRequiresPasswordOnly(t *testing.T) { + err := validate(rsc.Registry{ + URL: strPtr("https://huggingface.co"), + Private: boolPtr(true), + Type: strPtr(registryTypeHF), + }) + require.Error(t, err) + require.Contains(t, err.Error(), "Password") + + err = validate(rsc.Registry{ + URL: strPtr("https://huggingface.co"), + Private: boolPtr(true), + Type: strPtr(registryTypeHF), + Password: strPtr("hf_xxx"), + }) + require.NoError(t, err) +} + +func TestValidateEmailOptional(t *testing.T) { + err := validate(rsc.Registry{ + URL: strPtr("registry.example.com"), + Private: boolPtr(true), + Type: strPtr(registryTypeOCI), + Username: strPtr("user"), + Password: strPtr("secret"), + }) + require.NoError(t, err) +} + +func TestValidateTypeMustBeOciOrHf(t *testing.T) { + err := validate(rsc.Registry{ + URL: strPtr("registry.example.com"), + Type: strPtr("docker"), + }) + require.Error(t, err) + require.Contains(t, err.Error(), "oci or hf") +} + +func TestUnmarshalRegistryTypeCAInsecure(t *testing.T) { + raw := []byte(` +url: registry.example.com +private: true +type: oci +insecure: false +ca: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t +username: user +password: secret +`) + var registry rsc.Registry + require.NoError(t, yaml.UnmarshalStrict(raw, ®istry)) + require.Equal(t, "oci", *registry.Type) + require.NotNil(t, registry.Insecure) + require.False(t, *registry.Insecure) + require.Equal(t, "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t", *registry.CA) +} + +func TestUnmarshalRegistryRejectsLegacyFlags(t *testing.T) { + raw := []byte(` +url: registry.example.com +requiresCert: true +`) + var registry rsc.Registry + err := yaml.UnmarshalStrict(raw, ®istry) + require.Error(t, err) +} + +func TestToCreateRequestMapsTypeCAInsecure(t *testing.T) { + req := toCreateRequest(rsc.Registry{ + URL: strPtr("registry.example.com"), + Private: boolPtr(true), + Type: strPtr(registryTypeOCI), + CA: strPtr("pem"), + Insecure: boolPtr(true), + Username: strPtr("user"), + Password: strPtr("secret"), + }) + require.Equal(t, "registry.example.com", req.URL) + require.False(t, req.IsPublic) + require.Equal(t, "oci", req.Type) + require.Equal(t, "pem", req.CA) + require.True(t, req.Insecure) + require.Equal(t, "user", req.Username) +} + +func TestDefaultTypeIsOci(t *testing.T) { + raw := []byte("url: registry.example.com\nprivate: false\n") + var registry rsc.Registry + require.NoError(t, yaml.UnmarshalStrict(raw, ®istry)) + if registry.Type == nil || strings.TrimSpace(*registry.Type) == "" { + t := registryTypeOCI + registry.Type = &t + } + require.NoError(t, validate(registry)) + require.Equal(t, registryTypeOCI, *registry.Type) +} diff --git a/internal/deploy/runtimeclass/factory.go b/internal/deploy/runtimeclass/factory.go new file mode 100644 index 000000000..e17c32341 --- /dev/null +++ b/internal/deploy/runtimeclass/factory.go @@ -0,0 +1,115 @@ +package deployruntimeclass + +import ( + "fmt" + "strings" + + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + "github.com/eclipse-iofog/iofogctl/internal/config" + "github.com/eclipse-iofog/iofogctl/internal/execute" + clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" + "github.com/eclipse-iofog/iofogctl/pkg/util" + "gopkg.in/yaml.v2" +) + +type Options struct { + Namespace string + Yaml []byte + FullYAML []byte + Name string +} + +type remoteExecutor struct { + namespace string + name string + handler string +} + +func (exe *remoteExecutor) GetName() string { + return exe.name +} + +func (exe *remoteExecutor) Execute() error { + util.SpinStart(fmt.Sprintf("Deploying RuntimeClass %s", exe.GetName())) + clt, err := clientutil.NewControllerClient(exe.namespace) + if err != nil { + return err + } + + _, err = clt.GetRuntimeClass(exe.name) + if err != nil { + if !clientutil.IsClientNotFoundError(err) { + return err + } + return exe.createRuntimeClass(clt) + } + return exe.updateRuntimeClass(clt) +} + +func (exe *remoteExecutor) createRuntimeClass(clt *client.Client) error { + _, err := clt.CreateRuntimeClass(&client.RuntimeClassCreateRequest{ + Name: exe.name, + Handler: exe.handler, + }) + return err +} + +func (exe *remoteExecutor) updateRuntimeClass(clt *client.Client) error { + _, err := clt.UpdateRuntimeClass(exe.name, &client.RuntimeClassUpdateRequest{ + Handler: exe.handler, + }) + return err +} + +func NewExecutor(opt Options) (exe execute.Executor, err error) { + ns, err := config.GetNamespace(opt.Namespace) + if err != nil { + return exe, err + } + if len(ns.GetControllers()) == 0 { + return exe, util.NewInputError("This namespace does not have a Controller. You must first deploy a Controller before deploying RuntimeClasses") + } + + if len(opt.Name) == 0 { + return nil, util.NewInputError("Name must be specified") + } + if err := util.IsLowerAlphanumeric("RuntimeClass", opt.Name); err != nil { + return nil, err + } + + handler, err := parseRuntimeClassHandler(opt.FullYAML, opt.Yaml) + if err != nil { + return nil, err + } + + return &remoteExecutor{ + namespace: opt.Namespace, + name: opt.Name, + handler: handler, + }, nil +} + +// parseRuntimeClassHandler reads handler from the document root (P10-RC-YAML-1). +// FullYAML is preferred; spec YAML is a fallback when dispatch stuffed handler there. +func parseRuntimeClassHandler(fullYAML, specYAML []byte) (string, error) { + if handler := handlerFromYAML(fullYAML); handler != "" { + return handler, nil + } + if handler := handlerFromYAML(specYAML); handler != "" { + return handler, nil + } + return "", util.NewInputError("handler must be specified") +} + +func handlerFromYAML(raw []byte) string { + if len(raw) == 0 { + return "" + } + var doc struct { + Handler string `yaml:"handler"` + } + if err := yaml.Unmarshal(raw, &doc); err != nil { + return "" + } + return strings.TrimSpace(doc.Handler) +} diff --git a/internal/deploy/runtimeclass/factory_test.go b/internal/deploy/runtimeclass/factory_test.go new file mode 100644 index 000000000..1e12daa23 --- /dev/null +++ b/internal/deploy/runtimeclass/factory_test.go @@ -0,0 +1,41 @@ +package deployruntimeclass + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestParseRuntimeClassHandlerFromRoot(t *testing.T) { + full := []byte(` +apiVersion: datasance.com/v3 +kind: RuntimeClass +metadata: + name: spin +handler: spin +`) + handler, err := parseRuntimeClassHandler(full, nil) + require.NoError(t, err) + require.Equal(t, "spin", handler) +} + +func TestParseRuntimeClassHandlerFromSpecFallback(t *testing.T) { + spec := []byte("handler: wasmedge\n") + handler, err := parseRuntimeClassHandler(nil, spec) + require.NoError(t, err) + require.Equal(t, "wasmedge", handler) +} + +func TestParseRuntimeClassHandlerPrefersRootOverSpec(t *testing.T) { + full := []byte("handler: spin\n") + spec := []byte("handler: other\n") + handler, err := parseRuntimeClassHandler(full, spec) + require.NoError(t, err) + require.Equal(t, "spin", handler) +} + +func TestParseRuntimeClassHandlerMissing(t *testing.T) { + _, err := parseRuntimeClassHandler([]byte("kind: RuntimeClass\n"), []byte("{}\n")) + require.Error(t, err) + require.Contains(t, err.Error(), "handler") +} diff --git a/internal/describe/agent_platform_status_test.go b/internal/describe/agent_platform_status_test.go index 79f2afc2e..f01f3f5bb 100644 --- a/internal/describe/agent_platform_status_test.go +++ b/internal/describe/agent_platform_status_test.go @@ -28,7 +28,7 @@ func TestFormatAgentStatusPlatformStatus(t *testing.T) { }, } - got := FormatAgentStatus(status) + got := statusMap(FormatAgentStatus(status)) ps, ok := got["platformStatus"].(map[string]interface{}) require.True(t, ok, "platformStatus should be present") require.Equal(t, "Ready", ps["phase"]) @@ -44,6 +44,6 @@ func TestFormatAgentStatusPlatformStatus(t *testing.T) { } func TestFormatAgentStatusOmitsNilPlatformStatus(t *testing.T) { - got := FormatAgentStatus(rsc.AgentStatus{}) + got := statusMap(FormatAgentStatus(rsc.AgentStatus{})) require.NotContains(t, got, "platformStatus") } diff --git a/internal/describe/agent_status_test.go b/internal/describe/agent_status_test.go index fc9464dc1..e3f1e9307 100644 --- a/internal/describe/agent_status_test.go +++ b/internal/describe/agent_status_test.go @@ -34,7 +34,7 @@ func TestFormatAgentStatusGoldenV38Fields(t *testing.T) { SecurityStatus: "OK", WarningMessage: "HEALTHY", SecurityViolationInfo: "No violation", - CPUUsage: 0.13, + CPUUsage: 150, DiskUsage: 0.000000187, // 187 B as GiB (decimal) from Edgelet status PUT MemoryViolation: "false", DiskViolation: "false", @@ -53,7 +53,7 @@ func TestFormatAgentStatusGoldenV38Fields(t *testing.T) { ControlPlaneQuiesced: false, } - got := FormatAgentStatus(status) + got := statusMap(FormatAgentStatus(status)) for key, want := range expected { require.Contains(t, got, key, "missing status field %q", key) @@ -66,7 +66,7 @@ func TestFormatAgentStatusGoldenV38Fields(t *testing.T) { } func TestFormatAgentStatusV38FieldsAlwaysPresent(t *testing.T) { - got := FormatAgentStatus(rsc.AgentStatus{}) + got := statusMap(FormatAgentStatus(rsc.AgentStatus{})) require.Contains(t, got, "availableRuntimes") require.Contains(t, got, "runtimeAgentPhase") @@ -76,7 +76,7 @@ func TestFormatAgentStatusV38FieldsAlwaysPresent(t *testing.T) { } func TestFormatAgentStatusDiskUsageGiB(t *testing.T) { - got := FormatAgentStatus(rsc.AgentStatus{DiskUsage: 0.24}) + got := statusMap(FormatAgentStatus(rsc.AgentStatus{DiskUsage: 0.24})) require.Equal(t, "228.9 MB", got["diskUsage"]) } @@ -90,9 +90,149 @@ func TestFormatAgentStatusUptimeAndTimestamps(t *testing.T) { UptimeMs: (5*time.Hour + 3*time.Minute).Milliseconds(), } - got := FormatAgentStatus(status) + got := statusMap(FormatAgentStatus(status)) require.Equal(t, "2026-05-07T12:21:10Z", got["lastActive"]) require.Equal(t, "2026-05-07T12:21:10Z", got["lastStatusTime"]) require.Equal(t, "5h3m", got["uptime"]) } + +func TestFormatAgentStatusV39ModelAndRuntimeBlobs(t *testing.T) { + got := statusMap(FormatAgentStatus(rsc.AgentStatus{ + RuntimeClasses: `[{"name":"nvidia"}]`, + AvailableCdiDevices: `["nvidia.com/gpu=0"]`, + ModelStatus: `[{"name":"llama","state":"Ready"}]`, + ActiveModels: 2, + ModelLastUpdate: 1710000000000, + })) + + require.Equal(t, 2, got["activeModels"]) + require.Equal(t, "2024-03-09T16:00:00Z", got["modelLastUpdate"]) + + runtimeClasses, ok := got["runtimeClasses"].([]interface{}) + require.True(t, ok) + require.Len(t, runtimeClasses, 1) + require.Equal(t, "nvidia", runtimeClasses[0].(map[string]interface{})["name"]) + + cdi, ok := got["availableCdiDevices"].([]interface{}) + require.True(t, ok) + require.Equal(t, "nvidia.com/gpu=0", cdi[0]) + + models, ok := got["modelStatus"].([]interface{}) + require.True(t, ok) + require.Equal(t, "llama", models[0].(map[string]interface{})["name"]) +} + +func TestFormatAgentStatusV39KnowledgeFields(t *testing.T) { + got := statusMap(FormatAgentStatus(rsc.AgentStatus{ + KnowledgeStatus: `[{"name":"wiki"}]`, + ActiveKnowledge: 3, + KnowledgeLastUpdate: 1710000000456, + })) + + require.Equal(t, 3, got["activeKnowledge"]) + require.Equal(t, "2024-03-09T16:00:00Z", got["knowledgeLastUpdate"]) + + knowledge, ok := got["knowledgeStatus"].([]interface{}) + require.True(t, ok) + require.Equal(t, "wiki", knowledge[0].(map[string]interface{})["name"]) +} + +func TestFormatAgentStatusOmitsEmptyV39Blobs(t *testing.T) { + got := statusMap(FormatAgentStatus(rsc.AgentStatus{})) + require.NotContains(t, got, "runtimeClasses") + require.NotContains(t, got, "availableCdiDevices") + require.NotContains(t, got, "modelStatus") + require.NotContains(t, got, "modelLastUpdate") + require.NotContains(t, got, "knowledgeStatus") + require.NotContains(t, got, "knowledgeLastUpdate") + require.NotContains(t, got, "systemCpus") + require.NotContains(t, got, "systemTotalMemory") + require.NotContains(t, got, "systemAvailableMemory") + require.NotContains(t, got, "systemTotalDisk") + require.NotContains(t, got, "systemAvailableDisk") + require.NotContains(t, got, "systemTotalCPU") + require.NotContains(t, got, "systemOs") + require.NotContains(t, got, "processedMessages") + require.NotContains(t, got, "messageSpeed") + require.Equal(t, 0, got["activeModels"]) + require.Equal(t, 0, got["activeKnowledge"]) +} + +func TestFormatAgentStatusHostMetrics(t *testing.T) { + got := statusMap(FormatAgentStatus(rsc.AgentStatus{ + CPUUsage: 32.97, + SystemCpus: 4, + SystemTotalMemory: 8 * 1024 * 1024 * 1024, + SystemAvailableMemory: 3 * 1024 * 1024 * 1024, + SystemTotalDisk: 50 * 1024 * 1024 * 1024, + SystemAvailableDisk: 4 * 1024 * 1024 * 1024, + SystemTotalCPU: 21.03, + SystemOs: "linux", + SystemOsVersion: "6.8.0", + SystemKernelVersion: "6.8.0-60-generic", + })) + + require.Equal(t, "0.33 cores", got["cpuUsage"]) + require.Equal(t, 4, got["systemCpus"]) + require.Equal(t, "8.0 GB", got["systemTotalMemory"]) + require.Equal(t, "3.0 GB", got["systemAvailableMemory"]) + require.Equal(t, "50.0 GB", got["systemTotalDisk"]) + require.Equal(t, "4.0 GB", got["systemAvailableDisk"]) + require.Equal(t, "21.03 %", got["systemTotalCPU"]) + require.Equal(t, "linux", got["systemOs"]) + require.Equal(t, "6.8.0", got["systemOsVersion"]) + require.Equal(t, "6.8.0-60-generic", got["systemKernelVersion"]) +} + +func TestFormatAgentStatusFieldOrder(t *testing.T) { + keys := statusKeys(FormatAgentStatus(rsc.AgentStatus{ + Version: "v1.1.0-rc.5", + DaemonStatus: "RUNNING", + UptimeMs: time.Hour.Milliseconds(), + CPUUsage: 150, + MemoryUsage: 152, + DiskUsage: 0.24, + SystemOs: "linux", + SystemCpus: 4, + SystemTotalCPU: 21.03, + SystemTotalMemory: 8 * 1024 * 1024 * 1024, + SystemAvailableMemory: 3 * 1024 * 1024 * 1024, + ActiveModels: 1, + ActiveKnowledge: 2, + })) + + require.Equal(t, []string{ + "version", + "daemonStatus", + "securityStatus", + "securityViolationInfo", + "warningMessage", + "gpsStatus", + "ipAddress", + "ipAddressExternal", + "lastCommandTime", + "uptime", + "cpuUsage", + "memoryUsage", + "diskUsage", + "cpuViolation", + "memoryViolation", + "diskViolation", + "systemOs", + "systemCpus", + "systemTotalCPU", + "systemTotalMemory", + "systemAvailableMemory", + "availableRuntimes", + "runtimeAgentPhase", + "controlPlaneQuiesced", + "activeModels", + "activeKnowledge", + "repositoryStatus", + "isReadyToUpgrade", + "isReadyToRollback", + "tunnel", + "volumeMounts", + }, keys) +} diff --git a/internal/describe/factory.go b/internal/describe/factory.go index bd9b9d924..d656ee8ad 100644 --- a/internal/describe/factory.go +++ b/internal/describe/factory.go @@ -56,6 +56,14 @@ func NewExecutor(opt *Options) (execute.Executor, error) { return newRoleBindingExecutor(opt.Namespace, opt.Name, opt.Filename), nil case "serviceaccount": return newServiceAccountExecutor(opt.Namespace, opt.Name, opt.Filename), nil + case "knowledge": + return newKnowledgeExecutor(opt.Namespace, opt.Name, opt.Filename), nil + case "model": + return newModelExecutor(opt.Namespace, opt.Name, opt.Filename), nil + case "runtimeclass": + return newRuntimeClassExecutor(opt.Namespace, opt.Name, opt.Filename), nil + case "microservice-template": + return newMicroserviceTemplateExecutor(opt.Namespace, opt.Name, opt.Filename), nil default: return nil, util.NewInputError(fmt.Sprintf("Unknown resources: %s", opt.Resource)) } diff --git a/internal/describe/knowledge.go b/internal/describe/knowledge.go new file mode 100644 index 000000000..2e8e9a6a4 --- /dev/null +++ b/internal/describe/knowledge.go @@ -0,0 +1,84 @@ +package describe + +import ( + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + "github.com/eclipse-iofog/iofogctl/internal/config" + clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" +) + +type knowledgeExecutor struct { + namespace string + name string + filename string +} + +type knowledgeDescribeSpec struct { + UUID string `yaml:"uuid,omitempty"` + Repo string `yaml:"repo"` + Revision string `yaml:"revision,omitempty"` + RegistryID int `yaml:"registryId"` + Files []string `yaml:"files,omitempty"` + Format string `yaml:"format,omitempty"` +} + +func newKnowledgeExecutor(namespace, name, filename string) *knowledgeExecutor { + return &knowledgeExecutor{ + namespace: namespace, + name: name, + filename: filename, + } +} + +func (exe *knowledgeExecutor) GetName() string { + return exe.name +} + +func (exe *knowledgeExecutor) Execute() error { + clt, err := clientutil.NewControllerClient(exe.namespace) + if err != nil { + return err + } + + knowledge, err := clt.GetKnowledge(exe.name) + if err != nil { + return err + } + + var linkedAgents []string + link, err := clt.GetKnowledgeLink(exe.name) + if err != nil { + return err + } + if link != nil && len(link.FogUUIDs) > 0 { + agents, listErr := clt.ListAgents(client.ListAgentsRequest{}) + if listErr != nil { + return listErr + } + linkedAgents = clientutil.AgentNamesFromUUIDs(agents.Agents, link.FogUUIDs) + } + + return printDescribeHeader(exe.filename, knowledgeHeader(exe.namespace, exe.name, knowledge, linkedAgents)) +} + +func knowledgeHeader(namespace, name string, knowledge *client.Knowledge, linkedAgents []string) config.Header { + header := config.Header{ + APIVersion: config.LatestAPIVersion, + Kind: config.KnowledgeKind, + Metadata: config.HeaderMetadata{ + Namespace: namespace, + Name: name, + }, + Spec: knowledgeDescribeSpec{ + UUID: knowledge.UUID, + Repo: knowledge.Repo, + Revision: knowledge.Revision, + RegistryID: knowledge.RegistryID, + Files: knowledge.Files, + Format: knowledge.Format, + }, + } + if len(linkedAgents) > 0 { + header.Status = linkedAgentsStatus{LinkedAgents: linkedAgents} + } + return header +} diff --git a/internal/describe/microservice.go b/internal/describe/microservice.go index ef2394bdc..f73f1e98f 100644 --- a/internal/describe/microservice.go +++ b/internal/describe/microservice.go @@ -1,6 +1,8 @@ package describe import ( + "strings" + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" "github.com/eclipse-iofog/iofogctl/internal/config" clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" @@ -57,18 +59,20 @@ func (exe *microserviceExecutor) Execute() error { return err } + metadataName := exe.name + if _, msvcName, err := clientutil.ParseFQName(exe.name, "Microservice"); err == nil && strings.Contains(exe.name, "/") { + metadataName = msvcName + } + header := config.Header{ APIVersion: config.LatestAPIVersion, Kind: config.MicroserviceKind, Metadata: config.HeaderMetadata{ Namespace: exe.namespace, - Name: exe.name, - }, - Spec: yamlMsvc, - Status: map[string]interface{}{ - "status": FormatMicroserviceStatus(status), - "execStatus": FormatMicroserviceExecStatus(execStatus), + Name: metadataName, }, + Spec: yamlMsvc, + Status: FormatMicroserviceDescribeStatus(status, execStatus), } if exe.filename == "" { diff --git a/internal/describe/microservice_template.go b/internal/describe/microservice_template.go new file mode 100644 index 000000000..3653421df --- /dev/null +++ b/internal/describe/microservice_template.go @@ -0,0 +1,77 @@ +package describe + +import ( + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + "github.com/eclipse-iofog/iofogctl/internal/config" + clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" +) + +type microserviceTemplateExecutor struct { + namespace string + name string + filename string +} + +type microserviceTemplateVariable struct { + Key string `yaml:"key"` + Description string `yaml:"description,omitempty"` + DefaultValue interface{} `yaml:"defaultValue,omitempty"` +} + +type microserviceTemplateSpec struct { + Description string `yaml:"description,omitempty"` + Variables []microserviceTemplateVariable `yaml:"variables,omitempty"` + Microservice interface{} `yaml:"microservice,omitempty"` +} + +func newMicroserviceTemplateExecutor(namespace, name, filename string) *microserviceTemplateExecutor { + return µserviceTemplateExecutor{ + namespace: namespace, + name: name, + filename: filename, + } +} + +func (exe *microserviceTemplateExecutor) GetName() string { + return exe.name +} + +func (exe *microserviceTemplateExecutor) Execute() error { + clt, err := clientutil.NewControllerClient(exe.namespace) + if err != nil { + return err + } + + template, err := clt.GetMicroserviceTemplate(exe.name) + if err != nil { + return err + } + + return printDescribeHeader(exe.filename, microserviceTemplateHeader(exe.namespace, exe.name, template)) +} + +func microserviceTemplateHeader(namespace, name string, template *client.MicroserviceTemplate) config.Header { + spec := microserviceTemplateSpec{ + Description: template.Description, + Microservice: template.Microservice, + } + if len(template.Variables) > 0 { + spec.Variables = make([]microserviceTemplateVariable, 0, len(template.Variables)) + for _, variable := range template.Variables { + spec.Variables = append(spec.Variables, microserviceTemplateVariable{ + Key: variable.Key, + Description: variable.Description, + DefaultValue: variable.DefaultValue, + }) + } + } + return config.Header{ + APIVersion: config.LatestAPIVersion, + Kind: config.MicroserviceTemplateKind, + Metadata: config.HeaderMetadata{ + Namespace: namespace, + Name: name, + }, + Spec: spec, + } +} diff --git a/internal/describe/model.go b/internal/describe/model.go new file mode 100644 index 000000000..08c6c7807 --- /dev/null +++ b/internal/describe/model.go @@ -0,0 +1,96 @@ +package describe + +import ( + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + "github.com/eclipse-iofog/iofogctl/internal/config" + clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +type modelExecutor struct { + namespace string + name string + filename string +} + +type modelDescribeSpec struct { + UUID string `yaml:"uuid,omitempty"` + Repo string `yaml:"repo"` + Revision string `yaml:"revision,omitempty"` + RegistryID int `yaml:"registryId"` + Files []string `yaml:"files,omitempty"` + Format string `yaml:"format,omitempty"` +} + +type linkedAgentsStatus struct { + LinkedAgents []string `yaml:"linkedAgents,omitempty"` +} + +func newModelExecutor(namespace, name, filename string) *modelExecutor { + return &modelExecutor{ + namespace: namespace, + name: name, + filename: filename, + } +} + +func (exe *modelExecutor) GetName() string { + return exe.name +} + +func (exe *modelExecutor) Execute() error { + clt, err := clientutil.NewControllerClient(exe.namespace) + if err != nil { + return err + } + + model, err := clt.GetModel(exe.name) + if err != nil { + return err + } + + var linkedAgents []string + link, err := clt.GetModelLink(exe.name) + if err != nil { + return err + } + if link != nil && len(link.FogUUIDs) > 0 { + agents, listErr := clt.ListAgents(client.ListAgentsRequest{}) + if listErr != nil { + return listErr + } + linkedAgents = clientutil.AgentNamesFromUUIDs(agents.Agents, link.FogUUIDs) + } + + return printDescribeHeader(exe.filename, modelHeader(exe.namespace, exe.name, model, linkedAgents)) +} + +func modelHeader(namespace, name string, model *client.Model, linkedAgents []string) config.Header { + header := config.Header{ + APIVersion: config.LatestAPIVersion, + Kind: config.ModelKind, + Metadata: config.HeaderMetadata{ + Namespace: namespace, + Name: name, + }, + Spec: modelDescribeSpec{ + UUID: model.UUID, + Repo: model.Repo, + Revision: model.Revision, + RegistryID: model.RegistryID, + Files: model.Files, + Format: model.Format, + }, + } + if len(linkedAgents) > 0 { + header.Status = linkedAgentsStatus{LinkedAgents: linkedAgents} + } + return header +} + +func printDescribeHeader(filename string, header config.Header) error { + if filename == "" { + return util.Print(header) + } + return util.FPrint(header, filename) +} diff --git a/internal/describe/registry.go b/internal/describe/registry.go index a50481857..c2e246ced 100644 --- a/internal/describe/registry.go +++ b/internal/describe/registry.go @@ -52,6 +52,11 @@ func (exe *registryExecutor) Execute() error { Username: &r.Username, Email: &r.Email, Password: &r.Password, + Type: &r.Type, + Insecure: &r.Insecure, + } + if r.CA != "" { + registry.CA = &r.CA } header := config.Header{ diff --git a/internal/describe/runtimeclass.go b/internal/describe/runtimeclass.go new file mode 100644 index 000000000..56afd7cca --- /dev/null +++ b/internal/describe/runtimeclass.go @@ -0,0 +1,68 @@ +package describe + +import ( + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + "github.com/eclipse-iofog/iofogctl/internal/config" + clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" +) + +type runtimeClassExecutor struct { + namespace string + name string + filename string +} + +func newRuntimeClassExecutor(namespace, name, filename string) *runtimeClassExecutor { + return &runtimeClassExecutor{ + namespace: namespace, + name: name, + filename: filename, + } +} + +func (exe *runtimeClassExecutor) GetName() string { + return exe.name +} + +func (exe *runtimeClassExecutor) Execute() error { + clt, err := clientutil.NewControllerClient(exe.namespace) + if err != nil { + return err + } + + runtimeClass, err := clt.GetRuntimeClass(exe.name) + if err != nil { + return err + } + + var linkedAgents []string + link, err := clt.GetRuntimeClassLink(exe.name) + if err != nil { + return err + } + if link != nil && len(link.FogUUIDs) > 0 { + agents, listErr := clt.ListAgents(client.ListAgentsRequest{}) + if listErr != nil { + return listErr + } + linkedAgents = clientutil.AgentNamesFromUUIDs(agents.Agents, link.FogUUIDs) + } + + return printDescribeHeader(exe.filename, runtimeClassHeader(exe.namespace, exe.name, runtimeClass.Handler, linkedAgents)) +} + +func runtimeClassHeader(namespace, name, handler string, linkedAgents []string) config.Header { + header := config.Header{ + APIVersion: config.LatestAPIVersion, + Kind: config.RuntimeClassKind, + Metadata: config.HeaderMetadata{ + Namespace: namespace, + Name: name, + }, + Handler: handler, + } + if len(linkedAgents) > 0 { + header.Status = linkedAgentsStatus{LinkedAgents: linkedAgents} + } + return header +} diff --git a/internal/describe/status_format_test.go b/internal/describe/status_format_test.go new file mode 100644 index 000000000..cfead27b0 --- /dev/null +++ b/internal/describe/status_format_test.go @@ -0,0 +1,23 @@ +package describe + +import "gopkg.in/yaml.v2" + +func statusMap(s yaml.MapSlice) map[string]interface{} { + m := make(map[string]interface{}, len(s)) + for _, item := range s { + if k, ok := item.Key.(string); ok { + m[k] = item.Value + } + } + return m +} + +func statusKeys(s yaml.MapSlice) []string { + keys := make([]string, 0, len(s)) + for _, item := range s { + if k, ok := item.Key.(string); ok { + keys = append(keys, k) + } + } + return keys +} diff --git a/internal/describe/system_microservice.go b/internal/describe/system_microservice.go index 34ccf5ac4..5303a2183 100644 --- a/internal/describe/system_microservice.go +++ b/internal/describe/system_microservice.go @@ -64,11 +64,8 @@ func (exe *systemMicroserviceExecutor) Execute() error { Namespace: exe.namespace, Name: exe.name, }, - Spec: yamlMsvc, - Status: map[string]interface{}{ - "status": FormatMicroserviceStatus(status), - "execStatus": FormatMicroserviceExecStatus(execStatus), - }, + Spec: yamlMsvc, + Status: FormatMicroserviceDescribeStatus(status, execStatus), } if exe.filename == "" { diff --git a/internal/describe/testdata/agent-config-status-v38.yaml b/internal/describe/testdata/agent-config-status-v38.yaml index df69d2ed2..ffa0141d3 100644 --- a/internal/describe/testdata/agent-config-status-v38.yaml +++ b/internal/describe/testdata/agent-config-status-v38.yaml @@ -9,7 +9,7 @@ daemonStatus: UNKNOWN securityStatus: OK warningMessage: HEALTHY securityViolationInfo: No violation -cpuUsage: 0.13 % +cpuUsage: 1.50 cores cpuViolation: "false" diskUsage: 187 B diskViolation: "false" diff --git a/internal/describe/utils.go b/internal/describe/utils.go index c961b7014..0f8b46d69 100644 --- a/internal/describe/utils.go +++ b/internal/describe/utils.go @@ -1,17 +1,17 @@ package describe import ( + "encoding/json" "errors" "fmt" + "strings" "time" apps "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/apps" "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" rsc "github.com/eclipse-iofog/iofogctl/internal/resource" - clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" "github.com/eclipse-iofog/iofogctl/pkg/util" - // "github.com/eclipse-iofog/iofogctl/pkg/iofog" - // "github.com/eclipse-iofog/iofogctl/pkg/util" + "gopkg.in/yaml.v2" ) func MapClientMicroserviceToDeployMicroservice(msvc *client.MicroserviceInfo, clt *client.Client) (*apps.Microservice, *apps.MicroserviceStatusInfo, *apps.MicroserviceExecStatusInfo, error) { @@ -67,51 +67,57 @@ func MapClientMicroserviceToDeployMicroservice(msvc *client.MicroserviceInfo, cl // return msvcStatus, msvcExecStatus, nil // } -// FormatMicroserviceStatus formats microservice status for human-readable output -func FormatMicroserviceStatus(status *apps.MicroserviceStatusInfo) map[string]interface{} { - formatted := make(map[string]interface{}) - - // Core status fields - formatted["status"] = status.Status - formatted["containerId"] = status.ContainerID - formatted["percentage"] = status.Percentage - formatted["errorMessage"] = status.ErrorMessage - formatted["ipAddress"] = status.IPAddress - formatted["execSessionIds"] = status.ExecSessionIDs - formatted["healthStatus"] = status.HealthStatus - - // Format startTime as RFC3339 timestamp +// FormatMicroserviceStatus formats microservice status for human-readable output. +func FormatMicroserviceStatus(status *apps.MicroserviceStatusInfo) yaml.MapSlice { + formatted := yaml.MapSlice{} + formatted = appendItem(formatted, "status", status.Status) + formatted = appendItem(formatted, "containerId", status.ContainerID) + if status.PodID != "" { + formatted = appendItem(formatted, "podId", status.PodID) + } + formatted = appendItem(formatted, "percentage", status.Percentage) + formatted = appendItem(formatted, "healthStatus", status.HealthStatus) + formatted = appendItem(formatted, "ipAddress", status.IPAddress) if status.StartTime > 0 { - formatted["startTime"] = time.Unix(status.StartTime/1000, (status.StartTime%1000)*1000000).Format(time.RFC3339) + formatted = appendItem(formatted, "startTime", time.Unix(status.StartTime/1000, (status.StartTime%1000)*1000000).Format(time.RFC3339)) } - - // Format operatingDuration as human-readable duration if status.OperatingDuration > 0 { duration := time.Duration(status.OperatingDuration) * time.Millisecond - formatted["operatingDuration"] = util.FormatDuration(duration) + formatted = appendItem(formatted, "operatingDuration", util.FormatDuration(duration)) } - - // Format memory usage - if status.MemoryUsage > 0 { - formatted["memoryUsage"] = formatBytesAuto(status.MemoryUsage) - } - - // Format CPU usage if status.CPUUsage > 0 { - formatted["cpuUsage"] = fmt.Sprintf("%.2f %%", status.CPUUsage) + formatted = appendItem(formatted, "cpuUsage", formatCPUCores(status.CPUUsage)) } - + if status.MemoryUsage > 0 { + formatted = appendItem(formatted, "memoryUsage", formatBytesAuto(status.MemoryUsage)) + } + formatted = appendItem(formatted, "restartCount", status.RestartCount) + formatted = appendItem(formatted, "errorMessage", status.ErrorMessage) + formatted = appendItem(formatted, "lastError", status.LastError) + // lastErrorAt is Unix milliseconds; keep 0 when LastError is empty (older Controllers). + if status.LastErrorAt > 0 { + formatted = appendItem(formatted, "lastErrorAt", time.Unix(status.LastErrorAt/1000, (status.LastErrorAt%1000)*1000000).Format(time.RFC3339)) + } else { + formatted = appendItem(formatted, "lastErrorAt", int64(0)) + } + formatted = appendItem(formatted, "execSessionIds", status.ExecSessionIDs) return formatted } -// FormatMicroserviceExecStatus formats microservice exec status for human-readable output -func FormatMicroserviceExecStatus(execStatus *apps.MicroserviceExecStatusInfo) map[string]interface{} { - formatted := make(map[string]interface{}) - - formatted["status"] = execStatus.Status - formatted["execSessionId"] = execStatus.ExecSessionID +// FormatMicroserviceExecStatus formats microservice exec status for human-readable output. +func FormatMicroserviceExecStatus(execStatus *apps.MicroserviceExecStatusInfo) yaml.MapSlice { + return yaml.MapSlice{ + {Key: "status", Value: execStatus.Status}, + {Key: "execSessionId", Value: execStatus.ExecSessionID}, + } +} - return formatted +// FormatMicroserviceDescribeStatus nests status and execStatus in stable order. +func FormatMicroserviceDescribeStatus(status *apps.MicroserviceStatusInfo, execStatus *apps.MicroserviceExecStatusInfo) yaml.MapSlice { + return yaml.MapSlice{ + {Key: "status", Value: FormatMicroserviceStatus(status)}, + {Key: "execStatus", Value: FormatMicroserviceExecStatus(execStatus)}, + } } func constructMicroservice(msvcInfo *client.MicroserviceInfo, agentName, appName string, catalogItem *client.CatalogItemInfo) (msvc *apps.Microservice, status *apps.MicroserviceStatusInfo, execStatus *apps.MicroserviceExecStatusInfo, err error) { @@ -156,7 +162,7 @@ func constructMicroservice(msvcInfo *client.MicroserviceInfo, agentName, appName ARM64: arm64Image, RISCV64: riscv64Image, ARM: armImage, - Registry: clientutil.FormatRegistryID(registryID), + Registry: apps.RegistryRef(registryID), } for _, img := range imgArray { switch img.ArchID { @@ -218,19 +224,49 @@ func constructMicroservice(msvcInfo *client.MicroserviceInfo, agentName, appName msvc.Container.Runtime = msvcInfo.Runtime msvc.Container.Platform = msvcInfo.Platform msvc.Container.RunAsUser = msvcInfo.RunAsUser + msvc.Container.RunAsGroup = msvcInfo.RunAsGroup + msvc.Container.ReadOnlyRootFilesystem = msvcInfo.ReadOnlyRootFilesystem msvc.Container.CdiDevices = msvcInfo.CdiDevices msvc.Container.CapAdd = msvcInfo.CapAdd msvc.Container.CapDrop = msvcInfo.CapDrop + // Commands is the merged cmd/commands list from GET (commands wins when both are present). msvc.Container.Commands = msvcInfo.Commands + if len(msvcInfo.CommandsAlias) > 0 { + msvc.Container.Commands = msvcInfo.CommandsAlias + } + msvc.Container.Entrypoint = msvcInfo.Entrypoint + msvc.Container.WorkingDir = msvcInfo.WorkingDir msvc.Container.Ports = mapPorts(msvcInfo.Ports) msvc.Container.Volumes = &volumes msvc.Container.Env = &envs msvc.Container.ExtraHosts = &extraHosts msvc.Container.CPUSetCpus = msvcInfo.CPUSetCpus msvc.Container.MemoryLimit = &msvcInfo.MemoryLimit + if msvcInfo.Cpus != 0 { + cpus := msvcInfo.Cpus + msvc.Container.CPUs = &cpus + } + if msvcInfo.MemoryReservation != 0 { + reservation := msvcInfo.MemoryReservation + msvc.Container.MemoryReservation = &reservation + } + if msvcInfo.MemorySwap != 0 { + swap := msvcInfo.MemorySwap + msvc.Container.MemorySwap = &swap + } + if msvcInfo.ShmSize != 0 { + shm := msvcInfo.ShmSize + msvc.Container.ShmSize = &shm + } + msvc.Container.Sysctls = msvcInfo.Sysctls + msvc.Container.Ulimits = mapUlimits(msvcInfo.Ulimits) + msvc.Container.Devices = mapDevices(msvcInfo.Devices) + msvc.Container.Tmpfs = mapTmpfs(msvcInfo.Tmpfs) if hasHealthCheck { msvc.Container.HealthCheck = &healthCheck } + msvc.Models = mapMicroserviceCatalog(msvcInfo.Models) + msvc.Knowledge = mapMicroserviceKnowledgeCatalog(msvcInfo.Knowledge) if msvcInfo.NatsConfig != nil { msvc.NatsConfig = &apps.MicroserviceNatsConfig{ NatsAccess: msvcInfo.NatsConfig.NatsAccess, @@ -239,6 +275,15 @@ func constructMicroservice(msvcInfo *client.MicroserviceInfo, agentName, appName } msvc.Schedule = msvcInfo.Schedule msvc.Application = appName + if msvcInfo.ServiceAccount != nil { + msvc.ServiceAccount = &apps.MicroserviceServiceAccountRef{ + RoleRef: apps.RoleRef{ + Kind: msvcInfo.ServiceAccount.RoleRef.Kind, + Name: msvcInfo.ServiceAccount.RoleRef.Name, + APIGroup: msvcInfo.ServiceAccount.RoleRef.APIGroup, + }, + } + } status = new(apps.MicroserviceStatusInfo) status.Status = msvcInfo.Status.Status @@ -249,9 +294,13 @@ func constructMicroservice(msvcInfo *client.MicroserviceInfo, agentName, appName status.ContainerID = msvcInfo.Status.ContainerID status.Percentage = msvcInfo.Status.Percentage status.ErrorMessage = msvcInfo.Status.ErrorMessage + status.LastError = msvcInfo.Status.LastError + status.LastErrorAt = msvcInfo.Status.LastErrorAt + status.RestartCount = msvcInfo.Status.RestartCount status.IPAddress = msvcInfo.Status.IPAddress status.ExecSessionIDs = msvcInfo.Status.ExecSessionIDs status.HealthStatus = msvcInfo.Status.HealthStatus + status.PodID = msvcInfo.Status.PodID execStatus = new(apps.MicroserviceExecStatusInfo) execStatus.Status = msvcInfo.ExecStatus.Status execStatus.ExecSessionID = msvcInfo.ExecStatus.ExecSessionID @@ -301,103 +350,232 @@ func mapExtraHosts(in []client.MicroserviceExtraHost) (out []apps.MicroserviceEx return } -// FormatAgentStatus formats agent status for human-readable output -func FormatAgentStatus(status rsc.AgentStatus) map[string]interface{} { - // Use ordered map to ensure consistent output order - formatted := make(map[string]interface{}) - - // Core status fields (ordered for consistent output) - formatted["daemonStatus"] = status.DaemonStatus - formatted["securityStatus"] = status.SecurityStatus - formatted["warningMessage"] = status.WarningMessage - if status.PlatformStatus != nil { - formatted["platformStatus"] = formatPlatformStatus(status.PlatformStatus) +func mapMicroserviceCatalog(in *client.MicroserviceCatalog) *apps.MicroserviceCatalog { + if in == nil { + return nil } - formatted["securityViolationInfo"] = status.SecurityViolationInfo - formatted["availableRuntimes"] = status.AvailableRuntimes - formatted["runtimeAgentPhase"] = status.RuntimeAgentPhase - formatted["controlPlaneQuiesced"] = status.ControlPlaneQuiesced - - // Format timestamps - if status.LastActive > 0 { - formatted["lastActive"] = time.Unix(status.LastActive/1000, (status.LastActive%1000)*1000000).UTC().Format(time.RFC3339) + out := &apps.MicroserviceCatalog{ + BindPath: in.BindPath, + Permissions: in.Permissions, } - - // Format uptime as duration - if status.UptimeMs > 0 { - uptime := time.Duration(status.UptimeMs) * time.Millisecond - formatted["uptime"] = util.FormatDuration(uptime) + if len(in.Items) > 0 { + out.Items = make([]apps.MicroserviceCatalogItem, len(in.Items)) + for i, item := range in.Items { + out.Items[i] = apps.MicroserviceCatalogItem{Name: item.Name} + } } + return out +} - // Format usage — memoryUsage is MiB (binary); diskUsage is GiB (decimal); cpuUsage is percent. - if status.MemoryUsage > 0 { - memoryBytes := status.MemoryUsage * 1024 * 1024 - formatted["memoryUsage"] = formatBytesAuto(memoryBytes) +func mapMicroserviceKnowledgeCatalog(in *client.KnowledgeCatalog) *apps.KnowledgeCatalog { + if in == nil { + return nil } - if status.DiskUsage > 0 { - diskBytes := status.DiskUsage * 1_000_000_000 - formatted["diskUsage"] = formatBytesAuto(diskBytes) + out := &apps.KnowledgeCatalog{ + BindPath: in.BindPath, + Permissions: in.Permissions, } - if status.CPUUsage > 0 { - formatted["cpuUsage"] = fmt.Sprintf("%.2f %%", status.CPUUsage) + if len(in.Items) > 0 { + out.Items = make([]apps.KnowledgeCatalogItem, len(in.Items)) + for i, item := range in.Items { + out.Items[i] = apps.KnowledgeCatalogItem{Name: item.Name} + } } + return out +} - // Format system available resources - if status.SystemAvailableMemory > 0 { - // Convert from KB to bytes for auto-scaling - memoryBytes := status.SystemAvailableMemory - formatted["systemAvailableMemory"] = formatBytesAuto(memoryBytes) +func mapUlimits(in map[string]client.ContainerUlimit) map[string]apps.MicroserviceUlimit { + if len(in) == 0 { + return nil } - if status.SystemAvailableDisk > 0 { - // Convert from bytes to bytes for auto-scaling (already in bytes) - formatted["systemAvailableDisk"] = formatBytesAuto(float64(status.SystemAvailableDisk)) + out := make(map[string]apps.MicroserviceUlimit, len(in)) + for k, u := range in { + out[k] = apps.MicroserviceUlimit{Soft: u.Soft, Hard: u.Hard} } + return out +} - // Add system total CPU (placeholder since not available from SDK) - if status.SystemTotalCPU > 0 { - formatted["systemTotalCPU"] = fmt.Sprintf("%.2f %%", status.SystemTotalCPU) +func mapDevices(in []client.ContainerDevice) []apps.MicroserviceDevice { + if len(in) == 0 { + return nil } + out := make([]apps.MicroserviceDevice, len(in)) + for i, d := range in { + out[i] = apps.MicroserviceDevice{ + HostPath: d.HostPath, + ContainerPath: d.ContainerPath, + Permissions: d.Permissions, + } + } + return out +} - formatted["memoryViolation"] = status.MemoryViolation - formatted["diskViolation"] = status.DiskViolation - formatted["cpuViolation"] = status.CPUViolation - formatted["repositoryStatus"] = status.RepositoryStatus +func mapTmpfs(in []client.ContainerTmpfs) []apps.MicroserviceTmpfs { + if len(in) == 0 { + return nil + } + out := make([]apps.MicroserviceTmpfs, len(in)) + for i, t := range in { + out[i] = apps.MicroserviceTmpfs{ + ContainerPath: t.ContainerPath, + Mode: t.Mode, + } + if t.Size != 0 { + size := t.Size + out[i].Size = &size + } + } + return out +} - // Format last status time +// FormatAgentStatus formats agent status for human-readable output in a stable field order. +func FormatAgentStatus(status rsc.AgentStatus) yaml.MapSlice { + formatted := yaml.MapSlice{} + + formatted = appendItem(formatted, "version", status.Version) + formatted = appendItem(formatted, "daemonStatus", status.DaemonStatus) + formatted = appendItem(formatted, "securityStatus", status.SecurityStatus) + formatted = appendItem(formatted, "securityViolationInfo", status.SecurityViolationInfo) + formatted = appendItem(formatted, "warningMessage", status.WarningMessage) + formatted = appendItem(formatted, "gpsStatus", status.GpsStatus) + formatted = appendItem(formatted, "ipAddress", status.IPAddress) + formatted = appendItem(formatted, "ipAddressExternal", status.IPAddressExternal) + if status.LastActive > 0 { + formatted = appendItem(formatted, "lastActive", time.Unix(status.LastActive/1000, (status.LastActive%1000)*1000000).UTC().Format(time.RFC3339)) + } if status.LastStatusTimeMsUTC > 0 { - formatted["lastStatusTime"] = time.Unix(status.LastStatusTimeMsUTC/1000, (status.LastStatusTimeMsUTC%1000)*1000000).UTC().Format(time.RFC3339) + formatted = appendItem(formatted, "lastStatusTime", time.Unix(status.LastStatusTimeMsUTC/1000, (status.LastStatusTimeMsUTC%1000)*1000000).UTC().Format(time.RFC3339)) + } + if status.LastCommandTimeMsUTC > 0 { + formatted = appendItem(formatted, "lastCommandTime", time.Unix(status.LastCommandTimeMsUTC/1000, (status.LastCommandTimeMsUTC%1000)*1000000).Format(time.RFC3339)) + } else { + formatted = appendItem(formatted, "lastCommandTime", "Never") + } + if status.UptimeMs > 0 { + uptime := time.Duration(status.UptimeMs) * time.Millisecond + formatted = appendItem(formatted, "uptime", util.FormatDuration(uptime)) } - formatted["ipAddress"] = status.IPAddress - formatted["ipAddressExternal"] = status.IPAddressExternal + // memoryUsage is MiB (binary); diskUsage is GiB (decimal); cpuUsage is 100 = 1 core. + if status.CPUUsage > 0 { + formatted = appendItem(formatted, "cpuUsage", formatCPUCores(status.CPUUsage)) + } + if status.MemoryUsage > 0 { + formatted = appendItem(formatted, "memoryUsage", formatBytesAuto(status.MemoryUsage*1024*1024)) + } + if status.DiskUsage > 0 { + formatted = appendItem(formatted, "diskUsage", formatBytesAuto(status.DiskUsage*1_000_000_000)) + } + formatted = appendItem(formatted, "cpuViolation", status.CPUViolation) + formatted = appendItem(formatted, "memoryViolation", status.MemoryViolation) + formatted = appendItem(formatted, "diskViolation", status.DiskViolation) - // Format processed messages - if status.ProcessedMessaged > 0 { - formatted["processedMessages"] = formatNumber(status.ProcessedMessaged) + if hostMetricsReported(status) { + if status.SystemOs != "" { + formatted = appendItem(formatted, "systemOs", status.SystemOs) + } + if status.SystemOsVersion != "" { + formatted = appendItem(formatted, "systemOsVersion", status.SystemOsVersion) + } + if status.SystemKernelVersion != "" { + formatted = appendItem(formatted, "systemKernelVersion", status.SystemKernelVersion) + } + if status.SystemCpus != 0 { + formatted = appendItem(formatted, "systemCpus", status.SystemCpus) + } + if status.SystemTotalCPU > 0 { + formatted = appendItem(formatted, "systemTotalCPU", fmt.Sprintf("%.2f %%", status.SystemTotalCPU)) + } + if status.SystemTotalMemory > 0 { + formatted = appendItem(formatted, "systemTotalMemory", formatBytesAuto(float64(status.SystemTotalMemory))) + } + if status.SystemAvailableMemory > 0 { + formatted = appendItem(formatted, "systemAvailableMemory", formatBytesAuto(float64(status.SystemAvailableMemory))) + } + if status.SystemTotalDisk > 0 { + formatted = appendItem(formatted, "systemTotalDisk", formatBytesAuto(float64(status.SystemTotalDisk))) + } + if status.SystemAvailableDisk > 0 { + formatted = appendItem(formatted, "systemAvailableDisk", formatBytesAuto(float64(status.SystemAvailableDisk))) + } } - // Format message speed - if status.MessageSpeed > 0 { - formatted["messageSpeed"] = fmt.Sprintf("%.1f msg/s", status.MessageSpeed) + formatted = appendItem(formatted, "availableRuntimes", status.AvailableRuntimes) + if parsed, ok := parseJSONBlob(status.RuntimeClasses); ok { + formatted = appendItem(formatted, "runtimeClasses", parsed) + } + formatted = appendItem(formatted, "runtimeAgentPhase", status.RuntimeAgentPhase) + if parsed, ok := parseJSONBlob(status.AvailableCdiDevices); ok { + formatted = appendItem(formatted, "availableCdiDevices", parsed) + } + formatted = appendItem(formatted, "controlPlaneQuiesced", status.ControlPlaneQuiesced) + if status.PlatformStatus != nil { + formatted = appendItem(formatted, "platformStatus", formatPlatformStatus(status.PlatformStatus)) } - // Format last command time - if status.LastCommandTimeMsUTC > 0 { - formatted["lastCommandTime"] = time.Unix(status.LastCommandTimeMsUTC/1000, (status.LastCommandTimeMsUTC%1000)*1000000).Format(time.RFC3339) - } else { - formatted["lastCommandTime"] = "Never" + formatted = appendItem(formatted, "activeModels", status.ActiveModels) + if parsed, ok := parseJSONBlob(status.ModelStatus); ok { + formatted = appendItem(formatted, "modelStatus", parsed) + } + if status.ModelLastUpdate > 0 { + formatted = appendItem(formatted, "modelLastUpdate", formatUnixMillisUTC(status.ModelLastUpdate)) + } + formatted = appendItem(formatted, "activeKnowledge", status.ActiveKnowledge) + if parsed, ok := parseJSONBlob(status.KnowledgeStatus); ok { + formatted = appendItem(formatted, "knowledgeStatus", parsed) + } + if status.KnowledgeLastUpdate > 0 { + formatted = appendItem(formatted, "knowledgeLastUpdate", formatUnixMillisUTC(status.KnowledgeLastUpdate)) } - formatted["version"] = status.Version - formatted["isReadyToUpgrade"] = status.IsReadyToUpgrade - formatted["isReadyToRollback"] = status.IsReadyToRollback - formatted["tunnel"] = status.Tunnel - formatted["volumeMounts"] = status.VolumeMounts - formatted["gpsStatus"] = status.GpsStatus + formatted = appendItem(formatted, "repositoryStatus", status.RepositoryStatus) + formatted = appendItem(formatted, "isReadyToUpgrade", status.IsReadyToUpgrade) + formatted = appendItem(formatted, "isReadyToRollback", status.IsReadyToRollback) + formatted = appendItem(formatted, "tunnel", status.Tunnel) + formatted = appendItem(formatted, "volumeMounts", status.VolumeMounts) return formatted } +func hostMetricsReported(status rsc.AgentStatus) bool { + return status.SystemCpus != 0 || + status.SystemTotalMemory != 0 || + status.SystemAvailableMemory != 0 || + status.SystemTotalDisk != 0 || + status.SystemAvailableDisk != 0 || + status.SystemTotalCPU != 0 || + status.SystemOs != "" || + status.SystemOsVersion != "" || + status.SystemKernelVersion != "" +} + +func appendItem(dst yaml.MapSlice, key string, value interface{}) yaml.MapSlice { + return append(dst, yaml.MapItem{Key: key, Value: value}) +} + +func formatCPUCores(usage float64) string { + return fmt.Sprintf("%.2f cores", usage/100) +} + +func formatUnixMillisUTC(ms int64) string { + return time.Unix(ms/1000, (ms%1000)*1_000_000).UTC().Format(time.RFC3339) +} + +func parseJSONBlob(raw string) (interface{}, bool) { + raw = strings.TrimSpace(raw) + if raw == "" { + return nil, false + } + var v interface{} + if err := json.Unmarshal([]byte(raw), &v); err != nil { + return raw, true + } + if v == nil { + return nil, false + } + return v, true +} + func formatPlatformStatus(ps *client.PlatformStatus) map[string]interface{} { if ps == nil { return nil @@ -448,16 +626,3 @@ func formatBytesAuto(bytes float64) string { } return fmt.Sprintf("%.1f %cB", bytes/float64(div), "KMGTPE"[exp]) } - -func formatNumber(num int64) string { - if num < 1000 { - return fmt.Sprintf("%d", num) - } - if num < 1000000 { - return fmt.Sprintf("%.1fK", float64(num)/1000) - } - if num < 1000000000 { - return fmt.Sprintf("%.1fM", float64(num)/1000000) - } - return fmt.Sprintf("%.1fB", float64(num)/1000000000) -} diff --git a/internal/describe/v39_test.go b/internal/describe/v39_test.go new file mode 100644 index 000000000..13991ca84 --- /dev/null +++ b/internal/describe/v39_test.go @@ -0,0 +1,264 @@ +package describe + +import ( + "strings" + "testing" + "time" + + apps "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/apps" + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + "github.com/eclipse-iofog/iofogctl/internal/config" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v2" +) + +func TestModelHeaderYAML(t *testing.T) { + header := modelHeader("default", "smollm2-135m", &client.Model{ + UUID: "model-uuid", + Name: "smollm2-135m", + Repo: "ai/smollm2", + Revision: "sha256:abc", + RegistryID: 1, + Format: "gguf", + }, []string{"lima"}) + + require.Equal(t, config.ModelKind, header.Kind) + out, err := yaml.Marshal(header) + require.NoError(t, err) + text := string(out) + require.Contains(t, text, "kind: Model") + require.Contains(t, text, "uuid: model-uuid") + require.Contains(t, text, "registryId: 1") + require.Contains(t, text, "linkedAgents:") + require.Contains(t, text, "- lima") + require.True(t, strings.Contains(text, "spec:")) +} + +func TestKnowledgeHeaderYAML(t *testing.T) { + header := knowledgeHeader("default", "product-docs", &client.Knowledge{ + UUID: "knowledge-uuid", + Name: "product-docs", + Repo: "docs/product", + Revision: "sha256:def", + RegistryID: 3, + Files: []string{"guide.md"}, + Format: "jsonl", + }, []string{"lima"}) + + require.Equal(t, config.KnowledgeKind, header.Kind) + out, err := yaml.Marshal(header) + require.NoError(t, err) + text := string(out) + require.Contains(t, text, "kind: Knowledge") + require.Contains(t, text, "uuid: knowledge-uuid") + require.Contains(t, text, "registryId: 3") + require.Contains(t, text, "format: jsonl") + require.Contains(t, text, "linkedAgents:") + require.Contains(t, text, "- lima") + require.True(t, strings.Contains(text, "spec:")) +} + +func TestRuntimeClassHeaderYAML(t *testing.T) { + header := runtimeClassHeader("default", "spin", "spin", []string{"lima"}) + require.Equal(t, config.RuntimeClassKind, header.Kind) + require.Equal(t, "spin", header.Handler) + require.Nil(t, header.Spec) + + out, err := yaml.Marshal(header) + require.NoError(t, err) + text := string(out) + require.Contains(t, text, "kind: RuntimeClass") + require.Contains(t, text, "handler: spin") + require.NotContains(t, text, "spec:") + require.Contains(t, text, "linkedAgents:") + require.Contains(t, text, "- lima") + require.Less(t, strings.Index(text, "handler: spin"), strings.Index(text, "linkedAgents:")) +} + +func TestMicroserviceTemplateHeaderYAML(t *testing.T) { + header := microserviceTemplateHeader("default", "ms-template", &client.MicroserviceTemplate{ + Name: "ms-template", + Description: "Template for creating microservices", + Variables: []client.MicroserviceTemplateVariable{ + {Key: "application", Description: "Application name", DefaultValue: "test-app"}, + }, + Microservice: map[string]any{"application": "{{application}}"}, + }) + require.Equal(t, config.MicroserviceTemplateKind, header.Kind) + + out, err := yaml.Marshal(header) + require.NoError(t, err) + text := string(out) + require.Contains(t, text, "kind: MicroserviceTemplate") + require.Contains(t, text, "description: Template for creating microservices") + require.Contains(t, text, "key: application") + require.Contains(t, text, "defaultValue: test-app") +} + +func TestDescribeFactoryV39Resources(t *testing.T) { + for _, resource := range []string{"knowledge", "model", "runtimeclass", "microservice-template"} { + exe, err := NewExecutor(&Options{Resource: resource, Name: "demo", Namespace: "default"}) + require.NoError(t, err, resource) + require.NotNil(t, exe, resource) + require.Equal(t, "demo", exe.GetName()) + } +} + +func TestConstructMicroserviceV39Fields(t *testing.T) { + msvc, status, _, err := constructMicroservice(&client.MicroserviceInfo{ + Name: "infer", + Config: "{}", + Annotations: "{}", + RunAsGroup: "1000", + ReadOnlyRootFilesystem: true, + Entrypoint: []string{"/usr/bin/python"}, + WorkingDir: "/app", + Cpus: 1.5, + Commands: []string{"-c"}, + CommandsAlias: []string{"-m", "app"}, + Runtime: "nvidia", + CdiDevices: []string{"nvidia.com/gpu=0"}, + Sysctls: map[string]string{"net.core.somaxconn": "1024"}, + Ulimits: map[string]client.ContainerUlimit{"nofile": {Soft: 1024, Hard: 2048}}, + MemoryReservation: 128, + MemorySwap: 256, + ShmSize: 64, + Tmpfs: []client.ContainerTmpfs{{ContainerPath: "/tmp", Size: 64}}, + Devices: []client.ContainerDevice{{HostPath: "/dev/nvidia0", ContainerPath: "/dev/nvidia0", Permissions: "rwm"}}, + Models: &client.MicroserviceCatalog{ + BindPath: "/models", + Permissions: "ro", + Items: []client.MicroserviceCatalogItem{{Name: "llama"}}, + }, + Knowledge: &client.KnowledgeCatalog{ + BindPath: "/knowledge", + Permissions: "ro", + Items: []client.KnowledgeCatalogItem{{Name: "product-docs"}}, + }, + Status: client.MicroserviceStatusInfo{Status: "RUNNING", PodID: "pod-abc"}, + }, "lima", "test-app", nil) + require.NoError(t, err) + require.Equal(t, "1000", msvc.Container.RunAsGroup) + require.True(t, msvc.Container.ReadOnlyRootFilesystem) + require.Equal(t, []string{"/usr/bin/python"}, msvc.Container.Entrypoint) + require.Equal(t, "/app", msvc.Container.WorkingDir) + require.NotNil(t, msvc.Container.CPUs) + require.Equal(t, 1.5, *msvc.Container.CPUs) + require.Equal(t, []string{"-m", "app"}, msvc.Container.Commands) + require.Equal(t, "nvidia", msvc.Container.Runtime) + require.Equal(t, []string{"nvidia.com/gpu=0"}, msvc.Container.CdiDevices) + require.Equal(t, "1024", msvc.Container.Sysctls["net.core.somaxconn"]) + require.Equal(t, 1024, msvc.Container.Ulimits["nofile"].Soft) + require.NotNil(t, msvc.Container.MemoryReservation) + require.Equal(t, int64(128), *msvc.Container.MemoryReservation) + require.NotNil(t, msvc.Container.ShmSize) + require.Equal(t, int64(64), *msvc.Container.ShmSize) + require.Len(t, msvc.Container.Tmpfs, 1) + require.Equal(t, "/tmp", msvc.Container.Tmpfs[0].ContainerPath) + require.NotNil(t, msvc.Container.Tmpfs[0].Size) + require.Equal(t, int64(64), *msvc.Container.Tmpfs[0].Size) + require.Equal(t, "/dev/nvidia0", msvc.Container.Devices[0].HostPath) + require.NotNil(t, msvc.Models) + require.Equal(t, "/models", msvc.Models.BindPath) + require.Equal(t, "llama", msvc.Models.Items[0].Name) + require.NotNil(t, msvc.Knowledge) + require.Equal(t, "/knowledge", msvc.Knowledge.BindPath) + require.Equal(t, "ro", msvc.Knowledge.Permissions) + require.Equal(t, "product-docs", msvc.Knowledge.Items[0].Name) + msvcYAML, err := yaml.Marshal(msvc) + require.NoError(t, err) + require.Contains(t, string(msvcYAML), "knowledge:") + require.Contains(t, string(msvcYAML), "product-docs") + require.Equal(t, "pod-abc", status.PodID) + + formatted := statusMap(FormatMicroserviceStatus(status)) + require.Equal(t, "pod-abc", formatted["podId"]) + require.Equal(t, "", formatted["lastError"]) + require.Equal(t, int64(0), formatted["lastErrorAt"]) + require.Equal(t, 0, formatted["restartCount"]) +} + +func TestConstructMicroserviceStatusErrorExtras(t *testing.T) { + const lastErrorAt int64 = 1710000000123 + _, status, _, err := constructMicroservice(&client.MicroserviceInfo{ + Name: "infer", + Config: "{}", + Annotations: "{}", + Status: client.MicroserviceStatusInfo{ + Status: "RUNNING", + ErrorMessage: "", + LastError: "OOMKilled", + LastErrorAt: lastErrorAt, + RestartCount: 2, + }, + }, "lima", "test-app", nil) + require.NoError(t, err) + require.Equal(t, "OOMKilled", status.LastError) + require.Equal(t, lastErrorAt, status.LastErrorAt) + require.Equal(t, 2, status.RestartCount) + + formatted := statusMap(FormatMicroserviceStatus(status)) + require.Equal(t, "OOMKilled", formatted["lastError"]) + require.Equal(t, 2, formatted["restartCount"]) + require.Equal(t, "", formatted["errorMessage"]) + wantAt := time.Unix(lastErrorAt/1000, (lastErrorAt%1000)*1000000).Format(time.RFC3339) + require.Equal(t, wantAt, formatted["lastErrorAt"]) +} + +func TestFormatMicroserviceStatusCPUCores(t *testing.T) { + formatted := statusMap(FormatMicroserviceStatus(&apps.MicroserviceStatusInfo{ + Status: "RUNNING", + CPUUsage: 32.97, + LastError: "", + })) + require.Equal(t, "0.33 cores", formatted["cpuUsage"]) + require.Equal(t, []string{ + "status", + "containerId", + "percentage", + "healthStatus", + "ipAddress", + "cpuUsage", + "restartCount", + "errorMessage", + "lastError", + "lastErrorAt", + "execSessionIds", + }, statusKeys(FormatMicroserviceStatus(&apps.MicroserviceStatusInfo{ + Status: "RUNNING", + CPUUsage: 32.97, + }))) +} + +func TestConstructMicroserviceRegistryAndServiceAccount(t *testing.T) { + msvc, _, _, err := constructMicroservice(&client.MicroserviceInfo{ + Name: "clickhouse", + Config: "{}", + Annotations: "{}", + RegistryID: 5, + Images: []client.CatalogImage{{ + ContainerImage: "dhi.io/clickhouse-server:26.7", + ArchID: 1, + }}, + ServiceAccount: &client.MicroserviceServiceAccountRef{ + RoleRef: client.RoleRef{ + Kind: "Role", + Name: "microservice", + }, + }, + }, "lima", "test-app", nil) + require.NoError(t, err) + require.NotNil(t, msvc.Images) + require.Equal(t, apps.RegistryRef(5), msvc.Images.Registry) + require.NotNil(t, msvc.ServiceAccount) + require.Equal(t, "Role", msvc.ServiceAccount.RoleRef.Kind) + require.Equal(t, "microservice", msvc.ServiceAccount.RoleRef.Name) + + out, err := yaml.Marshal(msvc) + require.NoError(t, err) + text := string(out) + require.Contains(t, text, "registry: 5") + require.NotContains(t, text, "registry: \"5\"") + require.Contains(t, text, "serviceAccount:") + require.Contains(t, text, "name: microservice") +} diff --git a/internal/detach/knowledge/execute.go b/internal/detach/knowledge/execute.go new file mode 100644 index 000000000..b1ae983de --- /dev/null +++ b/internal/detach/knowledge/execute.go @@ -0,0 +1,49 @@ +package detachknowledge + +import ( + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + "github.com/eclipse-iofog/iofogctl/internal/execute" + clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +type Options struct { + Name string + Agents []string + Namespace string +} + +type executor struct { + name string + agents []string + namespace string +} + +func NewExecutor(opt Options) execute.Executor { + return &executor{ + name: opt.Name, + agents: opt.Agents, + namespace: opt.Namespace, + } +} + +func (exe *executor) GetName() string { + return exe.name +} + +func (exe *executor) Execute() error { + util.SpinStart("Detaching Knowledge") + + clt, err := clientutil.NewControllerClient(exe.namespace) + if err != nil { + return err + } + + agentUUIDs, err := clientutil.AgentUUIDsFromNames(clt, exe.agents) + if err != nil { + return err + } + + req := client.FogLinkRequest{FogUUIDs: agentUUIDs} + return clt.UnlinkKnowledge(exe.name, &req) +} diff --git a/internal/detach/model/execute.go b/internal/detach/model/execute.go new file mode 100644 index 000000000..2fe5a5f80 --- /dev/null +++ b/internal/detach/model/execute.go @@ -0,0 +1,49 @@ +package detachmodel + +import ( + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + "github.com/eclipse-iofog/iofogctl/internal/execute" + clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +type Options struct { + Name string + Agents []string + Namespace string +} + +type executor struct { + name string + agents []string + namespace string +} + +func NewExecutor(opt Options) execute.Executor { + return &executor{ + name: opt.Name, + agents: opt.Agents, + namespace: opt.Namespace, + } +} + +func (exe *executor) GetName() string { + return exe.name +} + +func (exe *executor) Execute() error { + util.SpinStart("Detaching Model") + + clt, err := clientutil.NewControllerClient(exe.namespace) + if err != nil { + return err + } + + agentUUIDs, err := clientutil.AgentUUIDsFromNames(clt, exe.agents) + if err != nil { + return err + } + + req := client.FogLinkRequest{FogUUIDs: agentUUIDs} + return clt.UnlinkModel(exe.name, &req) +} diff --git a/internal/detach/runtimeclass/execute.go b/internal/detach/runtimeclass/execute.go new file mode 100644 index 000000000..a8dfc91fd --- /dev/null +++ b/internal/detach/runtimeclass/execute.go @@ -0,0 +1,49 @@ +package detachruntimeclass + +import ( + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + "github.com/eclipse-iofog/iofogctl/internal/execute" + clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" + "github.com/eclipse-iofog/iofogctl/pkg/util" +) + +type Options struct { + Name string + Agents []string + Namespace string +} + +type executor struct { + name string + agents []string + namespace string +} + +func NewExecutor(opt Options) execute.Executor { + return &executor{ + name: opt.Name, + agents: opt.Agents, + namespace: opt.Namespace, + } +} + +func (exe *executor) GetName() string { + return exe.name +} + +func (exe *executor) Execute() error { + util.SpinStart("Detaching RuntimeClass") + + clt, err := clientutil.NewControllerClient(exe.namespace) + if err != nil { + return err + } + + agentUUIDs, err := clientutil.AgentUUIDsFromNames(clt, exe.agents) + if err != nil { + return err + } + + req := client.FogLinkRequest{FogUUIDs: agentUUIDs} + return clt.UnlinkRuntimeClass(exe.name, &req) +} diff --git a/internal/execute/utils.go b/internal/execute/utils.go index 27b8c0728..5fc6a6425 100644 --- a/internal/execute/utils.go +++ b/internal/execute/utils.go @@ -20,6 +20,7 @@ type headerDecode struct { Spec interface{} `yaml:"spec,omitempty"` Data interface{} `yaml:"data,omitempty"` Status interface{} `yaml:"status,omitempty"` + Handler string `yaml:"handler,omitempty"` Rules interface{} `yaml:"rules,omitempty"` RoleRef interface{} `yaml:"roleRef,omitempty"` Subjects interface{} `yaml:"subjects,omitempty"` @@ -158,6 +159,7 @@ func headerDecodeToHeader(h *headerDecode) *config.Header { Spec: h.Spec, Data: h.Data, Status: h.Status, + Handler: h.Handler, } switch kind { diff --git a/internal/execute/utils_test.go b/internal/execute/utils_test.go index 009a6e590..dea6eee75 100644 --- a/internal/execute/utils_test.go +++ b/internal/execute/utils_test.go @@ -1,10 +1,12 @@ package execute import ( + "bytes" "testing" "github.com/eclipse-iofog/iofogctl/internal/config" "github.com/stretchr/testify/require" + "gopkg.in/yaml.v2" ) func TestHeaderDecodeToHeaderRemoteControllerAlias(t *testing.T) { @@ -16,3 +18,23 @@ func TestHeaderDecodeToHeaderControllerUnchanged(t *testing.T) { header := headerDecodeToHeader(&headerDecode{Kind: config.RemoteControllerKind}) require.Equal(t, config.RemoteControllerKind, header.Kind) } + +func TestHeaderDecodeAcceptsRuntimeClassRootHandler(t *testing.T) { + raw := []byte(`apiVersion: datasance.com/v3 +kind: RuntimeClass +metadata: + name: spin +handler: spin +`) + dec := yaml.NewDecoder(bytes.NewReader(raw)) + dec.SetStrict(true) + var h headerDecode + require.NoError(t, dec.Decode(&h)) + require.Equal(t, config.RuntimeClassKind, h.Kind) + require.Equal(t, "spin", h.Handler) + require.Equal(t, "spin", h.Metadata.Name) + + header := headerDecodeToHeader(&h) + require.Equal(t, "spin", header.Handler) + require.Equal(t, config.RuntimeClassKind, header.Kind) +} diff --git a/internal/get/all.go b/internal/get/all.go index 2749d6c25..fbb8b7b94 100644 --- a/internal/get/all.go +++ b/internal/get/all.go @@ -15,6 +15,8 @@ var ( getVolumeTable, getServiceTable, getVolumeMountTable, + getModelTable, + getKnowledgeTable, } ) @@ -135,3 +137,19 @@ func getVolumeMountTable(namespace string, tableChan tableChannel) { err: err, } } + +func getModelTable(namespace string, tableChan tableChannel) { + table, err := generateModelsOutput(namespace) + tableChan <- tableQuery{ + table: table, + err: err, + } +} + +func getKnowledgeTable(namespace string, tableChan tableChannel) { + table, err := generateKnowledgeOutput(namespace) + tableChan <- tableQuery{ + table: table, + err: err, + } +} diff --git a/internal/get/all_test.go b/internal/get/all_test.go new file mode 100644 index 000000000..bf7fe9cf1 --- /dev/null +++ b/internal/get/all_test.go @@ -0,0 +1,24 @@ +package get + +import ( + "reflect" + "runtime" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGetAllRoutinesIncludeModelsAndKnowledge(t *testing.T) { + t.Parallel() + + names := make([]string, 0, len(routines)) + for _, routine := range routines { + names = append(names, runtime.FuncForPC(reflect.ValueOf(routine).Pointer()).Name()) + } + joined := strings.Join(names, "\n") + require.Contains(t, joined, "getModelTable") + require.Contains(t, joined, "getKnowledgeTable") + require.NotContains(t, joined, "getRuntimeClass") + require.NotContains(t, joined, "getMicroserviceTemplate") +} diff --git a/internal/get/catalog.go b/internal/get/catalog.go index 4dd76ddfa..25aab7571 100644 --- a/internal/get/catalog.go +++ b/internal/get/catalog.go @@ -51,7 +51,7 @@ func generateCatalogOutput(namespace string) error { ID: item.ID, Name: item.Name, Description: item.Description, - Registry: clientutil.FormatRegistryID(item.RegistryID), + Registry: apps.RegistryRef(item.RegistryID), } for _, image := range item.Images { switch client.ArchIDToName[image.ArchID] { @@ -93,7 +93,7 @@ func tabulateCatalogItems(catalogItems []apps.CatalogItem) error { strconv.Itoa(item.ID), item.Name, item.Description, - item.Registry, + strconv.Itoa(item.Registry.Int()), item.AMD64, item.ARM64, item.RISCV64, diff --git a/internal/get/controllers.go b/internal/get/controllers.go index becf1df98..2e8a0e20f 100644 --- a/internal/get/controllers.go +++ b/internal/get/controllers.go @@ -73,54 +73,50 @@ func generateControllerOutput(namespace string) (table [][]string, err error) { headers := []string{"CONTROLLER", "STATUS", "AGE", "UPTIME", "VERSION", "ADDR", "PORT"} table[0] = append(table[0], headers...) - // Populate rows - for idx, ctrlConfig := range controllers { - // Instantiate connection to controller - ctrl, err := clientutil.NewControllerClient(namespace) - if err != nil { - return table, err - } + if len(controllers) == 0 { + return table, nil + } - // Ping status - ctrlStatus, err := ctrl.GetStatus() - uptime := "-" - status := "Failing" - if err == nil { - uptime = util.FormatDuration(time.Duration(int64(ctrlStatus.UptimeSeconds)) * time.Second) - status = ctrlStatus.Status - } - // Handle k8s pod statuses - if len(podStatuses) != 0 && idx < len(podStatuses) { + uptime := "-" + version := "-" + apiStatus := "Failing" + ctrl, err := clientutil.NewControllerClient(namespace) + if err != nil { + return table, err + } + ctrlStatus, err := ctrl.GetStatus() + if err == nil { + uptime = util.FormatDuration(time.Duration(int64(ctrlStatus.UptimeSeconds)) * time.Second) + apiStatus = ctrlStatus.Status + version = ctrlStatus.Versions.Controller + } + + // Populate rows — reuse ADDR/PORT from config and VERSION/UPTIME from a single GetStatus() + for idx, ctrlConfig := range controllers { + status := apiStatus + if idx < len(podStatuses) { status = podStatuses[idx] } - // Get age age := "-" if ctrlConfig.GetCreatedTime() != "" { age, _ = util.ElapsedUTC(ctrlConfig.GetCreatedTime(), util.NowUTC()) } addr, port := getAddressAndPort(ctrlConfig.GetEndpoint(), client.ControllerPortString) - - if err != nil { - return nil, err - } - row := []string{ ctrlConfig.GetName(), status, age, uptime, - ctrlStatus.Versions.Controller, + version, addr, port, - // expiryDate, - // agentSeats, } table[idx+1] = append(table[idx+1], row...) } - return table, err + return table, nil } func updateControllerPods(controlPlane *rsc.KubernetesControlPlane, namespace string) (err error) { diff --git a/internal/get/factory.go b/internal/get/factory.go index 0342982a6..3f8645e30 100644 --- a/internal/get/factory.go +++ b/internal/get/factory.go @@ -62,6 +62,14 @@ func NewExecutor(resourceType, namespace string, showDetached bool, resourceName return nil, util.NewInputError("get auth-group requires a name") } return newAuthGroupExecutor(namespace, resourceName), nil + case "knowledge": + return newKnowledgeExecutor(namespace), nil + case "models": + return newModelExecutor(namespace), nil + case "runtimeclass": + return newRuntimeClassExecutor(namespace), nil + case "microservice-templates": + return newMicroserviceTemplateExecutor(namespace), nil default: msg := "Unknown resource: '" + resourceType + "'" return nil, util.NewInputError(msg) diff --git a/internal/get/knowledge.go b/internal/get/knowledge.go new file mode 100644 index 000000000..a13d9fe91 --- /dev/null +++ b/internal/get/knowledge.go @@ -0,0 +1,61 @@ +package get + +import ( + "strconv" + + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + rsc "github.com/eclipse-iofog/iofogctl/internal/resource" + clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" +) + +type knowledgeExecutor struct { + namespace string +} + +func newKnowledgeExecutor(namespace string) *knowledgeExecutor { + return &knowledgeExecutor{namespace: namespace} +} + +func (exe *knowledgeExecutor) GetName() string { + return "" +} + +func (exe *knowledgeExecutor) Execute() error { + printNamespace(exe.namespace) + table, err := generateKnowledgeOutput(exe.namespace) + if err != nil { + return err + } + return print(table) +} + +func generateKnowledgeOutput(namespace string) ([][]string, error) { + clt, err := clientutil.NewControllerClient(namespace) + if err != nil { + if rsc.IsNoControlPlaneError(err) { + return formatKnowledgeOutput(nil), nil + } + return nil, err + } + + resp, err := clt.ListKnowledge() + if err != nil { + return nil, err + } + return formatKnowledgeOutput(resp.Knowledge), nil +} + +func formatKnowledgeOutput(items []client.Knowledge) [][]string { + table := make([][]string, len(items)+1) + table[0] = []string{"NAME", "REPO", "REGISTRY_ID", "FORMAT", "REVISION"} + for idx, item := range items { + table[idx+1] = []string{ + item.Name, + item.Repo, + strconv.Itoa(item.RegistryID), + item.Format, + item.Revision, + } + } + return table +} diff --git a/internal/get/microservice_templates.go b/internal/get/microservice_templates.go new file mode 100644 index 000000000..da1426032 --- /dev/null +++ b/internal/get/microservice_templates.go @@ -0,0 +1,60 @@ +package get + +import ( + "strconv" + + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + rsc "github.com/eclipse-iofog/iofogctl/internal/resource" + clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" +) + +type microserviceTemplateExecutor struct { + namespace string + templates []client.MicroserviceTemplate +} + +func newMicroserviceTemplateExecutor(namespace string) *microserviceTemplateExecutor { + return µserviceTemplateExecutor{namespace: namespace} +} + +func (exe *microserviceTemplateExecutor) GetName() string { + return "" +} + +func (exe *microserviceTemplateExecutor) Execute() error { + if err := exe.init(); err != nil { + return err + } + printNamespace(exe.namespace) + return print(generateMicroserviceTemplateOutput(exe.templates)) +} + +func (exe *microserviceTemplateExecutor) init() error { + clt, err := clientutil.NewControllerClient(exe.namespace) + if err != nil { + if rsc.IsNoControlPlaneError(err) { + return nil + } + return err + } + + resp, err := clt.ListMicroserviceTemplates() + if err != nil { + return err + } + exe.templates = resp.MicroserviceTemplates + return nil +} + +func generateMicroserviceTemplateOutput(templates []client.MicroserviceTemplate) [][]string { + table := make([][]string, len(templates)+1) + table[0] = []string{"NAME", "DESCRIPTION", "VARIABLES"} + for idx, template := range templates { + table[idx+1] = []string{ + template.Name, + template.Description, + strconv.Itoa(len(template.Variables)), + } + } + return table +} diff --git a/internal/get/microservices.go b/internal/get/microservices.go index 6f4103ace..6da994651 100644 --- a/internal/get/microservices.go +++ b/internal/get/microservices.go @@ -102,24 +102,7 @@ func (exe *microserviceExecutor) generateMicroserviceOutput() (table [][]string) } else { agentName = agent.Name } - status := ms.Status.Status - switch status { - case "": - status = "-" - case "PULLING": - if ms.Status.Percentage > 0 { - status = fmt.Sprintf("%s (%d%s)", ms.Status.Status, int(math.Round(ms.Status.Percentage)), "%") - } - } - if ms.Status.ErrorMessage != "" { - msg := ms.Status.ErrorMessage - if strings.Contains(msg, "invalid mount config for type \"bind\"") { - msg = "Volume missing" - } else if strings.Contains(msg, "runtime create failed") { - msg = "Error starting container" - } - status = fmt.Sprintf("%s (%s)", ms.Status.Status, msg) - } + status := formatMicroserviceGetStatus(ms.Status) natsAccess := "false" if ms.NatsConfig != nil && ms.NatsConfig.NatsAccess { natsAccess = "true" @@ -139,3 +122,38 @@ func (exe *microserviceExecutor) generateMicroserviceOutput() (table [][]string) return table } + +// formatMicroserviceGetStatus builds the STATUS cell. lastErrorAt is omitted (describe only). +// Empty lastError + restartCount 0 (healthy / older Controller) stays quiet. +func formatMicroserviceGetStatus(st client.MicroserviceStatusInfo) string { + status := st.Status + switch status { + case "": + status = "-" + case "PULLING": + if st.Percentage > 0 { + status = fmt.Sprintf("%s (%d%s)", st.Status, int(math.Round(st.Percentage)), "%") + } + } + if st.ErrorMessage != "" { + msg := st.ErrorMessage + if strings.Contains(msg, "invalid mount config for type \"bind\"") { + msg = "Volume missing" + } else if strings.Contains(msg, "runtime create failed") { + msg = "Error starting container" + } + status = fmt.Sprintf("%s (%s)", st.Status, msg) + } + + var extras []string + if st.RestartCount > 0 { + extras = append(extras, fmt.Sprintf("restarts: %d", st.RestartCount)) + } + if st.LastError != "" && st.LastError != st.ErrorMessage { + extras = append(extras, fmt.Sprintf("lastError: %s", st.LastError)) + } + if len(extras) > 0 { + status = fmt.Sprintf("%s (%s)", status, strings.Join(extras, ", ")) + } + return status +} diff --git a/internal/get/models.go b/internal/get/models.go new file mode 100644 index 000000000..ed1d8c7d2 --- /dev/null +++ b/internal/get/models.go @@ -0,0 +1,61 @@ +package get + +import ( + "strconv" + + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + rsc "github.com/eclipse-iofog/iofogctl/internal/resource" + clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" +) + +type modelExecutor struct { + namespace string +} + +func newModelExecutor(namespace string) *modelExecutor { + return &modelExecutor{namespace: namespace} +} + +func (exe *modelExecutor) GetName() string { + return "" +} + +func (exe *modelExecutor) Execute() error { + printNamespace(exe.namespace) + table, err := generateModelsOutput(exe.namespace) + if err != nil { + return err + } + return print(table) +} + +func generateModelsOutput(namespace string) ([][]string, error) { + clt, err := clientutil.NewControllerClient(namespace) + if err != nil { + if rsc.IsNoControlPlaneError(err) { + return generateModelOutput(nil), nil + } + return nil, err + } + + resp, err := clt.ListModels() + if err != nil { + return nil, err + } + return generateModelOutput(resp.Models), nil +} + +func generateModelOutput(models []client.Model) [][]string { + table := make([][]string, len(models)+1) + table[0] = []string{"NAME", "REPO", "REGISTRY_ID", "FORMAT", "REVISION"} + for idx, model := range models { + table[idx+1] = []string{ + model.Name, + model.Repo, + strconv.Itoa(model.RegistryID), + model.Format, + model.Revision, + } + } + return table +} diff --git a/internal/get/registry.go b/internal/get/registry.go index f236c7676..a3834ca64 100644 --- a/internal/get/registry.go +++ b/internal/get/registry.go @@ -52,7 +52,8 @@ func tabulateRegistries(catalogItems []client.RegistryInfo) error { "URL", "USERNAME", "PRIVATE", - "SECURE", + "TYPE", + "INSECURE", } table[0] = append(table[0], headers...) // Populate rows @@ -63,7 +64,8 @@ func tabulateRegistries(catalogItems []client.RegistryInfo) error { item.URL, item.Username, strconv.FormatBool(!item.IsPublic), - strconv.FormatBool(item.IsSecure), + item.Type, + strconv.FormatBool(item.Insecure), } table[idx+1] = append(table[idx+1], row...) idx++ diff --git a/internal/get/runtimeclass.go b/internal/get/runtimeclass.go new file mode 100644 index 000000000..d19fdf9ac --- /dev/null +++ b/internal/get/runtimeclass.go @@ -0,0 +1,57 @@ +package get + +import ( + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + rsc "github.com/eclipse-iofog/iofogctl/internal/resource" + clientutil "github.com/eclipse-iofog/iofogctl/internal/util/client" +) + +type runtimeClassExecutor struct { + namespace string + runtimeClasses []client.RuntimeClass +} + +func newRuntimeClassExecutor(namespace string) *runtimeClassExecutor { + return &runtimeClassExecutor{namespace: namespace} +} + +func (exe *runtimeClassExecutor) GetName() string { + return "" +} + +func (exe *runtimeClassExecutor) Execute() error { + if err := exe.init(); err != nil { + return err + } + printNamespace(exe.namespace) + return print(generateRuntimeClassOutput(exe.runtimeClasses)) +} + +func (exe *runtimeClassExecutor) init() error { + clt, err := clientutil.NewControllerClient(exe.namespace) + if err != nil { + if rsc.IsNoControlPlaneError(err) { + return nil + } + return err + } + + resp, err := clt.ListRuntimeClasses() + if err != nil { + return err + } + exe.runtimeClasses = resp.RuntimeClasses + return nil +} + +func generateRuntimeClassOutput(runtimeClasses []client.RuntimeClass) [][]string { + table := make([][]string, len(runtimeClasses)+1) + table[0] = []string{"NAME", "HANDLER"} + for idx, runtimeClass := range runtimeClasses { + table[idx+1] = []string{ + runtimeClass.Name, + runtimeClass.Handler, + } + } + return table +} diff --git a/internal/get/system_microservices.go b/internal/get/system_microservices.go index 2ae0ba42c..a5677ff22 100644 --- a/internal/get/system_microservices.go +++ b/internal/get/system_microservices.go @@ -2,8 +2,6 @@ package get import ( "fmt" - "math" - "strings" "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" rsc "github.com/eclipse-iofog/iofogctl/internal/resource" @@ -101,24 +99,7 @@ func (exe *systemMicroserviceExecutor) generateMicroserviceOutput() (table [][]s } else { agentName = agent.Name } - status := ms.Status.Status - switch status { - case "": - status = "-" - case "PULLING": - if ms.Status.Percentage > 0 { - status = fmt.Sprintf("%s (%d%s)", ms.Status.Status, int(math.Round(ms.Status.Percentage)), "%") - } - } - if ms.Status.ErrorMessage != "" { - msg := ms.Status.ErrorMessage - if strings.Contains(msg, "invalid mount config for type \"bind\"") { - msg = "Volume missing" - } else if strings.Contains(msg, "runtime create failed") { - msg = "Error starting container" - } - status = fmt.Sprintf("%s (%s)", ms.Status.Status, msg) - } + status := formatMicroserviceGetStatus(ms.Status) row := []string{ ms.Name, diff --git a/internal/get/v39_test.go b/internal/get/v39_test.go new file mode 100644 index 000000000..44038b807 --- /dev/null +++ b/internal/get/v39_test.go @@ -0,0 +1,107 @@ +package get + +import ( + "testing" + + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + "github.com/stretchr/testify/require" +) + +func TestGenerateModelOutput(t *testing.T) { + table := generateModelOutput([]client.Model{ + {Name: "smollm2-135m", Repo: "ai/smollm2", RegistryID: 1, Format: "gguf", Revision: "sha256:abc"}, + }) + require.Equal(t, []string{"NAME", "REPO", "REGISTRY_ID", "FORMAT", "REVISION"}, table[0]) + require.Equal(t, []string{"smollm2-135m", "ai/smollm2", "1", "gguf", "sha256:abc"}, table[1]) +} + +func TestGenerateModelOutputEmpty(t *testing.T) { + table := generateModelOutput(nil) + require.Equal(t, []string{"NAME", "REPO", "REGISTRY_ID", "FORMAT", "REVISION"}, table[0]) + require.Len(t, table, 1) +} + +func TestGenerateKnowledgeOutput(t *testing.T) { + table := formatKnowledgeOutput([]client.Knowledge{ + {Name: "product-docs", Repo: "docs/product", RegistryID: 3, Format: "jsonl", Revision: "sha256:def"}, + }) + require.Equal(t, []string{"NAME", "REPO", "REGISTRY_ID", "FORMAT", "REVISION"}, table[0]) + require.Equal(t, []string{"product-docs", "docs/product", "3", "jsonl", "sha256:def"}, table[1]) +} + +func TestGenerateKnowledgeOutputEmpty(t *testing.T) { + table := formatKnowledgeOutput(nil) + require.Equal(t, []string{"NAME", "REPO", "REGISTRY_ID", "FORMAT", "REVISION"}, table[0]) + require.Len(t, table, 1) +} + +func TestGenerateRuntimeClassOutput(t *testing.T) { + table := generateRuntimeClassOutput([]client.RuntimeClass{ + {Name: "spin", Handler: "spin"}, + }) + require.Equal(t, []string{"NAME", "HANDLER"}, table[0]) + require.Equal(t, []string{"spin", "spin"}, table[1]) +} + +func TestGenerateMicroserviceTemplateOutput(t *testing.T) { + table := generateMicroserviceTemplateOutput([]client.MicroserviceTemplate{ + {Name: "ms-template", Description: "Template for creating microservices", Variables: []client.MicroserviceTemplateVariable{{Key: "application"}, {Key: "agent-name"}}}, + }) + require.Equal(t, []string{"NAME", "DESCRIPTION", "VARIABLES"}, table[0]) + require.Equal(t, []string{"ms-template", "Template for creating microservices", "2"}, table[1]) +} + +func TestNewExecutorV39Resources(t *testing.T) { + for _, resource := range []string{"knowledge", "models", "runtimeclass", "microservice-templates"} { + exe, err := NewExecutor(resource, "default", false, "") + require.NoError(t, err, resource) + require.NotNil(t, exe, resource) + } +} + +func TestFormatMicroserviceGetStatus(t *testing.T) { + require.Equal(t, "RUNNING", formatMicroserviceGetStatus(client.MicroserviceStatusInfo{Status: "RUNNING"})) + require.Equal(t, "-", formatMicroserviceGetStatus(client.MicroserviceStatusInfo{})) + require.Equal(t, "RUNNING (restarts: 2, lastError: OOMKilled)", formatMicroserviceGetStatus(client.MicroserviceStatusInfo{ + Status: "RUNNING", + LastError: "OOMKilled", + LastErrorAt: 1710000000123, + RestartCount: 2, + })) + require.Equal(t, "FAILED (crash loop) (restarts: 1)", formatMicroserviceGetStatus(client.MicroserviceStatusInfo{ + Status: "FAILED", + ErrorMessage: "crash loop", + LastError: "crash loop", + RestartCount: 1, + })) + require.Equal(t, "FAILED (Volume missing)", formatMicroserviceGetStatus(client.MicroserviceStatusInfo{ + Status: "FAILED", + ErrorMessage: `invalid mount config for type "bind"`, + })) +} + +func TestGenerateMicroserviceOutputCrashSummary(t *testing.T) { + exe := newMicroserviceExecutor("default") + exe.msvcPerID["healthy"] = &client.MicroserviceInfo{UUID: "healthy", Name: "healthy", Status: client.MicroserviceStatusInfo{Status: "RUNNING"}} + exe.msvcPerID["recovered"] = &client.MicroserviceInfo{ + UUID: "recovered", + Name: "recovered", + Status: client.MicroserviceStatusInfo{ + Status: "RUNNING", + LastError: "OOMKilled", + RestartCount: 2, + }, + } + table := exe.generateMicroserviceOutput() + require.Equal(t, []string{"MICROSERVICE", "STATUS", "AGENT", "NATS ACCESS", "VOLUMES", "PORTS"}, table[0]) + + byName := map[string][]string{} + for _, row := range table[1:] { + if len(row) == 0 { + continue + } + byName[row[0]] = row + } + require.Equal(t, "RUNNING", byName["healthy"][1]) + require.Equal(t, "RUNNING (restarts: 2, lastError: OOMKilled)", byName["recovered"][1]) +} diff --git a/internal/resource/edgelet_golden_test.go b/internal/resource/edgelet_golden_test.go index 808d6f9a0..a7b88def6 100644 --- a/internal/resource/edgelet_golden_test.go +++ b/internal/resource/edgelet_golden_test.go @@ -125,6 +125,17 @@ func TestGoldenRoundTripRemoteAgent(t *testing.T) { require.Equal(t, *agent.Config.Arch, *round.Config.Arch) } +func TestAgentConfigurationRejectsHALFields(t *testing.T) { + for _, field := range []string{ + "deviceScanFrequency: 60\n", + "bluetoothEnabled: true\n", + "abstractedHardwareEnabled: false\n", + } { + _, err := UnmarshallAgentConfiguration([]byte(field)) + require.Error(t, err, "expected HAL field %q to be rejected", field) + } +} + func TestArchStringToID(t *testing.T) { id, ok := ArchStringToID("amd64") require.True(t, ok) diff --git a/internal/resource/testdata/edgelet/agent-config-spec.yaml b/internal/resource/testdata/edgelet/agent-config-spec.yaml index 434b3c7dd..b0d1bcc04 100644 --- a/internal/resource/testdata/edgelet/agent-config-spec.yaml +++ b/internal/resource/testdata/edgelet/agent-config-spec.yaml @@ -13,14 +13,11 @@ logDirectory: /var/log/edgelet/ logFileCount: 10 statusFrequency: 10 changeFrequency: 10 -deviceScanFrequency: 60 -bluetoothEnabled: true watchdogEnabled: false gpsMode: auto gpsScanFrequency: 60 gpsDevice: '' edgeGuardFrequency: 0 -abstractedHardwareEnabled: false upstreamRouters: ['default-router'] routerConfig: routerMode: edge diff --git a/internal/resource/testdata/edgelet/local-agent-spec.yaml b/internal/resource/testdata/edgelet/local-agent-spec.yaml index 4df7547e6..b35895908 100644 --- a/internal/resource/testdata/edgelet/local-agent-spec.yaml +++ b/internal/resource/testdata/edgelet/local-agent-spec.yaml @@ -20,14 +20,11 @@ config: logFileCount: 10 statusFrequency: 10 changeFrequency: 10 - deviceScanFrequency: 60 - bluetoothEnabled: true watchdogEnabled: false gpsMode: auto gpsScanFrequency: 60 gpsDevice: '' edgeGuardFrequency: 0 - abstractedHardwareEnabled: false upstreamRouters: ['default-router'] routerConfig: routerMode: edge diff --git a/internal/resource/testdata/edgelet/remote-agent-spec.yaml b/internal/resource/testdata/edgelet/remote-agent-spec.yaml index 9b3730af9..e766e6f22 100644 --- a/internal/resource/testdata/edgelet/remote-agent-spec.yaml +++ b/internal/resource/testdata/edgelet/remote-agent-spec.yaml @@ -21,14 +21,11 @@ config: logFileCount: 10 statusFrequency: 10 changeFrequency: 10 - deviceScanFrequency: 60 - bluetoothEnabled: true watchdogEnabled: false gpsMode: auto gpsScanFrequency: 60 gpsDevice: '' edgeGuardFrequency: 0 - abstractedHardwareEnabled: false upstreamRouters: ['default-router'] routerConfig: routerMode: edge diff --git a/internal/resource/types.go b/internal/resource/types.go index 7ede0926b..77983df3d 100644 --- a/internal/resource/types.go +++ b/internal/resource/types.go @@ -143,14 +143,15 @@ type Events struct { } type Registry struct { - URL *string `yaml:"url"` - Private *bool `yaml:"private"` - Username *string `yaml:"username"` - Password *string `yaml:"password"` - Email *string `yaml:"email"` - RequiresCert *bool `yaml:"requiresCert"` - Certificate *string `yaml:"certificate,omitempty"` - ID int `yaml:"id"` + URL *string `yaml:"url"` + Private *bool `yaml:"private"` + Username *string `yaml:"username"` + Password *string `yaml:"password"` + Email *string `yaml:"email,omitempty"` + Type *string `yaml:"type"` + CA *string `yaml:"ca,omitempty"` + Insecure *bool `yaml:"insecure,omitempty"` + ID int `yaml:"id"` } type Volume struct { @@ -199,12 +200,18 @@ type AgentStatus struct { SecurityViolationInfo string `json:"securityViolationInfo" yaml:"securityViolationInfo"` WarningMessage string `json:"warningMessage" yaml:"warningMessage"` UptimeMs int64 `json:"daemonOperatingDuration" yaml:"uptime"` - MemoryUsage float64 `json:"memoryUsage" yaml:"memoryUsage"` // MiB (binary) from agent status PUT - DiskUsage float64 `json:"diskUsage" yaml:"diskUsage"` // GiB (decimal) from Edgelet status PUT - CPUUsage float64 `json:"cpuUsage" yaml:"cpuUsage"` // percent - SystemAvailableMemory float64 `json:"systemAvailableMemory" yaml:"systemAvailableMemory"` // bytes - SystemAvailableDisk float64 `json:"systemAvailableDisk" yaml:"systemAvailableDisk"` // bytes - SystemTotalCPU float64 `json:"systemTotalCPU" yaml:"systemTotalCPU"` + MemoryUsage float64 `json:"memoryUsage" yaml:"memoryUsage"` // MiB (binary) from agent status PUT + DiskUsage float64 `json:"diskUsage" yaml:"diskUsage"` // GiB (decimal) from Edgelet status PUT + CPUUsage float64 `json:"cpuUsage" yaml:"cpuUsage"` // 100 = 1 core + SystemCpus int `json:"systemCpus,omitempty" yaml:"systemCpus,omitempty"` + SystemTotalMemory int64 `json:"systemTotalMemory" yaml:"systemTotalMemory"` // bytes + SystemAvailableMemory int64 `json:"systemAvailableMemory" yaml:"systemAvailableMemory"` // bytes + SystemTotalDisk int64 `json:"systemTotalDisk" yaml:"systemTotalDisk"` // bytes + SystemAvailableDisk int64 `json:"systemAvailableDisk" yaml:"systemAvailableDisk"` // bytes + SystemTotalCPU float64 `json:"systemTotalCPU" yaml:"systemTotalCPU"` // host utilization 0–100% + SystemOs string `json:"systemOs" yaml:"systemOs"` + SystemOsVersion string `json:"systemOsVersion" yaml:"systemOsVersion"` + SystemKernelVersion string `json:"systemKernelVersion" yaml:"systemKernelVersion"` MemoryViolation string `json:"memoryViolation" yaml:"memoryViolation"` DiskViolation string `json:"diskViolation" yaml:"diskViolation"` CPUViolation string `json:"cpuViolation" yaml:"cpuViolation"` @@ -225,6 +232,14 @@ type AgentStatus struct { RuntimeAgentPhase string `json:"runtimeAgentPhase" yaml:"runtimeAgentPhase"` ControlPlaneQuiesced bool `json:"controlPlaneQuiesced" yaml:"controlPlaneQuiesced"` PlatformStatus *client.PlatformStatus `json:"platformStatus,omitempty" yaml:"platformStatus,omitempty"` + RuntimeClasses string `json:"runtimeClasses" yaml:"runtimeClasses"` + AvailableCdiDevices string `json:"availableCdiDevices" yaml:"availableCdiDevices"` + ModelStatus string `json:"modelStatus" yaml:"modelStatus"` + ActiveModels int `json:"activeModels" yaml:"activeModels"` + ModelLastUpdate int64 `json:"modelLastUpdate" yaml:"modelLastUpdate"` + KnowledgeStatus string `json:"knowledgeStatus" yaml:"knowledgeStatus"` + ActiveKnowledge int `json:"activeKnowledge" yaml:"activeKnowledge"` + KnowledgeLastUpdate int64 `json:"knowledgeLastUpdate" yaml:"knowledgeLastUpdate"` } // ArchStringToID maps canonical architecture names to Controller archId values. diff --git a/internal/util/client/api.go b/internal/util/client/api.go index 83e5b7678..8475ec398 100644 --- a/internal/util/client/api.go +++ b/internal/util/client/api.go @@ -216,36 +216,33 @@ func GetAgentConfig(agentName, namespace string) (agentConfig rsc.AgentConfigura Description: agentInfo.Description, Arch: &arch, AgentConfiguration: client.AgentConfiguration{ - NetworkInterface: &agentInfo.NetworkInterface, - ContainerEngineURL: &agentInfo.ContainerEngineURL, - ContainerEngine: &agentInfo.ContainerEngine, - DeploymentType: &agentInfo.DeploymentType, - DiskLimit: &agentInfo.DiskLimit, - DiskDirectory: &agentInfo.DiskDirectory, - MemoryLimit: &agentInfo.MemoryLimit, - CPULimit: &agentInfo.CPULimit, - LogLimit: &agentInfo.LogLimit, - LogDirectory: &agentInfo.LogDirectory, - LogFileCount: &agentInfo.LogFileCount, - StatusFrequency: &agentInfo.StatusFrequency, - ChangeFrequency: &agentInfo.ChangeFrequency, - DeviceScanFrequency: &agentInfo.DeviceScanFrequency, - BluetoothEnabled: &agentInfo.BluetoothEnabled, - WatchdogEnabled: &agentInfo.WatchdogEnabled, - GpsMode: &agentInfo.GpsMode, - GpsScanFrequency: &agentInfo.GpsScanFrequency, - GpsDevice: &agentInfo.GpsDevice, - EdgeGuardFrequency: &agentInfo.EdgeGuardFrequency, - AbstractedHardwareEnabled: &agentInfo.AbstractedHardwareEnabled, - LogLevel: agentInfo.LogLevel, - PruningFrequency: agentInfo.PruningFrequency, - AvailableDiskThreshold: agentInfo.AvailableDiskThreshold, - UpstreamRouters: upstreamRoutersPtr, - NetworkRouter: networkRouterPtr, - RouterConfig: routerConfig, - UpstreamNatsServers: upstreamNatsServersPtr, - NatsConfig: natsConfig, - TimeZone: agentInfo.TimeZone, + NetworkInterface: &agentInfo.NetworkInterface, + ContainerEngineURL: &agentInfo.ContainerEngineURL, + ContainerEngine: &agentInfo.ContainerEngine, + DeploymentType: &agentInfo.DeploymentType, + DiskLimit: &agentInfo.DiskLimit, + DiskDirectory: &agentInfo.DiskDirectory, + MemoryLimit: &agentInfo.MemoryLimit, + CPULimit: &agentInfo.CPULimit, + LogLimit: &agentInfo.LogLimit, + LogDirectory: &agentInfo.LogDirectory, + LogFileCount: &agentInfo.LogFileCount, + StatusFrequency: &agentInfo.StatusFrequency, + ChangeFrequency: &agentInfo.ChangeFrequency, + WatchdogEnabled: &agentInfo.WatchdogEnabled, + GpsMode: &agentInfo.GpsMode, + GpsScanFrequency: &agentInfo.GpsScanFrequency, + GpsDevice: &agentInfo.GpsDevice, + EdgeGuardFrequency: &agentInfo.EdgeGuardFrequency, + LogLevel: agentInfo.LogLevel, + PruningFrequency: agentInfo.PruningFrequency, + AvailableDiskThreshold: agentInfo.AvailableDiskThreshold, + UpstreamRouters: upstreamRoutersPtr, + NetworkRouter: networkRouterPtr, + RouterConfig: routerConfig, + UpstreamNatsServers: upstreamNatsServersPtr, + NatsConfig: natsConfig, + TimeZone: agentInfo.TimeZone, }, } @@ -259,9 +256,15 @@ func GetAgentConfig(agentName, namespace string) (agentConfig rsc.AgentConfigura MemoryUsage: agentInfo.MemoryUsage, DiskUsage: agentInfo.DiskUsage, CPUUsage: agentInfo.CPUUsage, + SystemCpus: agentInfo.SystemCpus, + SystemTotalMemory: agentInfo.SystemTotalMemory, SystemAvailableMemory: agentInfo.SystemAvailableMemory, + SystemTotalDisk: agentInfo.SystemTotalDisk, SystemAvailableDisk: agentInfo.SystemAvailableDisk, SystemTotalCPU: agentInfo.SystemTotalCPU, + SystemOs: agentInfo.SystemOs, + SystemOsVersion: agentInfo.SystemOsVersion, + SystemKernelVersion: agentInfo.SystemKernelVersion, MemoryViolation: agentInfo.MemoryViolation, DiskViolation: agentInfo.DiskViolation, CPUViolation: agentInfo.CPUViolation, @@ -280,6 +283,14 @@ func GetAgentConfig(agentName, namespace string) (agentConfig rsc.AgentConfigura RuntimeAgentPhase: agentInfo.RuntimeAgentPhase, ControlPlaneQuiesced: agentInfo.ControlPlaneQuiesced, PlatformStatus: agentInfo.PlatformStatus, + RuntimeClasses: agentInfo.RuntimeClasses, + AvailableCdiDevices: agentInfo.AvailableCdiDevices, + ModelStatus: agentInfo.ModelStatus, + ActiveModels: agentInfo.ActiveModels, + ModelLastUpdate: agentInfo.ModelLastUpdate, + KnowledgeStatus: agentInfo.KnowledgeStatus, + ActiveKnowledge: agentInfo.ActiveKnowledge, + KnowledgeLastUpdate: agentInfo.KnowledgeLastUpdate, } return agentConfig, tags, agentStatus, err diff --git a/internal/util/client/link.go b/internal/util/client/link.go new file mode 100644 index 000000000..2cf142d78 --- /dev/null +++ b/internal/util/client/link.go @@ -0,0 +1,59 @@ +package client + +import ( + "errors" + "fmt" + + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" +) + +const runtimeClassLink400Prefix = "cannot attach runtimeclass: RuntimeClass can only be linked to edgelet agents" + +// AgentUUIDsFromNames resolves agent names to Controller fog UUIDs. +func AgentUUIDsFromNames(clt *client.Client, names []string) ([]string, error) { + uuids := make([]string, 0, len(names)) + for _, name := range names { + agentInfo, err := clt.GetAgentByName(name) + if err != nil { + return nil, err + } + uuids = append(uuids, agentInfo.UUID) + } + return uuids, nil +} + +// AgentNamesFromUUIDs maps fog UUIDs to agent names. Unknown UUIDs are kept as-is. +func AgentNamesFromUUIDs(agents []client.AgentInfo, uuids []string) []string { + if len(uuids) == 0 { + return nil + } + nameByUUID := make(map[string]string, len(agents)) + for _, agent := range agents { + if agent.UUID != "" { + nameByUUID[agent.UUID] = agent.Name + } + } + names := make([]string, 0, len(uuids)) + for _, uuid := range uuids { + if name := nameByUUID[uuid]; name != "" { + names = append(names, name) + continue + } + names = append(names, uuid) + } + return names +} + +// PrefixRuntimeClassLink400 adds a stable CLI prefix when Controller rejects a +// RuntimeClass link with HTTP 400 (non-edgelet fog). Other errors pass through +// so 409 bodies with blocking microservice UUIDs remain visible. +func PrefixRuntimeClassLink400(err error) error { + if err == nil { + return nil + } + var httpErr *client.HTTPError + if errors.As(err, &httpErr) && httpErr.Code == 400 { + return fmt.Errorf("%s\n%w", runtimeClassLink400Prefix, err) + } + return err +} diff --git a/internal/util/client/link_test.go b/internal/util/client/link_test.go new file mode 100644 index 000000000..5225bc723 --- /dev/null +++ b/internal/util/client/link_test.go @@ -0,0 +1,40 @@ +package client + +import ( + "errors" + "fmt" + "testing" + + "github.com/eclipse-iofog/iofog-go-sdk/v3/pkg/client" + "github.com/stretchr/testify/require" +) + +func TestAgentNamesFromUUIDs(t *testing.T) { + agents := []client.AgentInfo{ + {UUID: "aaa", Name: "lima"}, + {UUID: "bbb", Name: "edge-2"}, + } + require.Equal(t, []string{"lima", "edge-2"}, AgentNamesFromUUIDs(agents, []string{"aaa", "bbb"})) + require.Equal(t, []string{"lima", "missing-uuid"}, AgentNamesFromUUIDs(agents, []string{"aaa", "missing-uuid"})) + require.Nil(t, AgentNamesFromUUIDs(agents, nil)) +} + +func TestPrefixRuntimeClassLink400(t *testing.T) { + require.Nil(t, PrefixRuntimeClassLink400(nil)) + + passthrough := fmt.Errorf("network") + require.Equal(t, passthrough, PrefixRuntimeClassLink400(passthrough)) + + conflict := client.NewHTTPError("bound microservice uuids: [ms-1]", 409) + require.Equal(t, conflict, PrefixRuntimeClassLink400(conflict)) + require.Contains(t, conflict.Error(), "ms-1") + + badLink := client.NewHTTPError("RuntimeClass can only be linked to edgelet agents", 400) + mapped := PrefixRuntimeClassLink400(badLink) + require.Error(t, mapped) + require.Contains(t, mapped.Error(), runtimeClassLink400Prefix) + require.Contains(t, mapped.Error(), "RuntimeClass can only be linked to edgelet agents") + var httpErr *client.HTTPError + require.True(t, errors.As(mapped, &httpErr)) + require.Equal(t, 400, httpErr.Code) +} diff --git a/pkg/iofog/install/edgelet_config.go b/pkg/iofog/install/edgelet_config.go index 36292d57a..5d26e0a7b 100644 --- a/pkg/iofog/install/edgelet_config.go +++ b/pkg/iofog/install/edgelet_config.go @@ -201,9 +201,6 @@ func applyEdgeletProfileOverrides(profile map[string]interface{}, hostOS, arch s if cfg.ChangeFrequency != nil { profile["changeFrequency"] = formatFloat(*cfg.ChangeFrequency) } - if cfg.DeviceScanFrequency != nil { - profile["scanDevicesFreq"] = formatFloat(*cfg.DeviceScanFrequency) - } if cfg.WatchdogEnabled != nil { profile["watchdogEnabled"] = boolToOnOff(*cfg.WatchdogEnabled) } @@ -376,9 +373,6 @@ func buildBootstrapProfileFromAgentSpec(arch string, latitude, longitude float64 if cfg.ChangeFrequency != nil { profile["changeFrequency"] = formatFloat(*cfg.ChangeFrequency) } - if cfg.DeviceScanFrequency != nil { - profile["scanDevicesFreq"] = formatFloat(*cfg.DeviceScanFrequency) - } if cfg.WatchdogEnabled != nil { profile["watchdogEnabled"] = boolToOnOff(*cfg.WatchdogEnabled) } diff --git a/pkg/iofog/install/edgelet_scripts_test.go b/pkg/iofog/install/edgelet_scripts_test.go index 6a0822c6c..e4184afa0 100644 --- a/pkg/iofog/install/edgelet_scripts_test.go +++ b/pkg/iofog/install/edgelet_scripts_test.go @@ -75,17 +75,22 @@ func TestInstallScriptOTAParity(t *testing.T) { t.Fatalf("load install.sh: %v", err) } for _, needle := range []string{ - "stop_edgelet_service", + "replace_staged_edgelet_binary", + "stage_pending_install_receipt", + "commit_pending_install_receipt", + "write_previous_release", "EDGELET_SERVICE_ACTION", "edgelet_cli_version", "--upgrade", - "installed_embed_hash", - "record_restart_data_plane_decision", + "--rollback", } { if !strings.Contains(content, needle) { t.Fatalf("install.sh missing OTA parity marker %q", needle) } } + if strings.Contains(content, "stop_edgelet_service") { + t.Fatalf("install.sh must not stop control itself; replace_staged_edgelet_binary drains first") + } if !strings.Contains(content, "Skipping daemon start (--skip-start); use start_edgelet.sh") { t.Fatalf("install.sh missing --skip-start handling on upgrade path") } @@ -95,9 +100,16 @@ func TestInstallScriptOTAParity(t *testing.T) { t.Fatalf("load lib/embed.sh: %v", err) } for _, needle := range []string{ + "embed_bundle_ready", + "7f454c46", + "containerd-shim-runc-v2", "installed_embed_hash", "binary_embed_hash", "should_restart_data_plane", + "quiesce_data_plane_for_replace", + "runtime drain --direct", + "Data-plane drain did not verify; binary was not replaced", + "replace_staged_edgelet_binary", "start_edgelet_containerd_unit", "write_restart_data_plane_marker", "consume_restart_data_plane_marker", @@ -113,6 +125,9 @@ func TestInstallScriptOTAParity(t *testing.T) { } for _, needle := range []string{ "stop_edgelet_service", + "stop_edgelet_containerd_unit", + "stop_edgelet_dataplane_processes", + "start_edgelet_containerd_after_drain", "restart_edgelet_services", "restart_edgelet_containerd_service", "consume_containerd_restarted_marker", @@ -147,6 +162,12 @@ func TestInstallScriptOTAParity(t *testing.T) { if !strings.Contains(start, "consume_restart_data_plane_marker") { t.Fatalf("start_edgelet.sh missing embed-hash redeploy handling") } + if !strings.Contains(start, "start_edgelet_containerd_after_drain") { + t.Fatalf("start_edgelet.sh must start the data plane after a verified drain") + } + if !strings.Contains(start, "commit_pending_install_receipt") { + t.Fatalf("start_edgelet.sh must commit the install receipt after services start") + } if !strings.Contains(start, "wait_edgelet_containerd_socket") { t.Fatalf("start_edgelet.sh missing embedded systemd start parity") } diff --git a/pkg/iofog/install/k8s.go b/pkg/iofog/install/k8s.go index 3748053b4..259e3cd79 100644 --- a/pkg/iofog/install/k8s.go +++ b/pkg/iofog/install/k8s.go @@ -710,17 +710,40 @@ func (k8s *Kubernetes) GetControllerPods() (podNames []Pod, err error) { if err != nil { return } - // Find Controller pods (label key follows build flavor: e.g. datasance.com/component or iofog.org/component) + // Find live Controller pods (label key follows build flavor: e.g. datasance.com/component or iofog.org/component) componentLabel := util.GetCliCrdGroup() + "/component" - for idx := range pods.Items { - if controllerPodLabelsMatch(pods.Items[idx].Labels, componentLabel) { - podNames = append(podNames, Pod{ - Name: pods.Items[idx].Name, - Status: string(pods.Items[idx].Status.Phase), - }) + podNames = selectControllerPods(pods.Items, componentLabel) + return +} + +func selectControllerPods(pods []corev1.Pod, componentLabel string) []Pod { + selected := []Pod{} + for idx := range pods { + pod := pods[idx] + if !controllerPodLabelsMatch(pod.Labels, componentLabel) { + continue } + if !isLiveControllerPod(pod) { + continue + } + selected = append(selected, Pod{ + Name: pod.Name, + Status: string(pod.Status.Phase), + }) + } + return selected +} + +func isLiveControllerPod(pod corev1.Pod) bool { + if pod.DeletionTimestamp != nil { + return false + } + switch pod.Status.Phase { + case corev1.PodRunning, corev1.PodPending: + return true + default: + return false } - return } func controllerPodLabelsMatch(labels map[string]string, componentLabel string) bool { diff --git a/pkg/iofog/install/k8s_controller_pods_test.go b/pkg/iofog/install/k8s_controller_pods_test.go index ee29f4320..47f52e7c3 100644 --- a/pkg/iofog/install/k8s_controller_pods_test.go +++ b/pkg/iofog/install/k8s_controller_pods_test.go @@ -4,6 +4,8 @@ import ( "testing" "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestControllerPodLabelsMatch(t *testing.T) { @@ -55,3 +57,104 @@ func TestControllerPodLabelsMatch(t *testing.T) { }) } } + +func TestIsLiveControllerPod(t *testing.T) { + t.Parallel() + + now := metav1.Now() + tests := []struct { + name string + pod corev1.Pod + want bool + }{ + { + name: "running", + pod: corev1.Pod{Status: corev1.PodStatus{Phase: corev1.PodRunning}}, + want: true, + }, + { + name: "pending", + pod: corev1.Pod{Status: corev1.PodStatus{Phase: corev1.PodPending}}, + want: true, + }, + { + name: "failed", + pod: corev1.Pod{Status: corev1.PodStatus{Phase: corev1.PodFailed}}, + }, + { + name: "succeeded", + pod: corev1.Pod{Status: corev1.PodStatus{Phase: corev1.PodSucceeded}}, + }, + { + name: "unknown", + pod: corev1.Pod{Status: corev1.PodStatus{Phase: corev1.PodUnknown}}, + }, + { + name: "empty phase", + pod: corev1.Pod{}, + }, + { + name: "running but terminating", + pod: corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{DeletionTimestamp: &now}, + Status: corev1.PodStatus{Phase: corev1.PodRunning}, + }, + }, + { + name: "pending but terminating", + pod: corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{DeletionTimestamp: &now}, + Status: corev1.PodStatus{Phase: corev1.PodPending}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + require.Equal(t, tt.want, isLiveControllerPod(tt.pod)) + }) + } +} + +func TestSelectControllerPodsFiltersFailedAndTerminating(t *testing.T) { + t.Parallel() + + now := metav1.Now() + componentLabel := "datasance.com/component" + controllerLabels := map[string]string{componentLabel: "controller"} + routerLabels := map[string]string{componentLabel: "router"} + + pods := []corev1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{Name: "controller-running", Labels: controllerLabels}, + Status: corev1.PodStatus{Phase: corev1.PodRunning}, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "controller-pending", Labels: controllerLabels}, + Status: corev1.PodStatus{Phase: corev1.PodPending}, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "controller-failed", Labels: controllerLabels}, + Status: corev1.PodStatus{Phase: corev1.PodFailed}, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "controller-succeeded", Labels: controllerLabels}, + Status: corev1.PodStatus{Phase: corev1.PodSucceeded}, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "controller-deleting", Labels: controllerLabels, DeletionTimestamp: &now}, + Status: corev1.PodStatus{Phase: corev1.PodRunning}, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "router-running", Labels: routerLabels}, + Status: corev1.PodStatus{Phase: corev1.PodRunning}, + }, + } + + selected := selectControllerPods(pods, componentLabel) + require.Equal(t, []Pod{ + {Name: "controller-running", Status: string(corev1.PodRunning)}, + {Name: "controller-pending", Status: string(corev1.PodPending)}, + }, selected) +} diff --git a/test/func/check.bash b/test/func/check.bash index ddc5d9761..8bcd77886 100755 --- a/test/func/check.bash +++ b/test/func/check.bash @@ -254,9 +254,6 @@ function checkApplication() { DESC_MSVC=$(iofogctl -v -n "$NS_CHECK" describe microservice $APPLICATION_NAME/"${MSVC1_NAME}") echo "${DESC_MSVC}" | grep "test_mode: true" echo "${DESC_MSVC}" | grep "data_label: Anonymous_Person" - # Deploying an application should not update Agent config. This is a legacy overload of the functionality. - # Use a separate AgentConfig Kind to update the required config. - # [[ "bluetoothEnabled: true" == $(iofogctl -v -n "$NS_CHECK" describe agent-config "${NAME}-0" | grep bluetooth | awk '{$1=$1};1' ) ]] # Check ports msvcWithPorts=$(iofogctl -v -n "$NS_CHECK" get microservices | grep "$PORT_EXT:$PORT_INT") [[ "${MSVC2_NAME}" == $(echo "$msvcWithPorts" | awk '{print $1}') ]] @@ -322,8 +319,6 @@ function checkAgent() { # Checks list taken from init.bash CHECKS=("name:.$AGENT_NAME" "description:.special.test.agent" -"bluetoothEnabled:.true" -"abstractedHardwareEnabled:.false" "latitude:.46\.464646" "longitude:.64\.646464" "memoryLimit:.8192" @@ -333,8 +328,7 @@ function checkAgent() { "logLimit:.12" "logFileCount:.11" "statusFrequency:.9" -"changeFrequency:.8" -"deviceScanFrequency:.61") +"changeFrequency:.8") for CHECK in ${CHECKS[@]}; do echo $CHECK [[ ! -z $(iofogctl -v -n "$NS_CHECK" describe agent "$AGENT_NAME" $OPTIONS | grep "$CHECK") ]] diff --git a/test/func/init.bash b/test/func/init.bash index d3b9f284f..dce9c6e54 100755 --- a/test/func/init.bash +++ b/test/func/init.bash @@ -399,8 +399,6 @@ metadata: name: ${NAME}-0 spec: config: - bluetoothEnabled: true - abstractedHardwareEnabled: false memoryLimit: 8192 diskDirectory: /tmp/iofog-agent/ description: special test agent @@ -412,7 +410,6 @@ spec: logFileCount: 11 statusFrequency: 9 changeFrequency: 8 - deviceScanFrequency: 61 container: image: ${AGENT_IMAGE}" > test/conf/local-agent.yaml } @@ -446,8 +443,6 @@ metadata: name: $AGENT_NAME spec: config: - bluetoothEnabled: true - abstractedHardwareEnabled: false memoryLimit: 8192 diskDirectory: /tmp/iofog-agent/ description: special test agent @@ -459,7 +454,6 @@ spec: logFileCount: 11 statusFrequency: 9 changeFrequency: 8 - deviceScanFrequency: 61 host: ${HOSTS[$IDX]} ssh: user: ${USERS[$IDX]} @@ -631,8 +625,7 @@ spec: agent: name: \"{{$APP_TEMPLATE_KEY}}\" config: - bluetoothEnabled: true # this will install the iofog/restblue microservice - abstractedHardwareEnabled: false + description: special test agent images: arm: edgeworx/healthcare-heart-rate:arm-v1 x86: edgeworx/healthcare-heart-rate:x86-v1 diff --git a/versions.mk b/versions.mk index 5bf15893b..47f433217 100644 --- a/versions.mk +++ b/versions.mk @@ -1,6 +1,6 @@ -OPERATOR_VERSION ?= 3.8.3-rc.2 -CONTROLLER_VERSION ?= 3.8.3-rc.2 -ROUTER_VERSION ?= 3.8.3-rc.2 -NATS_VERSION ?= 2.14.5-rc.1 -EDGELET_BINARY_VERSION ?= v1.0.3-rc.2 -EDGELET_IMAGE_TAG ?= 1.0.3-rc.2 +OPERATOR_VERSION ?= 3.9.0-rc.1 +CONTROLLER_VERSION ?= 3.9.0-rc.9 +ROUTER_VERSION ?= 3.9.0-rc.1 +NATS_VERSION ?= 2.14.7-rc.1 +EDGELET_BINARY_VERSION ?= v1.1.0-rc.5 +EDGELET_IMAGE_TAG ?= 1.1.0-rc.5