Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions image/cli/mascli/functions/provision_fyre
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ function provision_fyre_noninteractive() {
esac
done

INSTALL_SERVICE_MESH=true
INSTALL_KIALI=true

# Check all args have been set
[[ -z "$FYRE_USERNAME" ]] && provision_fyre_help "FYRE_USERNAME is not set"
[[ -z "$FYRE_APIKEY" ]] && provision_fyre_help "FYRE_APIKEY is not set"
Expand Down
3 changes: 3 additions & 0 deletions image/cli/mascli/functions/provision_roks
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ function provision_roks_noninteractive() {
esac
done

INSTALL_SERVICE_MESH=true
INSTALL_KIALI=true

# Check all args have been set
[[ -z "$IBMCLOUD_APIKEY" ]] && provision_roks_help "IBMCLOUD_APIKEY is not set"
[[ -z "$IBMCLOUD_RESOURCEGROUP" ]] && provision_roks_help "IBMCLOUD_RESOURCEGROUP is not set"
Expand Down
3 changes: 2 additions & 1 deletion python/src/mas/cli/install/argParser.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,14 +166,15 @@ def isValidFile(parser: argparse.ArgumentParser, arg: str) -> str:
required=False,
help="Configure MAS to use Service Mesh networking (default: false)",
choices=["true", "false"],
default="true",
)
masAdvancedArgGroup.add_argument(
"--manual-routes",
dest="mas_manual_route_mgmt",
required=False,
action="store_const",
const="true",
default="false",
default="true",
help="Disable automatic creation of routes.",
)
masAdvancedArgGroup.add_argument(
Expand Down
14 changes: 13 additions & 1 deletion python/src/mas/cli/must_gather/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ def planCollection(self, parsedArgs, outputDir: str):
CollectionPlan: Complete plan with all collection tasks organized into groups
"""
from .collection_plan import CollectionPlan
from .dependencies import kafka, mongodb, grafana, cert_manager, db2, cp4d, rhoai
from .dependencies import kafka, mongodb, grafana, cert_manager, db2, cp4d, rhoai, servicemesh

# Type assertion: dynClient is guaranteed to be non-None by connect()
assert self.dynamicClient is not None, "Kubernetes client must be initialized before planning collection"
Expand Down Expand Up @@ -382,6 +382,18 @@ def planCollection(self, parsedArgs, outputDir: str):
else:
logger.debug("Skipping CP4D collection (not in collectors list)")

# Service Mesh
if "servicemesh" in enabledCollectors:
servicemesh.addServiceMeshToCollectionPlan(
plan=plan,
dynClient=self.dynamicClient,
outputDir=outputDir,
noLogs=parsedArgs.no_logs,
ibmCRDs=self.ibmCRDsList,
)
else:
logger.debug("Skipping ServiceMesh collection (not in collectors list)")

# Red Hat OpenShift AI
if "rhoai" in enabledCollectors:
rhoai.addRHOAIToCollectionPlan(
Expand Down
18 changes: 17 additions & 1 deletion python/src/mas/cli/must_gather/arg_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,23 @@ def _parse_args_wrapper(args=None, namespace=None):


# Define all available collectors
ALL_COLLECTORS = ["ocp", "db2", "kafka", "mongodb", "cp4d", "cert-manager", "grafana", "sls", "mas", "rhoai", "aiservice", "lic", "pipelines", "amlen"]
ALL_COLLECTORS = [
"ocp",
"db2",
"kafka",
"mongodb",
"cp4d",
"cert-manager",
"grafana",
"sls",
"mas",
"rhoai",
"aiservice",
"lic",
"pipelines",
"amlen",
"servicemesh",
]


def validateCollectors(collectorsStr: str) -> str:
Expand Down
108 changes: 108 additions & 0 deletions python/src/mas/cli/must_gather/dependencies/servicemesh.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# *****************************************************************************
# 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
#
# *****************************************************************************

"""Service Mesh dependency collector."""

import logging
from typing import Set
from kubernetes.dynamic import DynamicClient
from .utils import discoverNamespacesFromCR

logger = logging.getLogger(__name__)

# ServiceMesh-specific custom resources to collect (apiVersion, kind)
SERVICEMESH_CLUSTER_RESOURCES = [
("sailoperator.io/v1", "Istio"),
("sailoperator.io/v1", "IstioCNI"),
]
SERVICEMESH_NS_RESOURCES = [
("networking.istio.io/v1", "Gateway"),
("networking.istio.io/v1", "VirtualService"),
]


def _discoverServiceMeshNamespaces(dynClient: DynamicClient) -> Set[str]:
"""Discover namespaces containing ServiceMesh resources.

Discovers namespaces by finding all ServiceMesh custom resources in the cluster.

Args:
dynClient (DynamicClient): Kubernetes Dynamic Client for API access

Returns:
set: Set of namespace names where ServiceMesh CRs exist
"""
namespaces = set()
for apiVersion, kind in SERVICEMESH_NS_RESOURCES:
namespaces.update(discoverNamespacesFromCR(dynClient=dynClient, kind=kind, apiVersion=apiVersion))

return namespaces


def addServiceMeshToCollectionPlan(plan, dynClient: DynamicClient, outputDir: str, noLogs: bool, ibmCRDs: list):
"""Add ServiceMesh collection tasks to the collection plan.

Discovers ServiceMesh namespaces and adds collection groups for each namespace
to the provided collection plan.

Args:
plan (CollectionPlan): Collection plan to add tasks to
dynClient (DynamicClient): Kubernetes Dynamic Client for API access
outputDir (str): Base output directory for collected resources
noLogs (bool): If True, skip pod log collection
ibmCRDs (list): List of IBM CRD information for collection
"""
from ..common.task_generation import generateNamespaceCollectionTasks
from ..common.resources import collectResources

# Collect cluster-scoped ServiceMesh resources first
logger.debug("Collecting cluster-scoped ServiceMesh resources")
clusterTasks = []

# Add Istio and IstioCNI (cluster-scoped)
for apiVersion, kind in SERVICEMESH_CLUSTER_RESOURCES:
# clusterTasks.append((
# lambda: collectResources(namespace=None, apiVersion=apiVersion, kind=kind, outputDir=outputDir, allNamespaces=False) # None = cluster-scoped
# )
clusterTasks.append(
(
kind,
collectResources,
None, # namespace=None for cluster-scoped
apiVersion,
kind,
outputDir,
False, # allNamespaces
)
)

if clusterTasks:
plan.addGroup("ServiceMesh (Cluster)", clusterTasks)
logger.debug(f"Added {len(clusterTasks)} cluster-scoped ServiceMesh tasks")

# Now collect namespace-scoped resources
logger.debug("Discovering ServiceMesh namespaces")
serviceMeshNamespaces = _discoverServiceMeshNamespaces(dynClient)

if serviceMeshNamespaces:
logger.info(f"Discovered {len(serviceMeshNamespaces)} ServiceMesh namespace(s): {', '.join(sorted(serviceMeshNamespaces))}")
for ns in sorted(serviceMeshNamespaces):
tasks = generateNamespaceCollectionTasks(
dynClient=dynClient,
namespace=ns,
outputDir=outputDir,
noLogs=noLogs,
customResources=SERVICEMESH_NS_RESOURCES,
ibmCRDs=ibmCRDs,
)
plan.addGroup(f"ServiceMesh ({ns})", tasks)
logger.debug(f"Added {len(tasks)} ServiceMesh collection tasks for namespace {ns}")
else:
logger.info("No ServiceMesh namespaces discovered")
2 changes: 1 addition & 1 deletion python/tests/unit/must_gather/test_arg_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ def test_parser_collectors_default_all_enabled(self):
"""
parser = mustGatherArgParser
args = parser.parse_args([])
expected = "ocp,db2,kafka,mongodb,cp4d,cert-manager,grafana,sls,mas,rhoai,aiservice,lic,pipelines,amlen"
expected = "ocp,db2,kafka,mongodb,cp4d,cert-manager,grafana,sls,mas,rhoai,aiservice,lic,pipelines,amlen,servicemesh"
assert args.collectors == expected

def test_parser_collectors_single_collector(self):
Expand Down
28 changes: 28 additions & 0 deletions python/tests/unit/must_gather/test_collectors_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,34 @@ def test_aiservice_collector_disabled_when_excluded(self, mustGatherApp):
mustGatherApp.planCollection(args, "/tmp/test-output")
assert "aiservice" not in args.collectors

def test_servicemesh_collector_enabled_by_default(self, mustGatherApp):
"""Test that Service Mesh collector is enabled by default.

GIVEN default collectors configuration
WHEN planCollection is called
THEN ServiceMesh resources are included in collection plan.
"""
parser = mustGatherArgParser
args = parser.parse_args([])

with patch.object(mustGatherApp, "_collectMustGather"):
mustGatherApp.planCollection(args, "/tmp/test-output")
assert "servicemesh" in args.collectors

def test_servicemesh_collector_disabled_when_excluded(self, mustGatherApp):
"""Test that Service Mesh collector is disabled when excluded from collectors.

GIVEN collectors configuration without aiservice
WHEN planCollection is called
THEN AIService resources are not included in collection plan.
"""
parser = mustGatherArgParser
args = parser.parse_args(["--collectors", "ocp,mas"])

with patch.object(mustGatherApp, "_collectMustGather"):
mustGatherApp.planCollection(args, "/tmp/test-output")
assert "servicemesh" not in args.collectors

def test_multiple_collectors_combination(self, mustGatherApp):
"""Test that multiple collectors can be combined.

Expand Down
8 changes: 8 additions & 0 deletions tekton/src/tasks/suite-app-install.yml.j2
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,14 @@ spec:
- name: MAS_FACILITIES_SCHEDULERAGENT_DEPLOYMENTMODE
value: $(params.mas_ws_facilities_scheduleragent_deploymentmode)

# configure service mesh domain routing
- name: servicemesh-domain-config
command:
- /opt/app-root/src/run-role.sh
- servicemesh_domain_config
image: "quay.io/ibmmas/cli:latest"
imagePullPolicy: $(params.image_pull_policy)

- name: app-wait-for-approval
# If configmap/approval-app-cfg-$(params.mas_app_id) exists then set STATUS=pending and wait for it to be changed to "approved"
command:
Expand Down
8 changes: 8 additions & 0 deletions tekton/src/tasks/suite-verify.yml.j2
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ spec:
image: "quay.io/ibmmas/cli:latest"
imagePullPolicy: $(params.image_pull_policy)

# configure service mesh domain routing
- name: servicemesh-domain-config
command:
- /opt/app-root/src/run-role.sh
- servicemesh_domain_config
image: "quay.io/ibmmas/cli:latest"
imagePullPolicy: $(params.image_pull_policy)

# If configmap/approval-suite-verify exists then set STATUS=pending and wait for it to be changed to "approved"
- name: suite-wait-for-approval
image: "quay.io/ibmmas/cli:latest"
Expand Down
Loading