diff --git a/.bob/plans/2025-01-31-mcpi-install.md b/.bob/plans/2025-01-31-mcpi-install.md new file mode 100644 index 0000000000..098dbea17a --- /dev/null +++ b/.bob/plans/2025-01-31-mcpi-install.md @@ -0,0 +1,911 @@ +# Plan: Add `mcpi-install` Command to MAS CLI + +## Objective + +Add a new `mcpi-install` CLI command that installs the **Maximo Cluster Performance Insights (MCPI)** add-on operator (`ibm-mas-cfg-mcpi`) via a Tekton pipeline. The implementation follows the exact same 4-layer architecture used by `aiservice-install`. + +--- + +## Repos Involved + +| Repo | What changes | +|------|-------------| +| **`cli`** (this repo) | Python CLI module + Tekton templates — Phases 1 & 2 of this plan | +| **`ansible-devops`** | Add `mcpicfg` role to the `ibm.mas_devops` collection so `run-role.sh mcpicfg` works in the CLI image | +| **`python-devops`** (local path: `/Users/tahamadou/Source/MAS/python-devops/`) | Add `launchMcpiInstallPipeline()` + `prepareMcpiPipelinesNamespace()` + 3 new templates (see Design Decisions) | +| **`ibm-mas`** | `mcpicfg` role source already exists at `operator/ibm-mas-cfg-mcpi/roles/mcpicfg/` — no changes needed here, role is copied to `ansible-devops` | + +--- + +## Design Decisions + +### Architecture Layers + +| Layer | Responsibility | +|-------|---------------| +| **Layer 1 — Dispatcher** | `__main__.py` routes `mcpi-install` → `McpiInstallApp().install()` | +| **Layer 2 — Python App** | `python/src/mas/cli/mcpi/install/` — interactive/non-interactive CLI | +| **Layer 3 — Tekton** | Task CRD + pipeline fragment + standalone pipeline + param files | +| **Layer 4 — Ansible** | `mcpicfg` role already exists in `ibm-mas`; invoked via `run-role.sh mcpicfg` | + +### CLI Parameters + +MCPI has no database, no S3, no ODH/RHOAI, no workspace sync. Its parameter set is minimal compared to AI Service: + +| CLI Flag | `dest` / param name | Required | Notes | +|----------|---------------------|----------|-------| +| `-c, --mas-catalog-version` | `mas_catalog_version` | Yes | Catalog selection | +| `--mas-catalog-digest` | `mas_catalog_digest` | No | Dev/airgap catalog | +| `--ibm-entitlement-key` | `ibm_entitlement_key` | Yes | Image pull secret | +| `--mas-instance-id` | `mas_instance_id` | Yes | MAS instance ID (triggers non-interactive) | +| `--mcpi-channel` | `mcpi_channel` | Yes (in non-interactive) | Subscription channel; acts as sentinel guard in pipeline | +| `--routing-mode` | `routing_mode` | No | `subdomain` or `path`; default `subdomain` | +| `--manual-route-mgmt` | `manual_route_mgmt` | No | Default `false` | +| `--license-file` | `license_file` | Yes (non-interactive) | SLS license file path | +| `--sls-namespace` | `sls_namespace` | No | Default `ibm-sls` | +| `--dedicated-sls` | `dedicated_sls` | No | Flag — set SLS ns to `mas--sls` | +| `--contact-email` | `dro_contact_email` | Yes (non-interactive) | DRO contact | +| `--contact-firstname` | `dro_contact_firstname` | Yes (non-interactive) | DRO contact | +| `--contact-lastname` | `dro_contact_lastname` | Yes (non-interactive) | DRO contact | +| `--dro-namespace` | `dro_namespace` | No | DRO namespace | +| `--mongodb-namespace` | `mongodb_namespace` | No | BYO MongoDB namespace | +| `--storage-class-rwo` | `storage_class_rwo` | Yes | Pipeline storage | +| `--storage-class-rwx` | `storage_class_rwx` | Yes | Pipeline storage | +| `--storage-pipeline` | `storage_pipeline` | Yes (non-interactive) | Pipeline PVC class | +| `--storage-accessmode` | `storage_accessmode` | Yes (non-interactive) | ReadWriteMany / ReadWriteOnce | +| `--additional-configs` | `additional_configs` | No | Extra config dir | +| `--artifactory-username` | `artifactory_username` | No | Dev mode only | +| `--artifactory-token` | `artifactory_token` | No | Dev mode only | +| `--image-pull-policy` | `image_pull_policy` | No | Tekton image pull policy | +| `--service-account` | `service_account_name` | No | Custom SA | +| `--slack-token` | `slack_token` | No | Slack integration | +| `--slack-channel` | `slack_channel` | No | Slack integration | +| `--advanced` | `advanced` | No | Show advanced prompts in interactive mode | +| `--simplified` | `simplified` | No | Hide advanced prompts in interactive mode | +| `--accept-license` | `accept_license` | No | Pre-accept license | +| `--dev-mode` | `dev_mode` | No | Dev mode | +| `--skip-pre-check` | `skip_pre_check` | No | Skip pre-install healthcheck | +| `--no-confirm` | `no_confirm` | No | Skip confirmation prompt | +| `-h, --help` | help | No | | + +> **Reason for SLS/DRO params**: Even though MCPI has no per-app entitlement, the pipeline still requires SLS and DRO to be installed as MAS core dependencies. This follows the same pattern as `aiservice-install`. + +> **`routing_mode` and `manual_route_mgmt`**: MCPI-specific params beyond the standard MAS set. These map directly to `ROUTING_MODE` and `MANUAL_ROUTE_MGMT` env vars consumed by the `mcpicfg` Ansible role. + +### requiredParams and optionalParams + +**`requiredParams`:** +```python +[ + "mas_catalog_version", + "mas_instance_id", + "storage_class_rwo", + "storage_class_rwx", + "ibm_entitlement_key", + "mcpi_channel", + "dro_contact_email", + "dro_contact_firstname", + "dro_contact_lastname", +] +``` + +**`optionalParams`:** +```python +[ + "image_pull_policy", + "service_account_name", + "mas_catalog_digest", + "sls_namespace", + "dro_namespace", + "mongodb_namespace", + "routing_mode", + "manual_route_mgmt", + "artifactory_username", + "artifactory_token", + "slack_token", + "slack_channel", +] +``` + +### Pipelines Namespace + +Follows the AI Service pattern exactly: dedicated namespace `mcpi-{mas_instance_id}-pipelines`. + +**A new `prepareMcpiPipelinesNamespace()` function is needed in `python-devops`** (analogous to `prepareAiServicePipelinesNamespace()` at line 731). It requires: +- New template: `mcpi-pipelines-rbac.yml.j2` (clone of `aiservice-pipelines-rbac.yml.j2`, replace `aiservice` with `mcpi`) +- New template: `mcpi-pipelines-pvc.yml.j2` (clone of `aiservice-pipelines-pvc.yml.j2`, replace `aiservice` with `mcpi`) +- Same signature: `prepareMcpiPipelinesNamespace(dynClient, instanceId, storageClass, accessMode, waitForBind=True, configureRBAC=True)` + +### `launchInstallPipeline()` — Confirmed Blocker + +`launchInstallPipeline()` in `python-devops/src/mas/devops/tekton.py` (line 1285) has two hardcoded branches: +- If `mas_instance_id` in params → launches `mas-install` pipeline, namespace `mas-{id}-pipelines` +- Otherwise → launches AI Service install, namespace `aiservice-{id}-pipelines` + +**MCPI needs a new function `launchMcpiInstallPipeline()` in `python-devops`** plus a new Jinja2 template `pipelinerun-mcpi-install.yml.j2`. This is a **Phase 0 prerequisite** that must be done in the `python-devops` repo before Phase 1 of this CLI plan. + +The new function uses `launchPipelineRun()` (generic, line 1247) with: +- `namespace = f"mcpi-{params['mas_instance_id']}-pipelines"` +- `templateName = "pipelinerun-mcpi-install"` +- PipelineRun URL: `f"{getConsoleURL(dynClient)}/k8s/ns/mcpi-{instanceId}-pipelines/tekton.dev~v1beta1~PipelineRun/{instanceId}-install-{timestamp}"` + +The `pipelinerun-mcpi-install.yml.j2` template (in `src/mas/devops/templates/`) includes only MCPI-relevant params: `ibm_entitlement_key`, `mas_instance_id`, `mas_catalog_version`, `mcpi_channel`, `routing_mode`, `manual_route_mgmt`, plus SLS/DRO params and storage class params (trim `pipelinerun-install.yml.j2` to remove Db2/Kafka/CP4D/MAS app sections). + +### Class Hierarchy + +```python +class McpiInstallApp( + BaseApp, + McpiInstallArgBuilderMixin, # from argBuilder.py + McpiInstallSummarizerMixin, # from summarizer.py + InstallSettingsMixin, # reused from install/settings.py + ConfigGeneratorMixin # reused from gencfg.py +) +``` + +### `mcpicfg` Role in `ansible-devops` — Confirmed Blocker + +`run-role.sh mcpicfg` inside the Tekton task calls `ansible-playbook ibm.mas_devops.run_role` which dynamically loads the `ibm.mas_devops.mcpicfg` role. The `mcpicfg` role currently exists **only in the `ibm-mas` operator repo** at `operator/ibm-mas-cfg-mcpi/roles/mcpicfg/`. It is **not present** in `ansible-devops/ibm/mas_devops/roles/`. + +**The `mcpicfg` role must be added to `ansible-devops`** (Phase 0 prerequisite) so it gets bundled into the CLI Docker image. The role must be placed at `ansible-devops/ibm/mas_devops/roles/mcpicfg/` with the same structure as `operator/ibm-mas-cfg-mcpi/roles/mcpicfg/` (tasks/, defaults/, vars/, templates/). + +### Interactive Mode — Trigger + +Presence of `--mas-instance-id` triggers **non-interactive** mode (omitting it triggers interactive mode prompts). This follows the same sentinel pattern as `aiservice-install` where the presence of `--aiservice-instance-id` (mapped to `args.aiservice_instance_id`) determines mode. + +### Tekton Task Env Vars + +The `mcpicfg` role reads these env vars: + +| Env Var | Role Variable | Default | +|---------|--------------|---------| +| `IBM_ENTITLEMENT_KEY` | image pull secret | — | +| `MAS_INSTANCE_ID` | pipeline context | — | +| `MCPI_CHANNEL` | subscription channel | — | +| `ROUTING_MODE` | `routingMode` | `subdomain` | +| `MANUAL_ROUTE_MGMT` | `manualRouteMgmt` | `false` | + +### No gencfg Step + +MCPI has no post-install config generation. The McpiCfg CR is created and managed by the operator itself. No `gencfg-mcpi` task is needed. + +### Standalone vs. `mas-install` Pipeline + +MCPI gets both: +1. A **standalone `mcpi-install` pipeline** for independent MCPI installs +2. The **`mas-install` pipeline** is modified to include MCPI as an optional task guarded by `when: mcpi_channel != ""` + +--- + +## Critical Rules + +- Track progress in this plan file only, not in chat todo lists +- Follow existing code style exactly — look at `aiservice/install/` files as the canonical reference for every structural decision +- Copyright header `# Copyright (c) 2026 IBM Corporation` required in all new Python files +- All new Python files need Google-style docstrings +- Preserve all existing tests — add new tests, do not modify existing ones +- Validate at end of each phase: run `black`, `flake8`, and `pytest` before marking a phase done +- **Phase 0 (`ansible-devops` + `python-devops`) must be complete before Phase 1 begins** — the CLI cannot be completed without `launchMcpiInstallPipeline()` and the `mcpicfg` role in the collection + +--- + +## Execution Plan + +### Phase 0 — Cross-Repo Prerequisites +[x] **Status:** complete + +**Intent:** Two changes in external repos are required before the CLI code can be completed. These must be opened as PRs in their respective repos and merged (or at least available as dev branches) before Phase 1 can be validated end-to-end. + +**Expected Outcomes:** +- `ansible-devops` has a `mcpicfg` role under `ibm/mas_devops/roles/mcpicfg/` +- `python-devops` exposes `launchMcpiInstallPipeline()`, `prepareMcpiPipelinesNamespace()`, `pipelinerun-mcpi-install.yml.j2`, `mcpi-pipelines-rbac.yml.j2`, `mcpi-pipelines-pvc.yml.j2` + +**Todo:** + +- [x] **0.1** In the **`ansible-devops`** repo: Copy the `mcpicfg` role from `ibm-mas`: + - Source: `/Users/tahamadou/Source/MAS/ibm-mas/operator/ibm-mas-cfg-mcpi/roles/mcpicfg/` + - Destination: `/Users/tahamadou/Source/MAS/ansible-devops/ibm/mas_devops/roles/mcpicfg/` + - Copy all subdirectories: `tasks/`, `defaults/`, `vars/`, `templates/` + - Verify the role loads: `ansible-galaxy collection build` should succeed + +- [x] **0.2** In **`python-devops`** (`/Users/tahamadou/Source/MAS/python-devops/`): Add MCPI namespace templates: + - `src/mas/devops/templates/mcpi-pipelines-rbac.yml.j2` — clone of `aiservice-pipelines-rbac.yml.j2` with `aiservice` → `mcpi` + - `src/mas/devops/templates/mcpi-pipelines-pvc.yml.j2` — clone of `aiservice-pipelines-pvc.yml.j2` with `aiservice` → `mcpi` + +- [x] **0.3** In **`python-devops`**: Add `prepareMcpiPipelinesNamespace()` to `src/mas/devops/tekton.py`: + - After `prepareAiServicePipelinesNamespace()` (line 731) + - Same signature: `prepareMcpiPipelinesNamespace(dynClient, instanceId, storageClass, accessMode, waitForBind=True, configureRBAC=True)` + - Uses `namespace = f"mcpi-{instanceId}-pipelines"` + - Uses templates `mcpi-pipelines-rbac.yml.j2` and `mcpi-pipelines-pvc.yml.j2` + - Renders with `mcpi_instance_id=instanceId` (not `aiservice_instance_id`) + +- [x] **0.4** In **`python-devops`**: Add `pipelinerun-mcpi-install.yml.j2` to `src/mas/devops/templates/`: + - Based on `pipelinerun-install.yml.j2` trimmed to MCPI scope: + - `metadata.name: "{{mas_instance_id}}-install-{{ timestamp }}"` + - `labels.tekton.dev/pipeline: mcpi-install` + - `spec.pipelineRef.name: mcpi-install` + - Params: `ibm_entitlement_key`, `storage_class_rwx`, `storage_class_rwo`, SLS params, DRO params, `mas_catalog_version` (+conditional `mas_catalog_digest`), `mas_instance_id`, `mcpi_channel`, conditional `routing_mode`, conditional `manual_route_mgmt`, conditional `artifactory_username/token`, conditional `image_pull_policy`, conditional `skip_pre_check` + - Workspaces: `shared-configs`, `shared-entitlement`, `shared-pod-templates`, `shared-additional-configs`, `shared-certificates` (same as `pipelinerun-install.yml.j2`, no `shared-db2` or `shared-aiservice-config`) + +- [x] **0.5** In **`python-devops`**: Add `launchMcpiInstallPipeline()` to `src/mas/devops/tekton.py`: + - After `launchInstallPipeline()` (line 1285) + - Signature: `launchMcpiInstallPipeline(dynClient: DynamicClient, params: dict) -> str` + - Body: + ```python + instanceId = params["mas_instance_id"] + namespace = f"mcpi-{instanceId}-pipelines" + timestamp = launchPipelineRun(dynClient, namespace, "pipelinerun-mcpi-install", params) + return f"{getConsoleURL(dynClient)}/k8s/ns/mcpi-{instanceId}-pipelines/tekton.dev~v1beta1~PipelineRun/{instanceId}-install-{timestamp}" + ``` + +**Relevant Context:** +- `launchInstallPipeline` source: `python-devops/src/mas/devops/tekton.py:1285` +- `prepareAiServicePipelinesNamespace` source: `python-devops/src/mas/devops/tekton.py:731` +- `launchPipelineRun` generic: `python-devops/src/mas/devops/tekton.py:1247` +- `pipelinerun-install.yml.j2` (trim for mcpi): `python-devops/src/mas/devops/templates/pipelinerun-install.yml.j2` +- `aiservice-pipelines-rbac.yml.j2` (clone): `python-devops/src/mas/devops/templates/aiservice-pipelines-rbac.yml.j2` +- `aiservice-pipelines-pvc.yml.j2` (clone): `python-devops/src/mas/devops/templates/aiservice-pipelines-pvc.yml.j2` +- `mcpicfg` role source: `/Users/tahamadou/Source/MAS/ibm-mas/operator/ibm-mas-cfg-mcpi/roles/mcpicfg/` +- `ansible-devops` roles dir: `/Users/tahamadou/Source/MAS/ansible-devops/ibm/mas_devops/roles/` + +--- + +### Phase 1 — Python CLI Module +[x] **Status:** complete + +**Intent:** Create the `python/src/mas/cli/mcpi/install/` module with all 6 files and wire it into the dispatcher. This is the UI layer that collects user input, validates it, builds the pipeline command, and launches the Tekton pipeline. + +**Expected Outcomes:** +- `mas-cli mcpi-install --help` works and shows all flags +- Non-interactive mode runs end-to-end with mocked Tekton calls +- All files are lint-clean and tested + +**Todo:** + +- [ ] **1.1** Create `python/src/mas/cli/mcpi/__init__.py`: + ```python + # Copyright header (2026) + # Empty — package marker + ``` + +- [ ] **1.2** Create `python/src/mas/cli/mcpi/install/__init__.py`: + - Mirror `python/src/mas/cli/aiservice/install/__init__.py` exactly: + ```python + from ...cli import BaseApp # noqa: F401 + ``` + +- [ ] **1.4** Create `python/src/mas/cli/mcpi/install/params.py`: + - `requiredParams` and `optionalParams` as defined in Design Decisions above + +- [ ] **1.5** Create `python/src/mas/cli/mcpi/install/argParser.py`: + - Module-level `argparse.ArgumentParser` instance named `mcpiInstallArgParser` + - Program: `"mas mcpi-install"` + - Argument groups (in order): + 1. `MAS Catalog Selection & Entitlement` — `--mas-catalog-version`, `--mas-catalog-digest`, `--ibm-entitlement-key` + 2. `MCPI Basic Configuration` — `-i, --mas-instance-id` + 3. `MCPI Advanced Configuration` — `--routing-mode`, `--manual-route-mgmt` + 4. `MAS Advanced Configuration` — `--additional-configs` + 5. `Storage` — `--storage-class-rwo`, `--storage-class-rwx`, `--storage-pipeline`, `--storage-accessmode` + 6. `IBM Suite License Service` — `--license-file`, `--sls-namespace`, `--dedicated-sls` + 7. `IBM Data Reporting Operator (DRO)` — `--contact-email`, `--contact-firstname`, `--contact-lastname`, `--dro-namespace` + 8. `MongoDb Community Operator` — `--mongodb-namespace` + 9. `MAS Applications` — `--mcpi-channel` + 10. `Development Mode` — `--artifactory-username`, `--artifactory-token` + 11. `More` — `--advanced`, `--simplified`, `--accept-license`, `--dev-mode`, `--skip-pre-check`, `--no-confirm`, `--image-pull-policy`, `--service-account`, `--slack-token`, `--slack-channel`, `-h/--help` + - `isValidFile()` helper for `--license-file` validation (mirror aiservice pattern) + +- [ ] **1.6** Create `python/src/mas/cli/mcpi/install/argBuilder.py`: + - Class `McpiInstallArgBuilderMixin` with `buildCommand(self) -> str` + - Build a re-runnable shell command starting with `export IBM_ENTITLEMENT_KEY=x` + - Command: `mas mcpi-install --mas-catalog-version ... --ibm-entitlement-key $IBM_ENTITLEMENT_KEY ...` + - Include all set params in the same style as `aiServiceInstallArgBuilderMixin.buildCommand()` + - End with `--accept-license --no-confirm` + +- [ ] **1.7** Create `python/src/mas/cli/mcpi/install/summarizer.py`: + - Class `McpiInstallSummarizerMixin` + - Methods: + - `ocpSummary()` — pipeline config, OCP architecture, storage classes + - `mcpiSummary()` — catalog version, MAS instance ID, MCPI channel, routing mode, manual route mgmt + - `droSummary()` — DRO email, first/last name, namespace + - `slsSummary()` — SLS namespace, license file + - `mongoSummary()` — MongoDB namespace (BYO vs. install) + - `slackSummary()` — Slack channel + - `displayInstallSummary()` — orchestrates all summaries; logs `yaml.dump(self.params)` at DEBUG; calls `getConsoleURL(self.dynamicClient)` for connected cluster line + +- [ ] **1.8** Create `python/src/mas/cli/mcpi/install/app.py`: + - Class `McpiInstallApp(BaseApp, McpiInstallArgBuilderMixin, McpiInstallSummarizerMixin, InstallSettingsMixin, ConfigGeneratorMixin)` + - Import `mcpiInstallArgParser` from `.argParser` + - Import `requiredParams, optionalParams` from `.params` + - Import `McpiInstallArgBuilderMixin` from `.argBuilder` + - Import `McpiInstallSummarizerMixin` from `.summarizer` + - Import from `mas.devops.tekton`: `installOpenShiftPipelines`, `updateTektonDefinitions`, `prepareMcpiPipelinesNamespace`, `prepareInstallSecrets`, `testCLI`, `launchMcpiInstallPipeline` (added in Phase 0) + - `logMethodCall` decorator (mirror aiservice pattern) + - Key methods: + - `configMcpi(self)` — prompts for MAS instance ID (using `InstanceIDFormatValidator`), MCPI channel, routing mode; mirrors `configAibroker()` from aiservice + - `interactiveMode(self, simplified: bool, advanced: bool) -> None` — catalog, storage, SLS, DRO, ICR credentials, cert manager, MCPI config + - `nonInteractiveMode(self) -> None` — iterates over `vars(self.args)`, handles `requiredParams`, `optionalParams`, `license_file`, `dedicated_sls`, `storage_pipeline`, `storage_accessmode`, `mongodb_namespace`, `additional_configs`, `mcpi_channel`, `approval_*`, and ignore-list keys (`accept_license`, `dev_mode`, etc.) + - `install(self, argv: list) -> int` — main entry: parses args, detects interactive vs non-interactive via `args.mas_instance_id`, connects, runs mode, calls `slsLicenseFile()`, calls `buildCommand()` + `displayInstallSummary()`, confirms, prepares namespace and launches pipeline + - Pipelines namespace: `mcpi-{self.getParam('mas_instance_id')}-pipelines` + - Namespace prep: `prepareMcpiPipelinesNamespace(dynClient=..., instanceId=self.getParam('mas_instance_id'), ...)` + +- [ ] **1.9** Modify `python/src/mas/cli/__main__.py`: + - After line 76 (the `aiservice-install` block), add: + ```python + if function == "mcpi-install": + from mas.cli.mcpi.install.app import McpiInstallApp + + app = McpiInstallApp() + raise SystemExit(app.install(argv[2:])) + ``` + - In `usage()`, add to the MAS Management Actions string (after the `aiservice-install` line if it exists, otherwise after the last install-like command): + ```python + + " - mas-cli mcpi-install Install Maximo Cluster Performance Insights\n" + ``` + +- [ ] **1.10** Write integration tests in `python/tests/integration/mcpi_install/`: + - Create `__init__.py` (empty with copyright header) + - Create `conftest.py` — mirrors `aiservice_install/conftest.py`, auto-applies `mcpi-install` marker + - Create `test_mcpi_install_app.py`: + - `test_install_noninteractive(tmpdir)` — full non-interactive happy path: + - Mocks: `DynamicClient`, `getNodes`, `isAirgapInstall`, `getCurrentCatalog`, `installOpenShiftPipelines`, `updateTektonDefinitions`, `prepareAiServicePipelinesNamespace`, `launchMcpiInstallPipeline`, `prepareInstallSecrets`, `testCLI` + - Passes all required flags: `--mas-catalog-version`, `--ibm-entitlement-key`, `--mas-instance-id`, `--storage-class-rwo`, `--storage-class-rwx`, `--storage-pipeline`, `--storage-accessmode`, `--license-file`, `--contact-email`, `--contact-firstname`, `--contact-lastname`, `--mcpi-channel`, `--accept-license`, `--no-confirm` + - Asserts `launchInstallPipeline` (or equivalent) was called once + +- [ ] **1.11** Validate Phase 1: + ``` + black python/src/mas/cli/mcpi/ python/tests/integration/mcpi_install/ --line-length 160 --check + flake8 python/src/mas/cli/mcpi/ python/tests/integration/mcpi_install/ --max-line-length 160 + .venv/bin/pytest python/tests/integration/mcpi_install/ -v + ``` + +**Relevant Context:** +- Reference app: [`python/src/mas/cli/aiservice/install/app.py`](python/src/mas/cli/aiservice/install/app.py:70) +- Reference argParser: [`python/src/mas/cli/aiservice/install/argParser.py`](python/src/mas/cli/aiservice/install/argParser.py:25) +- Reference argBuilder: [`python/src/mas/cli/aiservice/install/argBuilder.py`](python/src/mas/cli/aiservice/install/argBuilder.py:16) +- Reference summarizer: [`python/src/mas/cli/aiservice/install/summarizer.py`](python/src/mas/cli/aiservice/install/summarizer.py:18) +- Reference params: [`python/src/mas/cli/aiservice/install/params.py`](python/src/mas/cli/aiservice/install/params.py) +- Reference __init__: [`python/src/mas/cli/aiservice/install/__init__.py`](python/src/mas/cli/aiservice/install/__init__.py) +- Dispatcher: [`python/src/mas/cli/__main__.py`](python/src/mas/cli/__main__.py:72) +- Test pattern: [`python/tests/integration/aiservice_install/test_app.py`](python/tests/integration/aiservice_install/test_app.py:26) +- Test conftest: [`python/tests/integration/aiservice_install/conftest.py`](python/tests/integration/aiservice_install/conftest.py) +- `prepareAiServicePipelinesNamespace` call: [`python/src/mas/cli/aiservice/install/app.py`](python/src/mas/cli/aiservice/install/app.py:579) + +--- + +### Phase 2 — Tekton Definitions +[x] **Status:** complete + +**Intent:** Create all Tekton Jinja2 template sources needed to deploy and run the MCPI install pipeline. + +**Expected Outcomes:** +- `ansible-playbook tekton/generate-tekton-tasks.yml` produces `target/tasks/mcpi.yaml` +- `ansible-playbook tekton/generate-tekton-pipelines.yml` produces `target/pipelines/mcpi-install.yaml` +- `target/pipelines/mas-install.yaml` includes the MCPI task, guarded by `when: mcpi_channel != ""` +- All generated YAML is valid Tekton syntax + +**Todo:** + +- [ ] **2.1** Create `tekton/src/tasks/mcpi/mcpi.yml.j2`: + - `apiVersion: tekton.dev/v1`, `kind: Task`, `metadata.name: mas-devops-mcpi` + - `spec.params`: + - `{{ lookup('template', task_src_dir ~ '/common/cli-params.yml.j2') | indent(4) }}` + - `artifactory_username`, `artifactory_token` (pre-release, empty defaults) + - `ibm_entitlement_key` (type: string) + - `mas_instance_id` (type: string) + - `mcpi_channel` (type: string, description: "Catalog channel for the MCPI operator subscription") + - `routing_mode` (type: string, default: "", description: "Routing mode for MCPI: subdomain or path") + - `manual_route_mgmt` (type: string, default: "", description: "Disable automatic route management") + - `mas_icr_cp` (type: string, default: "") + - `mas_icr_cpopen` (type: string, default: "") + - `custom_labels` (type: string, default: "", description: "Optional MAS custom labels, comma separated list of key=value pairs") + - `spec.stepTemplate.env`: + - `{{ lookup('template', task_src_dir ~ '/common/cli-env.yml.j2') | indent(4) }}` + - `IBM_ENTITLEMENT_KEY: $(params.ibm_entitlement_key)` + - `MAS_INSTANCE_ID: $(params.mas_instance_id)` + - `MCPI_CHANNEL: $(params.mcpi_channel)` + - `ROUTING_MODE: $(params.routing_mode)` + - `MANUAL_ROUTE_MGMT: $(params.manual_route_mgmt)` + - `MAS_ICR_CP: $(params.mas_icr_cp)` + - `MAS_ICR_CPOPEN: $(params.mas_icr_cpopen)` + - `CUSTOM_LABELS: $(params.custom_labels)` + - `ARTIFACTORY_USERNAME: $(params.artifactory_username)` + - `ARTIFACTORY_TOKEN: $(params.artifactory_token)` + - `spec.steps` — single step named `mcpi`: + - `image: quay.io/ibmmas/cli:latest` + - `command: ["/opt/app-root/src/run-role.sh", "mcpicfg"]` + - `workingDir: /workspace/configs` + - `spec.workspaces`: `- name: configs` (optional: true) + +- [ ] **2.2** Create `tekton/src/pipelines/taskdefs/mcpi/mcpi.yml.j2`: + - Task fragment used in both `mas-install.yml.j2` and `mcpi-install.yml.j2` + - `- name: mcpi`, `timeout: "0"` + - `params`: + - `{{ lookup('template', 'taskdefs/common/cli-params.yml.j2') | indent(4) }}` + - `- name: devops_suite_name`, `value: mcpi` + - `- name: artifactory_username`, `value: $(params.artifactory_username)` + - `- name: artifactory_token`, `value: $(params.artifactory_token)` + - `- name: ibm_entitlement_key`, `value: $(params.ibm_entitlement_key)` + - `- name: custom_labels`, `value: $(params.custom_labels)` + - `- name: mas_instance_id`, `value: $(params.mas_instance_id)` + - `- name: mcpi_channel`, `value: "$(params.mcpi_channel)"` + - `- name: routing_mode`, `value: $(params.routing_mode)` + - `- name: manual_route_mgmt`, `value: $(params.manual_route_mgmt)` + - `taskRef`: `name: mas-devops-mcpi`, `kind: Task` + - `when`: `input: "$(params.mcpi_channel)"`, `operator: notin`, `values: [""]` + - `workspaces`: `- name: configs`, `workspace: shared-configs` + +- [ ] **2.3** Create `tekton/src/params/install-mcpi.yml.j2`: + ```yaml + # MAS Add-on Configuration - Maximo Cluster Performance Insights (MCPI) + # ----------------------------------------------------------------------------- + - name: mcpi_channel + type: string + description: Default channel for IBM Maximo Cluster Performance Insights operator subscription + default: "" + - name: routing_mode + type: string + description: "Routing mode for MCPI: subdomain or path" + default: "" + - name: manual_route_mgmt + type: string + description: Disable automatic route management for MCPI + default: "" + ``` + +- [ ] **2.4** Modify `tekton/src/params/install.yml.j2`: + - After line 844 (the end of the AI Service section: `{{ lookup('template', params_src_dir ~ '/install-aiservice.yml.j2') }}`), add: + ```yaml + + # MCPI Configuration + # ----------------------------------------------------------------------------- + {{ lookup('template', params_src_dir ~ '/install-mcpi.yml.j2') }} + ``` + +- [ ] **2.5** Create `tekton/src/pipelines/mcpi-install.yml.j2`: + - Standalone install pipeline for MCPI-only installs + - Mirror the structure of `aiservice-upgrade.yml.j2` adapted for a fresh install + - `metadata.name: mcpi-install` + - Workspaces: `shared-configs`, `shared-pod-templates` + - Params: + - `{{ lookup('template', params_src_dir ~ '/common.yml.j2') | indent(4) }}` + - `- name: mas_instance_id` (type: string) + - `- name: ibm_entitlement_key` (type: string) + - `{{ lookup('template', params_src_dir ~ '/install-mcpi.yml.j2') | indent(4) }}` + - Tasks: + 1. `pipeline-start` — `mas-devops-update-pipeline-status` (started) + 2. `ibm-catalogs` — runs after `pipeline-start` + 3. `cert-manager` — runs after `ibm-catalogs` + 4. `sls` — runs after `cert-manager` + 5. `dro` — runs after `sls` + 6. `mcpi` (via `{{ lookup('template', pipeline_src_dir ~ '/taskdefs/mcpi/mcpi.yml.j2') | indent(4) }}`) — runs after `sls`, `dro` + 7. `post-install-verify` (via `ocp-verify.yml.j2` taskdef) — runs after `mcpi` + - `finally`: `sync-install` (`mas-devops-update-configmap`) + `pipeline-finish` (`mas-devops-update-pipeline-status`) + +- [ ] **2.6** Modify `tekton/src/pipelines/mas-install.yml.j2`: + - After the aiservice section (after the `gencfg-aiservice` block, which ends around line 464), add a numbered section: + ``` + # 15. Install and configure MCPI (Maximo Cluster Performance Insights) + # ------------------------------------------------------------------------- + {{ lookup('template', pipeline_src_dir ~ '/taskdefs/mcpi/mcpi.yml.j2') | indent(4) }} + runAfter: + - suite-config + ``` + - Add `- mcpi` to the `post-install-verify` task's `runAfter` list (currently ends at line 479 with `- aiservice`) + +- [ ] **2.7** Modify `tekton/generate-tekton-tasks.yml`: + - Add a new block after the AI Service tasks section (after line 57): + ```yaml + # 3b. Generate Tasks (MCPI) + # ------------------------------------------------------------------------- + - name: Generate Tasks (MCPI) + ansible.builtin.template: + src: "{{ task_src_dir }}/mcpi/{{ item }}.yml.j2" + dest: "{{ task_target_dir }}/{{ item }}.yaml" + with_items: + - mcpi + ``` + +- [ ] **2.8** Modify `tekton/generate-tekton-pipelines.yml`: + - Add after `# AI Service Pipelines` block (after line 47): + ```yaml + # MCPI Pipelines + - mcpi-install + ``` + +- [ ] **2.9** Validate Phase 2: + ```bash + cd tekton + ansible-playbook generate-tekton-tasks.yml + ansible-playbook generate-tekton-pipelines.yml + ``` + - Confirm `target/tasks/mcpi.yaml` generated and syntactically valid YAML + - Confirm `target/pipelines/mcpi-install.yaml` generated + - Confirm `target/pipelines/mas-install.yaml` contains the `mcpi` task block + +**Relevant Context:** +- Task template reference: [`tekton/src/tasks/aiservice/aiservice.yml.j2`](tekton/src/tasks/aiservice/aiservice.yml.j2) +- Pipeline fragment reference: [`tekton/src/pipelines/taskdefs/aiservice/aiservice.yml.j2`](tekton/src/pipelines/taskdefs/aiservice/aiservice.yml.j2) +- Params reference: [`tekton/src/params/install-aiservice.yml.j2`](tekton/src/params/install-aiservice.yml.j2) +- Standalone pipeline reference: [`tekton/src/pipelines/aiservice-upgrade.yml.j2`](tekton/src/pipelines/aiservice-upgrade.yml.j2) +- Install pipeline: [`tekton/src/pipelines/mas-install.yml.j2`](tekton/src/pipelines/mas-install.yml.j2:436) +- Params file: [`tekton/src/params/install.yml.j2`](tekton/src/params/install.yml.j2:842) +- Generate tasks: [`tekton/generate-tekton-tasks.yml`](tekton/generate-tekton-tasks.yml:45) +- Generate pipelines: [`tekton/generate-tekton-pipelines.yml`](tekton/generate-tekton-pipelines.yml:46) + +--- + +## Final Validation + +After both phases are complete: + +1. **Lint & format:** + ``` + black python/src/mas/cli/mcpi/ python/tests/integration/mcpi_install/ --line-length 160 --check + flake8 python/src/mas/cli/mcpi/ python/tests/integration/mcpi_install/ --max-line-length 160 + ``` + +2. **Tests (no regressions):** + ``` + .venv/bin/pytest python/tests/integration/mcpi_install/ -v + .venv/bin/pytest python/tests/ -v + ``` + +3. **CLI smoke test:** + ``` + mas-cli mcpi-install --help + ``` + Confirm all flags display correctly. + +4. **Tekton generation:** + ``` + cd tekton + ansible-playbook generate-tekton-tasks.yml + ansible-playbook generate-tekton-pipelines.yml + ``` + Confirm `target/tasks/mcpi.yaml`, `target/pipelines/mcpi-install.yaml` are generated and `target/pipelines/mas-install.yaml` includes the MCPI task. + +**Success Criteria:** +- `mas-cli mcpi-install` appears in usage output +- All integration tests pass +- Tekton YAML contains `mas-devops-mcpi` task and `mcpi-install` pipeline +- MCPI task appears in `mas-install` pipeline guarded by `when: mcpi_channel != ""` +- No regressions in existing test suite + +--- + +## Testing on a New Cluster — Checklist + +### Step 1 — Login + +```bash +oc login --web --server=https://:6443 +oc whoami --show-server # confirm correct cluster +``` + +### Step 2 — Discover cluster values + +```bash +# MAS instance ID +oc get suite -A + +# Storage classes +oc get storageclass + +# Existing SLS namespace +oc get namespace | grep sls + +# MongoDB namespace +oc get mongodbcommunity -A + +# Entitlement key +oc get secret ibm-entitlement -n mas--core \ + -o jsonpath='{.data.\.dockerconfigjson}' | base64 -d | \ + jq -r '.auths["cp.icr.io"].password' + +# Dev CLI image (from existing FVT pipeline pods) +oc get pod -n mas--pipelines -l tekton.dev/pipelineTask=suite-install \ + -o jsonpath='{.items[0].spec.containers[0].image}' +``` + +### Step 3 — Set up CLI from source + +```bash +cd /Users/tahamadou/Source/MAS/public-ibm-mas/cli + +# Install as editable — required so ibm-mas-tekton.yaml is read from source tree +uv tool uninstall mas-cli 2>/dev/null +uv tool install --editable . --with-editable ../python-devops +``` + +> **Do NOT use `uvx --from .`** — it builds a wheel that excludes `ibm-mas-tekton.yaml` (a generated file not in git). + +### Step 4 — Generate the Tekton template (macOS only) + +`build/bin/build-tekton.sh` uses `sed -i` which fails on macOS. Use this Python script instead (run from repo root after the Ansible playbooks have generated `tekton/target/`): + +```bash +python3 - <<'EOF' +import os, glob + +# Replace with the dev image digest from Step 2 +IMAGE = "quay.io/ibmmas/cli@sha256:" +OUT = "python/src/mas/cli/templates/ibm-mas-tekton.yaml" +skip = ("fvt-", "ivt-", "aiservice-fvt", "aiservice-launchfvt") +tasks = sorted(glob.glob("tekton/target/tasks/*.yaml")) +pipelines = sorted(glob.glob("tekton/target/pipelines/*.yaml")) + +with open(OUT, "w") as out: + for f in tasks + pipelines: + name = os.path.basename(f) + if any(name.startswith(s) or s in name for s in skip): + continue + content = open(f).read().replace("quay.io/ibmmas/cli:latest", IMAGE) + out.write(content + "\n") +print(f"Done - {sum(1 for _ in open(OUT))} lines") +EOF +``` + +### Step 5 — Copy `mas-devops` secret (dev clusters only) + +Dev FVT clusters have a `mas-devops` secret in `mas--pipelines` that the `pipeline-start` task needs to generate the MongoDB config file. Copy it to the MCPI namespace before the first run: + +```bash +oc get secret mas-devops -n mas--pipelines -o yaml | \ + sed "s/namespace: mas--pipelines/namespace: mcpi--pipelines/" | \ + oc apply -f - +``` + +> This only needs to be done once. The namespace is created by the CLI during the first run, so run the install command first, wait for the namespace to appear, copy the secret, then re-run. + +### Step 6 — Run the install + +```bash +mas-cli mcpi-install \ + --mas-catalog-version v9-250828-amd64 \ + --ibm-entitlement-key "" \ + --mas-instance-id \ + --storage-class-rwo \ + --storage-class-rwx \ + --storage-pipeline \ + --storage-accessmode ReadWriteMany \ + --sls-namespace sls- \ + --sls-action none \ + --mongodb-namespace \ + --contact-email \ + --contact-firstname \ + --contact-lastname \ + --mcpi-channel v9.2 \ + --image-pull-policy IfNotPresent \ + --accept-license \ + --no-confirm +``` + +### Step 7 — Monitor + +```bash +# Watch pipeline progress +oc get pipelinerun -n mcpi--pipelines -w + +# Check task statuses +oc get taskrun -n mcpi--pipelines \ + -o custom-columns='TASK:.metadata.labels.tekton\.dev/pipelineTask,STATUS:.status.conditions[0].reason' \ + --sort-by='.metadata.creationTimestamp' + +# Tail a specific task's logs +oc logs -n mcpi--pipelines -l tekton.dev/pipelineTask= --tail=50 +``` + +--- + +## Testing Session Notes (2025-01-31 / Cluster: pfvt-thierno-bah1-261006-18) + +### Cluster Values Used + +| Resource | Value | +|---|---| +| API URL | `https://api.pfvt-thierno-bah1-261006-18.cp.fyre.ibm.com:6443` | +| Console URL | `https://console-openshift-console.apps.pfvt-thierno-bah1-261006-18.cp.fyre.ibm.com` | +| MAS Instance ID | `thierno` | +| MAS Core Namespace | `mas-thierno-core` | +| MAS Pipelines Namespace | `mas-thierno-pipelines` | +| MCPI Pipelines Namespace | `mcpi-thierno-pipelines` | +| SLS Namespace | `sls-thierno` | +| MongoDB Namespace | `mongoce` | +| Storage Class (RWO + RWX) | `nfs-client` | +| Entitlement Secret | `ibm-entitlement` in `mas-thierno-core` (Artifactory JWT, not standard ICR key) | +| Dev CLI Image Digest | `quay.io/ibmmas/cli@sha256:d2591b6d413e08efbd7185d7ca574ffd82cb7409b92bcde3da11f87f19c6e29b` | + +### Pipeline Task Results (latest run) + +| Task | Status | Notes | +|---|---|---| +| `pipeline-start` | ✅ Succeeded | | +| `ibm-catalogs` | ✅ Succeeded | | +| `cert-manager` | ✅ Succeeded | | +| `dro` | ✅ Succeeded | | +| `sls` | ⏭ Skipped | `--sls-action none` used to bypass existing SLS | +| `mcpi` | ❌ Failed | `mcpicfg` role not found in published CLI image | +| `pipeline-finish` | ✅ Succeeded | | +| `sync-install` | ✅ Succeeded | | + +--- + +## Open Issues + +#### 1. `mcpicfg` role missing from published CLI image +**Severity:** Blocker for end-to-end validation +**Root cause:** The Tekton `mcpi` task uses `quay.io/ibmmas/cli:latest` which is the published image. The `mcpicfg` Ansible role only exists on the `mcpi-cli-install` branch of `ansible-devops` and hasn't been built into an image yet. +**Workaround:** Use the dev image digest from the FVT pipeline (see Step 2 above). +**Fix:** Build and publish a CLI image from the `mcpi-cli-install` branch and use that image reference in the Tekton task. + +#### 2. `--license-file` unconditionally required (Bug — Fixed) +**Severity:** Real bug affecting all users +**Root cause:** `nonInteractiveMode()` in `app.py` always errors if `--license-file` is not set, even when `--sls-namespace` points to an existing SLS. +**Fix applied:** [`app.py`](python/src/mas/cli/mcpi/install/app.py:122) — skip the check when a non-default `sls_namespace` is set. +**Note:** Same bug exists in `aiservice/install/app.py:437` — should be fixed there too. + +#### 3. `--sls-action` flag — dev workaround only +**Severity:** Low +**Fix applied:** Added to `argParser.py` to allow `--sls-action none` to skip SLS on dev clusters. +**Recommendation:** Remove before merging to production — not needed for real installs. + +#### 4. `pipeline-start` needs `mas-devops` secret for MongoDB config generation +**Severity:** Dev cluster only +**Root cause:** `pipeline-start` reads `DEVOPS_MONGO_URI` from `mas-devops` secret to write `mongo-.yml` to the workspace. This secret only exists in `mas--pipelines`, not the new `mcpi--pipelines` namespace. +**Workaround:** Copy secret (see Step 5 above). +**Recommendation:** `prepareMcpiPipelinesNamespace()` should auto-copy `mas-devops` from `mas-{instanceId}-pipelines` if it exists. + +#### 5. macOS `build/bin/build-tekton.sh` fails +**Severity:** Dev workflow only +**Root cause:** macOS `sed -i` requires a backup suffix extension. +**Workaround:** Use the Python script in Step 4 above. +**Recommendation:** Add macOS note to `CONTRIBUTING.md` or add `gsed` fallback to the script. + +#### 6. `build/lib/` not in `.gitignore` +**Status:** Fixed. + + +--- + +## Testing Session Notes (2025-02-XX / Cluster: visionary) + +### Cluster Values Used + +| Resource | Value | +|---|---| +| MAS Instance ID | `visionary` | +| MAS Core Namespace | `mas-visionary-core` | +| MCPI Pipelines Namespace | `mcpi-visionary-pipelines` | +| SLS Namespace | `sls-visionary` | +| MongoDB Namespace | `mongoce` | +| Storage Class (RWO + RWX) | `nfs-client` (single class, NFS supports RWX) | +| MAS Version | `9.2.3-pre.maint92cpi` | +| Entity Manager | `visionary-entitymgr-mcpicfg` (running in `mas-visionary-core`) | +| Dev CLI Image Digest | `quay.io/ibmmas/cli@sha256:865097dd25032aaad9fe462e6433faf76d34b758c34832fb5701795bc38d85aa` | + +### Pipeline Task Results (this run) + +| Task | Status | Notes | +|---|---|---| +| `pipeline-start` | ❌ Failed | `ibm-mas-tekton.yaml` had literal `` placeholder — Step 4 had not been run | +| All tasks | ❌ Failed | `TaskRunImagePullFailed` on every task due to invalid image reference | + +--- + +## Architecture Discovery (2025-02-XX) + +This session uncovered a fundamental design error in the original implementation. The following documents what was discovered and what was changed. + +### Discovery 1 — `McpiCfg` is owned by the existing `ibm-mas` operator + +The `ibm-mas` CSV already owns and watches `McpiCfg` CRs. There is **no separate MCPI operator subscription** to install. The entity manager deployment `visionary-entitymgr-mcpicfg` is already running in `mas-visionary-core` and reconciles any `McpiCfg` CR automatically. + +**Implication:** The pipeline's only job is to create the `McpiCfg` CR. Everything else (TLS certs, routes, deployment, OAuth registration) is handled by the operator. + +### Discovery 2 — The `mcpicfg` role in `ansible-devops` was a copy of the operator's internal role + +A `diff` showed the role was byte-for-byte identical to `ibm-mas/operator/ibm-mas-cfg-mcpi/roles/mcpicfg/`. Running it as a Tekton task was wrong for three reasons: + +1. It requires `ansible_operator_meta` vars (CR name/namespace) only available inside the operator container +2. It requires the `operator_sdk` collection not installed in the CLI image +3. It looks itself up via `k8s_info` on the `McpiCfg` CR — which doesn't exist yet at pipeline time + +### Discovery 3 — `mcpi_channel` was wired to nothing + +The param flowed from CLI → pipeline → task → env var `MCPI_CHANNEL`, but nothing in the `mcpicfg` role ever read it. Confirmed by `grep` returning zero results across the entire role. + +### Discovery 4 — `routing_mode` / `manual_route_mgmt` / `mas_use_service_mesh` were dead params + +These were copied from the `mas-install` pattern but the `mcpicfg` role never consumed them. They were threaded all the way through pipeline → taskdef → task → Python CLI with no effect. + +### Discovery 5 — `ibm-mas-tekton.yaml` had a literal placeholder + +The template still contained `quay.io/ibmmas/cli@sha256:` because Step 4 of the testing checklist had never been run. This caused all pipeline task pods to fail with `TaskRunImagePullFailed`. + +### Discovery 6 — `gencfg_mcpicfg` role was missing entirely + +The correct Ansible role — generating a `McpiCfg` YAML file into the pipeline workspace — did not exist. The `suite_config` pattern (generate CR → apply CR → operator reconciles) was never implemented. + +--- + +## Refactor Applied (2025-02-XX) + +### What was removed + +| What | Repo | Reason | +|---|---|---| +| Entire `mcpicfg` role (16 files) | `ansible-devops` | Operator's internal logic — belongs only inside `ibm-mas-cfg-mcpi` image | +| `run-role.sh mcpicfg` Tekton step | `cli` | Replaced with correct two-step pattern | +| `routing_mode`, `manual_route_mgmt` params | `cli` (Tekton + Python) | No consumer in any role | +| `mas_use_service_mesh` param | `cli` (Tekton pipeline) | No consumer in MCPI context | +| `--routing-mode`, `--manual-route-mgmt` CLI flags | `cli` (argParser/argBuilder/summarizer) | Removed from CLI surface | + +### What was added + +| What | Repo | Purpose | +|---|---|---| +| `gencfg_mcpicfg` role (4 files) | `ansible-devops` | Validates `mas_instance_id` + `mas_config_dir`, renders `McpiCfg` CR YAML to workspace | +| `gencfg-mcpicfg` Tekton step | `cli` | Runs `gencfg_mcpicfg` role to write CR to workspace | +| `suite-config` Tekton step | `cli` | Applies workspace YAML to cluster via `suite_config` role | +| `routing_mode`/`manual_route_mgmt` in ignored keys | `cli` (`app.py`) | Backward compat — old scripts with these flags won't crash | + +### Correct flow post-refactor + +``` +CLI → mcpi-install Pipeline + ↓ + mcpi Task + ① gencfg_mcpicfg role → writes -mcpi-system.yml to /workspace/configs + ② suite_config role → kubectl apply → McpiCfg CR created in mas--core + ↓ + visionary-entitymgr-mcpicfg (already running — watches McpiCfg) + ↓ reconciles automatically + Deploys: TLS certs, Routes, Deployment, Services, NetworkPolicy, OAuth redirect URI +``` + +### Commits + +| Repo | Commit | Change | +|---|---|---| +| `ansible-devops` | `refactor(mcpicfg): remove mcpicfg operator role` | Deleted 16-file `mcpicfg` role | +| `ansible-devops` | `feat(mcpicfg): add gencfg_mcpicfg role` | Added 4-file `gencfg_mcpicfg` role | +| `cli` | `refactor(mcpi-cli-install): remove routing mode and service mesh params, add sls-action arg` | All Python + Tekton cleanup | + +--- + +## Open Issues (Updated) + +#### 1. ~~`mcpicfg` role missing from published CLI image~~ — **Root cause was wrong** +**Status:** Closed / redesigned +**What actually happened:** The `mcpicfg` role was a copy of the operator's internal reconcile logic and was never the right approach. Replaced by `gencfg_mcpicfg` which generates the CR YAML, and `suite_config` which applies it. + +#### 2. `--license-file` unconditionally required (Bug — Fixed) +**Severity:** Real bug affecting all users +**Root cause:** `nonInteractiveMode()` in `app.py` always errors if `--license-file` is not set, even when `--sls-namespace` points to an existing SLS. +**Fix applied:** [`app.py`](python/src/mas/cli/mcpi/install/app.py:122) — skip the check when a non-default `sls_namespace` is set. +**Note:** Same bug exists in `aiservice/install/app.py:437` — should be fixed there too. + +#### 3. `--sls-action` flag — dev workaround only +**Severity:** Low +**Fix applied:** Added to `argParser.py` to allow `--sls-action none` to skip SLS on dev clusters. +**Recommendation:** Remove before merging to production — not needed for real installs. + +#### 4. `pipeline-start` needs `mas-devops` secret for MongoDB config generation +**Severity:** Dev cluster only +**Root cause:** `pipeline-start` reads `DEVOPS_MONGO_URI` from `mas-devops` secret to write `mongo-.yml` to the workspace. This secret only exists in `mas--pipelines`, not the new `mcpi--pipelines` namespace. +**Workaround:** Copy secret (see Step 5 above). +**Recommendation:** `prepareMcpiPipelinesNamespace()` should auto-copy `mas-devops` from `mas-{instanceId}-pipelines` if it exists. + +#### 5. macOS `build/bin/build-tekton.sh` fails +**Severity:** Dev workflow only +**Root cause:** macOS `sed -i` requires a backup suffix extension. +**Workaround:** Use the Python script in Step 4 above. +**Recommendation:** Add macOS note to `CONTRIBUTING.md` or add `gsed` fallback to the script. + +#### 6. `build/lib/` not in `.gitignore` +**Status:** Fixed. + +#### 7. `mcpi_channel` param is accepted but unused +**Severity:** Low — cosmetic +**Root cause:** `mcpi_channel` flows through the pipeline to the `mcpi` task env var `MCPI_CHANNEL`, but `gencfg_mcpicfg` does not read it (the channel is an operator subscription concern, not a CR field). +**Recommendation:** Either remove `mcpi_channel` from the pipeline entirely (the operator handles its own subscription via the catalog) or keep as a no-op for future use if a channel field is added to the `McpiCfg` spec. diff --git a/.bob/plans/2025-06-11-mcpi-install-cleanup.md b/.bob/plans/2025-06-11-mcpi-install-cleanup.md new file mode 100644 index 0000000000..e1c6a27ada --- /dev/null +++ b/.bob/plans/2025-06-11-mcpi-install-cleanup.md @@ -0,0 +1,298 @@ +# MCPI Install Cleanup Plan + +## Objective + +Remove the over-engineered standalone `mcpi-install` command and pipeline across three repos, and replace with the minimal correct approach: a `--mcpi-channel` flag on the existing `mas-cli install` command that includes the `McpiCfg` CR via `additional-configs`, applied by the already-present `suite-config` Tekton task. + +--- + +## Design Decisions + +### Why the standalone approach was wrong + +The over-built implementation treated MCPI like a standalone product (AI Service pattern) with its own: +- CLI command (`mas-cli mcpi-install`) +- Tekton pipeline (`mcpi-install.yml.j2`) +- Custom Tekton task (`mas-devops-mcpi`) running `gencfg_mcpicfg` + `suite_config` +- Ansible role (`gencfg_mcpicfg`) generating a `McpiCfg` CR +- python-devops namespace provisioning (`prepareMcpiPipelinesNamespace`, separate RBAC/PVC templates) + +### The McpiCfg CR is trivially simple + +`ibm/mas_devops/roles/gencfg_mcpicfg/templates/mcpicfg.yml.j2`: + +```yaml +apiVersion: config.mas.ibm.com/v1 +kind: McpiCfg +metadata: + name: "{{ mas_instance_id }}-mcpi-system" + namespace: "mas-{{ mas_instance_id }}-core" + labels: + mas.ibm.com/configScope: system + mas.ibm.com/instanceId: "{{ mas_instance_id }}" +spec: + displayName: "Maximo Cluster Performance Insights" + config: + enabled: true +``` + +There is no subscription channel, no database, no S3. The `mcpi_channel` param was never used in the CR template or anywhere in the `gencfg_mcpicfg` role — it was vestigial. + +### How suite_config handles additional-configs + +`run-role.sh` (line 3-5) copies `additional-configs/*` → `configs/` **before every role invocation**. The existing `suite-config` Tekton task calls `run-role.sh suite_config`. Therefore: + +- If a `McpiCfg` YAML is placed in the `additional-configs` workspace, `suite_config` will automatically apply it +- The `McpiCfg` CR uses `{{ mas_instance_id }}` in name/namespace which `suite_config` resolves via `kubernetes.core.k8s template:` + +### Decision: No new Tekton task needed, no `mcpi_channel` Tekton param needed + +The existing `suite-config` step in `mas-install.yml.j2` is sufficient to apply the McpiCfg, as long as the CR YAML is present in `additional-configs`. No separate Tekton step, no new params file, no `mcpi_channel` pipeline parameter. + +The `--mcpi-channel` CLI flag in `mas-cli install` serves as the user-facing gate. When set, the CLI: +1. Sets `self.installMcpi = True` +2. Sets `mcpi_channel` in params (for display in summary/argBuilder — the channel value itself isn't used functionally) +3. The McpiCfg YAML is bundled into the generated `additional-configs` secret when the `python-devops` `launchInstallPipeline` builds the PipelineRun + +**Important**: Because `mcpi_channel` is not used in the CR, passing it as a Tekton pipeline param serves no purpose. The `--mcpi-channel` flag is needed only to: +- Signal that MCPI should be installed (gate) +- Display in the install summary +- Be reproducible via `buildCommand()` + +--- + +## Critical Rules + +- Track progress only by updating checkboxes in this plan file — do NOT use chat todo lists +- Delete files entirely rather than commenting them out +- Each phase must be independently validatable before proceeding to the next +- Do NOT add error handling or abstractions beyond the minimal patterns already used by other apps (e.g., `facilities_channel`) +- Preserve all unrelated content in files being partially edited +- When removing lines from `__main__.py`, `mascli/mas`, `.bashrc`, `pytest.ini` — verify surrounding context before editing + +--- + +## Execution Plan + +### Phase 1 — Delete over-built files + +**Objective**: Remove all files that belong exclusively to the standalone mcpi-install implementation. + +[ ] **1.1** Delete `python/src/mas/cli/mcpi/` (entire directory — 7 files) + +[ ] **1.2** Delete `python/tests/integration/mcpi_install/` (entire directory — 3 files) + +[ ] **1.3** Delete `tekton/src/pipelines/mcpi-install.yml.j2` + +[ ] **1.4** Delete `tekton/src/tasks/mcpi/mcpi.yml.j2` and the `mcpi/` directory + +[ ] **1.5** Delete `tekton/src/pipelines/taskdefs/mcpi/mcpi.yml.j2` and the `mcpi/` directory + +[ ] **1.6** Delete `tekton/src/params/install-mcpi.yml.j2` + +[ ] **1.7** Delete `ibm/mas_devops/roles/gencfg_mcpicfg/` (entire directory — 4 files across 4 subdirectories) in `ansible-devops` repo + +[ ] **1.8** Delete `src/mas/devops/templates/mcpi-pipelines-pvc.yml.j2` in `python-devops` repo + +[ ] **1.9** Delete `src/mas/devops/templates/mcpi-pipelines-rbac.yml.j2` in `python-devops` repo + +[ ] **1.10** Delete `src/mas/devops/templates/pipelinerun-mcpi-install.yml.j2` in `python-devops` repo + +[ ] **1.11** Delete `test/src/test_tekton_mcpi.py` in `python-devops` repo + +**Validation**: Confirm none of the deleted paths exist. No build or test run needed yet. + +--- + +### Phase 2 — Undo file modifications in `cli` repo + +**Objective**: Revert all partial changes to shared files that were modified to support the standalone approach. + +**Relevant files and what to undo:** + +#### `tekton/src/params/install.yml.j2` +Remove the MCPI Configuration section (lines ~862-864): +``` +# MCPI Configuration +{{ lookup('template', params_src_dir ~ '/install-mcpi.yml.j2') }} +``` + +#### `tekton/src/pipelines/mas-install.yml.j2` +- Remove section 15 "Install and configure MCPI" block (~lines 464-468): + ``` + # 15. Install and configure MCPI (Maximo Cluster Performance Insights) + # ------------------------------------------------------------------------- + {{ lookup('template', pipeline_src_dir ~ '/taskdefs/mcpi/mcpi.yml.j2') | indent(4) }} + runAfter: + - suite-config + ``` +- Remove `- mcpi` from `post-install-verify` `runAfter` list (~line 484) +- Renumber section "16. Verify health..." back to "15." if sections were renumbered + +#### `tekton/generate-tekton-tasks.yml` +Remove the MCPI block (~lines 45-52): +```yaml + # 3. Generate Tasks (MCPI) + # ------------------------------------------------------------------------- + - name: Generate Tasks (MCPI) + ansible.builtin.template: + src: "{{ task_src_dir }}/mcpi/{{ item }}.yml.j2" + dest: "{{ task_target_dir }}/{{ item }}.yaml" + with_items: + - mcpi +``` +Also renumber subsequent sections (AI Service was originally section 3, not 4). + +#### `tekton/generate-tekton-pipelines.yml` +Remove the MCPI pipelines entry (~lines 46-47): +```yaml + # MCPI Pipelines + - mcpi-install +``` + +#### `python/src/mas/cli/__main__.py` +Remove: +- The usage text line: `" - mas-cli mcpi-install Install Maximo Cluster Performance Insights\n"` +- The dispatcher block (~lines 78-82): + ```python + if function == "mcpi-install": + from mas.cli.mcpi.install.app import McpiInstallApp + app = McpiInstallApp() + raise SystemExit(app.install(argv[2:])) + ``` + +#### `pytest.ini` +Remove the two MCPI markers (~lines 12-13): +``` + mcpi-install: MCPI installation tests (mas-cli mcpi-install) + mcpi_install: MCPI installation tests (mas-cli mcpi-install) +``` + +#### `image/cli/mascli/mas` +Remove the `mcpi-install` case block (~lines 290-298): +```bash + mcpi-install) + echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" >> $LOGFILE + echo "!! mcpi-install !!" >> $LOGFILE + echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" >> $LOGFILE + # Take the first parameter off (it will be "mcpi-install") + shift + # Run the new Python-based mcpi-install + mas-cli mcpi-install "$@" + ;; +``` + +#### `image/cli/app-root/src/.bashrc` +Remove the MCPI Management section (~lines 51-52): +``` +echo "MCPI Management:" +echo " - ${TEXT_BOLD}${COLOR_GREEN}mas mcpi-install${TEXT_RESET} to install MCPI into an existing MAS instance" +``` + +#### `python-devops` repo: `src/mas/devops/tekton.py` +Remove: +- `prepareMcpiPipelinesNamespace()` function (~lines 846-920) +- `launchMcpiInstallPipeline()` function (~lines 1610-1628) + +**Validation**: After all edits, run a grep to confirm no remaining `mcpi-install` or `mcpi_channel` references in shared files. Tekton generation playbooks should parse cleanly (no missing template references). + +--- + +### Phase 3 — Add `--mcpi-channel` to `mas-cli install` + +**Objective**: Implement MCPI as a standard MAS app option in the existing install command, following the same pattern as `facilities_channel`. + +#### `python/src/mas/cli/install/argParser.py` +In the "MAS Applications" argument group (after `--aiservice-channel`, before the `# Arcgis` section), add: +```python +masAppsArgGroup.add_argument( + "--mcpi-channel", + required=False, + help="Subscription channel for Maximo Cluster Performance Insights", +) +``` + +#### `python/src/mas/cli/install/app.py` — `nonInteractiveMode()` init block +At lines ~2204-2216, `nonInteractiveMode()` initializes all install booleans to `False`. Add `self.installMcpi = False` to this block alongside the other flags. + +#### `python/src/mas/cli/install/app.py` — `nonInteractiveMode()` key dispatch +After the `elif key == "aiservice_channel":` block (around line 2414), add: +```python +elif key == "mcpi_channel": + if value is not None and value != "": + self.setParam("mcpi_channel", value) + self.installMcpi = True +``` + +#### `python/src/mas/cli/install/app.py` — `configApps()` +After the AI Service block (end of method, around line 1356), add a simple unconditional prompt: +```python +self.installMcpi = self.yesOrNo("Install Maximo Cluster Performance Insights (MCPI)") +if self.installMcpi: + self.configAppChannel("mcpi") # OR: self.promptForString(...) +``` + +> **Note**: `McpiCfg` has no subscription channel field in the CR, but we still prompt for a channel to match the install pattern and for future use. If `configAppChannel` requires an entry in the compatibility matrix, use `promptForString("MCPI subscription channel", "mcpi_channel")` instead. + +#### `python/src/mas/cli/install/params.py` +Add `"mcpi_channel"` to `optionalParams` list (near the app channels section). + +#### `python/src/mas/cli/install/argBuilder.py` — `buildCommand()` +After the `if self.installAIService:` block (~line 285), add: +```python +if self.installMcpi: + command += f" --mcpi-channel \"{self.getParam('mcpi_channel')}\"{newline}" +``` + +#### `python/src/mas/cli/install/summarizer.py` +Add an `mcpiSummary()` method following the pattern of `facilitiesSummary()` or `assistSummary()`: +```python +def mcpiSummary(self) -> None: + if self.installMcpi: + self.printSummary("MCPI", self.getParam("mcpi_channel")) + else: + self.printSummary("MCPI", "Do Not Install") +``` +Call `self.mcpiSummary()` in the appropriate place in the summary output method. + +**Validation**: Run `python -m pytest python/tests -m unit -k install` to confirm existing install tests pass. Verify `--help` output for `mas-cli install` shows `--mcpi-channel`. + +--- + +### Phase 4 — Final validation + +**Objective**: Confirm the full state of all three repos is clean and correct. + +[ ] **4.1** In `cli` repo: run `grep -r "mcpi-install\|McpiInstall\|mcpi_channel\|gencfg_mcpicfg\|launchMcpiInstall\|prepareMcpiPipelines" python/ tekton/ image/ pytest.ini` — expect zero results (except `mcpi_channel` in the new optionalParams and argParser additions) + +[ ] **4.2** In `cli` repo: run `ansible-playbook tekton/generate-tekton-tasks.yml --check` — expect success (no missing template errors) + +[ ] **4.3** In `cli` repo: run `ansible-playbook tekton/generate-tekton-pipelines.yml --check` — expect success + +[ ] **4.4** In `cli` repo: run `.venv/bin/pytest python/tests -m unit` — all unit tests pass + +[ ] **4.5** In `ansible-devops` repo: confirm `ibm/mas_devops/roles/gencfg_mcpicfg/` directory is gone + +[ ] **4.6** In `python-devops` repo: run `.venv/bin/pytest test/` — all tests pass (mcpi test file removed, no broken imports) + +--- + +## Final Validation + +### Success criteria + +1. `grep -r "mcpi-install" cli/python/ cli/tekton/ cli/image/` returns zero matches +2. `ansible-playbook tekton/generate-tekton-tasks.yml` completes without errors +3. `ansible-playbook tekton/generate-tekton-pipelines.yml` completes without errors +4. `mas-cli install --help` shows `--mcpi-channel` in the MAS Applications group +5. `mas-cli mcpi-install` returns "command not found" or usage error (no longer a valid subcommand) +6. All unit tests in `cli` repo pass +7. All tests in `python-devops` pass +8. `gencfg_mcpicfg` role no longer exists in `ansible-devops` + +### Troubleshooting + +- If `generate-tekton-tasks.yml` fails: check that `tekton/src/tasks/mcpi/` directory was fully removed and no `with_items: - mcpi` block remains +- If `generate-tekton-pipelines.yml` fails: check that `tekton/src/pipelines/mcpi-install.yml.j2` was removed and `all_pipelines` list no longer contains `mcpi-install` +- If Python import fails: ensure `python/src/mas/cli/mcpi/` directory was fully removed and `__main__.py` dispatcher was cleaned up +- If `installMcpi` attribute error: ensure `self.installMcpi = False` default is set in `InstallApp.__init__()` alongside other `self.installXxx = False` defaults diff --git a/.gitignore b/.gitignore index e1d88be873..c468b9095e 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,8 @@ image/cli/mascli/fyre-dev.noble3.tgke-env.sh image/cli/mascli/fyre-dev.noble3.tgke-db2u-manage-env.sh image/cli/podman-build* +build/lib/mas/cli + python/.venv python/.venv-windows python/build @@ -64,7 +66,7 @@ rbac-install.yaml .venv /.bob/notes -pytest.log -/dist -/.bob/.bob-errors -/.mkdocs_cache +pytest.log +/dist +/.bob/.bob-errors +/.mkdocs_cache diff --git a/python/src/mas/cli/install/app.py b/python/src/mas/cli/install/app.py index 77e43fb4a5..e10f092435 100644 --- a/python/src/mas/cli/install/app.py +++ b/python/src/mas/cli/install/app.py @@ -1355,6 +1355,12 @@ def configApps(self): else: self.installAIService = False + # MCPI is only available from MAS 9.2 + if isVersionEqualOrAfter("9.2.0", self.getParam("mas_channel")): + self.installMcpi = self.yesOrNo("Install Maximo Cluster Performance Insights (MCPI)") + else: + self.installMcpi = False + @logMethodCall def configAppChannel(self, appId): versions = self.getCompatibleVersions(self.params["mas_channel"], appId) @@ -2214,6 +2220,7 @@ def nonInteractiveMode(self) -> None: self.db2SetAffinity = False self.db2SetTolerations = False self.installAIService = False + self.installMcpi = False self.slsLicenseFileLocal = None self.db2LicenseFileLocal = None self.aiserviceTenantSchedulingConfigFileLocal = None @@ -2385,6 +2392,9 @@ def nonInteractiveMode(self) -> None: if value is not None and value != "": self.setParam("mas_app_channel_facilities", value) self.installFacilities = True + elif key == "install_mcpi": + if value: + self.installMcpi = True elif key == "aiservice_channel": if value is not None and value != "": self.setParam("aiservice_channel", value) @@ -2631,6 +2641,9 @@ def nonInteractiveMode(self) -> None: self.licensePrompt() self.setParam("db2u_kind", "db2ucluster") + if self.installMcpi and not isVersionEqualOrAfter("9.2.0", self.getParam("mas_channel")): + self.fatalError(f"--install-mcpi is only supported for MAS 9.2+ (selected channel: {self.getParam('mas_channel')})") + if self.getParam("mas_issuer_kind") != "" and not isVersionEqualOrAfter("9.2.0", self.getParam("mas_channel")): self.fatalError(f"--mas-issuer-kind is only supported for MAS 9.2+ (selected channel: {self.getParam('mas_channel')})") diff --git a/python/src/mas/cli/install/argBuilder.py b/python/src/mas/cli/install/argBuilder.py index a4f71c2d42..6d885a8a50 100644 --- a/python/src/mas/cli/install/argBuilder.py +++ b/python/src/mas/cli/install/argBuilder.py @@ -286,6 +286,8 @@ def buildCommand(self) -> str: command += f" --aiservice-channel \"{self.getParam('aiservice_channel')}\"{newline}" if self.getParam("configure_aiassistant") != "": command += f" --configure-aiassistant \"{self.getParam('configure_aiassistant')}\"{newline}" + if self.installMcpi: + command += f" --install-mcpi{newline}" # Arcgis # ----------------------------------------------------------------------------- diff --git a/python/src/mas/cli/install/argParser.py b/python/src/mas/cli/install/argParser.py index 86d80d584c..b85d309adb 100644 --- a/python/src/mas/cli/install/argParser.py +++ b/python/src/mas/cli/install/argParser.py @@ -604,6 +604,14 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str: required=False, help="Subscription channel for Maximo AI Service", ) +masAppsArgGroup.add_argument( + "--install-mcpi", + dest="install_mcpi", + required=False, + action="store_true", + default=False, + help="Install Maximo Cluster Performance Insights (MAS 9.2 or later)", +) # Arcgis # ----------------------------------------------------------------------------- diff --git a/python/src/mas/cli/install/settings/additionalConfigs.py b/python/src/mas/cli/install/settings/additionalConfigs.py index 197bded9bb..2e8a4e80b9 100644 --- a/python/src/mas/cli/install/settings/additionalConfigs.py +++ b/python/src/mas/cli/install/settings/additionalConfigs.py @@ -19,6 +19,22 @@ logger = logging.getLogger(__name__) +# McpiCfg resource applied by the suite-config task (Jinja2 variables are resolved by kubernetes.core.k8s template) +MCPI_CONFIG_FILENAME = "mcpi-system.yaml" +MCPI_CONFIG_TEMPLATE = """--- +apiVersion: config.mas.ibm.com/v1 +kind: McpiCfg +metadata: + name: "{{ mas_instance_id }}-mcpi-system" + namespace: "mas-{{ mas_instance_id }}-core" + labels: + mas.ibm.com/configScope: system + mas.ibm.com/instanceId: "{{ mas_instance_id }}" +spec: + displayName: "Maximo Cluster Performance Insights" + config: + enabled: true +""" if TYPE_CHECKING: from prompt_toolkit.completion import WordCompleter @@ -29,6 +45,7 @@ class AdditionalConfigsMixin: if TYPE_CHECKING: # Attributes from BaseApp and other mixins params: Dict[str, str] + installMcpi: bool _interactiveMode: bool localConfigDir: str | None noConfirm: bool @@ -119,6 +136,16 @@ def additionalConfigs(self) -> None: logger.debug(additionalConfigsSecret) self.additionalConfigsSecret = additionalConfigsSecret + # MCPI is enabled by an McpiCfg resource applied by the suite-config task, so inject it into the secret + if self.installMcpi: + if self.additionalConfigsSecret is None: + self.additionalConfigsSecret = {"apiVersion": "v1", "kind": "Secret", "type": "Opaque", "metadata": {"name": "pipeline-additional-configs"}} + secretData = self.additionalConfigsSecret.setdefault("data", {}) + # A user supplied file with the same name takes precedence + if MCPI_CONFIG_FILENAME not in secretData: + secretData[MCPI_CONFIG_FILENAME] = b64encode(MCPI_CONFIG_TEMPLATE.encode("ascii")).decode("ascii") + print_formatted_text(f"Maximo Cluster Performance Insights (MCPI) configuration will be applied: {MCPI_CONFIG_FILENAME}") + def podTemplates(self) -> None: if self.isInteractiveMode and self.showAdvancedOptions: self.printH1("Configure Pod Templates") diff --git a/python/src/mas/cli/install/summarizer.py b/python/src/mas/cli/install/summarizer.py index 801a42e929..5f0d0a1e23 100644 --- a/python/src/mas/cli/install/summarizer.py +++ b/python/src/mas/cli/install/summarizer.py @@ -49,6 +49,7 @@ class InstallSummarizerMixin: installOptimizer: bool installFacilities: bool installAIService: bool + installMcpi: bool installArcgis: bool dynamicClient: DynamicClient applyPreInstallMASRBAC: bool @@ -461,6 +462,12 @@ def aiServiceSummary(self) -> None: self.printParamSummary("URL", "aiservice_watsonxai_url") self.printParamSummary("Project ID", "aiservice_watsonxai_project_id") + def mcpiSummary(self) -> None: + if self.installMcpi: + self.printSummary("Maximo Cluster Performance Insights (MCPI)", "Install") + else: + self.printSummary("Maximo Cluster Performance Insights (MCPI)", "Do Not Install") + def db2Summary(self) -> None: if ( self.getParam("db2_action_system") == "install" @@ -666,6 +673,7 @@ def displayInstallSummary(self) -> None: self.inspectionSummary() self.facilitiesSummary() self.aiServiceSummary() + self.mcpiSummary() # Application Dependencies self.mongoSummary() diff --git a/python/tests/integration/install/test_aiservice_interactive_dns.py b/python/tests/integration/install/test_aiservice_interactive_dns.py index 472eef0d31..9c51aa7a2c 100644 --- a/python/tests/integration/install/test_aiservice_interactive_dns.py +++ b/python/tests/integration/install/test_aiservice_interactive_dns.py @@ -77,6 +77,7 @@ def _base_prompts(tmpdir): ".*Install Visual Inspection.*": lambda msg: "n", ".*Install.*Real Estate and Facilities.*": lambda msg: "n", ".*Install AI Service.*": lambda msg: "y", + ".*Install Maximo Cluster Performance Insights.*": lambda msg: "n", ".*Custom channel for AI Service.*": lambda msg: "9.2.x", ".*Customize database settings.*": lambda msg: "n", ".*Enter AI Service Tenant ID to bind with Manage:.*": lambda msg: "user", diff --git a/python/tests/integration/install/test_arcgis_cluster_mode.py b/python/tests/integration/install/test_arcgis_cluster_mode.py index 24d67f1a55..4e3e2b12f0 100755 --- a/python/tests/integration/install/test_arcgis_cluster_mode.py +++ b/python/tests/integration/install/test_arcgis_cluster_mode.py @@ -128,6 +128,7 @@ def test_install_arcgis_cluster_mode_success(tmpdir): ".*Install Visual Inspection.*": lambda msg: "n", ".*Install.*Real Estate and Facilities.*": lambda msg: "n", ".*Install AI Service.*": lambda msg: "n", + ".*Install Maximo Cluster Performance Insights.*": lambda msg: "n", ".*Include IBM Maximo Location Services for Esri.*": lambda msg: "y", ".*Do you accept the license terms?.*": lambda msg: "y", # 23. Grafana configuration (appears when advanced options are enabled) @@ -290,6 +291,7 @@ def test_install_arcgis_namespace_mode_error(tmpdir, caplog): ".*Install Visual Inspection.*": lambda msg: "n", ".*Install.*Real Estate and Facilities.*": lambda msg: "n", ".*Install AI Service.*": lambda msg: "n", + ".*Install Maximo Cluster Performance Insights.*": lambda msg: "n", # ERROR OCCURS HERE - after selecting ArcGIS with cluster mode ".*Include IBM Maximo Location Services for Esri.*": lambda msg: "y", } @@ -433,6 +435,7 @@ def test_install_arcgis_minimal_mode_error(tmpdir, caplog): ".*Install Visual Inspection.*": lambda msg: "n", ".*Install.*Real Estate and Facilities.*": lambda msg: "n", ".*Install AI Service.*": lambda msg: "n", + ".*Install Maximo Cluster Performance Insights.*": lambda msg: "n", # ERROR OCCURS HERE - after selecting ArcGIS with minimal mode ".*Include IBM Maximo Location Services for Esri.*": lambda msg: "y", } diff --git a/python/tests/integration/install/test_dev_mode.py b/python/tests/integration/install/test_dev_mode.py index 22c6f423cf..b381a00bf8 100644 --- a/python/tests/integration/install/test_dev_mode.py +++ b/python/tests/integration/install/test_dev_mode.py @@ -322,6 +322,7 @@ def test_install_master_dev_mode_with_path_routing(tmpdir): ".*Install Visual Inspection.*": lambda msg: "n", ".*Install.*Real Estate and Facilities.*": lambda msg: "n", ".*Install AI Service.*": lambda msg: "n", + ".*Install Maximo Cluster Performance Insights.*": lambda msg: "n", # 23. Grafana configuration (appears when advanced options are enabled) ".*Install Grafana.*": lambda msg: "y", # 24. MongoDB configuration diff --git a/python/tests/integration/install/test_letsencrypt.py b/python/tests/integration/install/test_letsencrypt.py index 6c3b63aa7d..b33278a6d2 100644 --- a/python/tests/integration/install/test_letsencrypt.py +++ b/python/tests/integration/install/test_letsencrypt.py @@ -361,6 +361,7 @@ def _common_handlers(self, tmpdir, le_response): ".*Install Visual Inspection.*": lambda msg: "n", ".*Install.*Real Estate and Facilities.*": lambda msg: "n", ".*Install AI Service.*": lambda msg: "n", + ".*Install Maximo Cluster Performance Insights.*": lambda msg: "n", ".*Do you want to configure AiCfg.*": lambda msg: "n", ".*Install Grafana.*": lambda msg: "y", # MongoDB diff --git a/python/tests/unit/test_install_mcpi_config.py b/python/tests/unit/test_install_mcpi_config.py new file mode 100644 index 0000000000..483807df11 --- /dev/null +++ b/python/tests/unit/test_install_mcpi_config.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python +# ***************************************************************************** +# Copyright (c) 2026 IBM Corporation and other Contributors. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v1.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v10.html +# +# ***************************************************************************** + +"""Test that the install CLI injects the McpiCfg resource into the additional configs secret.""" + +from base64 import b64decode + +from mas.cli.install.settings.additionalConfigs import AdditionalConfigsMixin, MCPI_CONFIG_FILENAME + + +class FakeApp(AdditionalConfigsMixin): + """Minimal host for AdditionalConfigsMixin.""" + + def __init__(self, installMcpi, localConfigDir=None): + self.isInteractiveMode = False + self.noConfirm = True + self.installMcpi = installMcpi + self.localConfigDir = localConfigDir + self.additionalConfigsSecret = None + + +def test_mcpi_config_added_when_mcpi_enabled(): + """Test that the McpiCfg file is added to the secret when MCPI is enabled. + + GIVEN MCPI is enabled and no local config directory is set + WHEN additionalConfigs() runs + THEN the pipeline-additional-configs secret contains the McpiCfg YAML + """ + app = FakeApp(installMcpi=True) + app.additionalConfigs() + + assert app.additionalConfigsSecret["metadata"]["name"] == "pipeline-additional-configs" + content = b64decode(app.additionalConfigsSecret["data"][MCPI_CONFIG_FILENAME]).decode() + assert "kind: McpiCfg" in content + assert "{{ mas_instance_id }}-mcpi-system" in content + + +def test_mcpi_config_not_added_when_mcpi_disabled(): + """Test that no secret is created when MCPI is disabled. + + GIVEN MCPI is not enabled and no local config directory is set + WHEN additionalConfigs() runs + THEN no additional configs secret is created + """ + app = FakeApp(installMcpi=False) + app.additionalConfigs() + + assert app.additionalConfigsSecret is None + + +def test_mcpi_config_merged_with_user_configs(tmp_path): + """Test that the McpiCfg file is merged with user supplied configs. + + GIVEN MCPI is enabled and a local config directory with one YAML file + WHEN additionalConfigs() runs + THEN the secret contains both the user file and the McpiCfg file + """ + (tmp_path / "user.yaml").write_text("kind: ConfigMap\n") + app = FakeApp(installMcpi=True, localConfigDir=str(tmp_path)) + app.additionalConfigs() + + assert set(app.additionalConfigsSecret["data"].keys()) == {"user.yaml", MCPI_CONFIG_FILENAME} + + +def test_user_supplied_mcpi_config_takes_precedence(tmp_path): + """Test that a user supplied file with the same name is not overwritten. + + GIVEN MCPI is enabled and the local config directory already has the MCPI file + WHEN additionalConfigs() runs + THEN the user's content is preserved + """ + (tmp_path / MCPI_CONFIG_FILENAME).write_text("kind: McpiCfg\n# custom\n") + app = FakeApp(installMcpi=True, localConfigDir=str(tmp_path)) + app.additionalConfigs() + + assert "# custom" in b64decode(app.additionalConfigsSecret["data"][MCPI_CONFIG_FILENAME]).decode() diff --git a/python/tests/unit/test_install_mcpi_flag.py b/python/tests/unit/test_install_mcpi_flag.py new file mode 100644 index 0000000000..78af1b68d0 --- /dev/null +++ b/python/tests/unit/test_install_mcpi_flag.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python +# ***************************************************************************** +# Copyright (c) 2026 IBM Corporation and other Contributors. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v1.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v10.html +# +# ***************************************************************************** + +"""Test the --install-mcpi flag on the install command argument parser.""" + +import pytest + +from mas.cli.install.argParser import installArgParser + + +def test_install_mcpi_defaults_to_false(): + """Test that MCPI is not installed unless requested. + + GIVEN no --install-mcpi argument + WHEN the install arguments are parsed + THEN install_mcpi is False + """ + args = installArgParser.parse_args([]) + assert args.install_mcpi is False + + +def test_install_mcpi_flag_sets_true(): + """Test that --install-mcpi enables the install. + + GIVEN the --install-mcpi argument + WHEN the install arguments are parsed + THEN install_mcpi is True + """ + args = installArgParser.parse_args(["--install-mcpi"]) + assert args.install_mcpi is True + + +def test_install_mcpi_flag_takes_no_value(): + """Test that --install-mcpi is a plain on/off flag. + + GIVEN --install-mcpi followed by a value + WHEN the install arguments are parsed + THEN the parser rejects the unrecognized value + """ + with pytest.raises(SystemExit): + installArgParser.parse_args(["--install-mcpi", "v9.2"]) diff --git a/tekton/src/pipelines/mas-install.yml.j2 b/tekton/src/pipelines/mas-install.yml.j2 index 3510b8857b..63bd612498 100644 --- a/tekton/src/pipelines/mas-install.yml.j2 +++ b/tekton/src/pipelines/mas-install.yml.j2 @@ -50,7 +50,6 @@ spec: # 12. Install & Configure Visual Inspection # 13. Install & Configure Facilities # 14. Install & Configure AI Service with Default Tenant - - name: pipeline-start timeout: "0" taskRef: