Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: capture secretstore resources in support bundle #372

Merged
merged 24 commits into from
Sep 25, 2024
Merged
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
8 changes: 8 additions & 0 deletions azext_edge/edge/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

from knack.help_files import helps

from azext_edge.edge.providers.edge_api import SECRETSTORE_API_V1, SECRETSYNC_API_V1

from .providers.edge_api import MQ_ACTIVE_API
from .providers.support_bundle import (
COMPAT_ARCCONTAINERSTORAGE_APIS,
Expand Down Expand Up @@ -56,6 +58,8 @@ def load_iotops_help():
- {COMPAT_CLUSTER_CONFIG_APIS.as_str()}
- {COMPAT_DATAFLOW_APIS.as_str()}
- {COMPAT_ARCCONTAINERSTORAGE_APIS.as_str()}
- {SECRETSYNC_API_V1.as_str()}
- {SECRETSTORE_API_V1.as_str()}

Note: logs from evicted pod will not be captured, as they are inaccessible. For details
on why a pod was evicted, please refer to the related pod and node files.
Expand All @@ -81,6 +85,10 @@ def load_iotops_help():
- name: Include arc container storage resources in the support bundle.
text: >
az iot ops support create-bundle --ops-service acs

- name: Include secretstore resources in the support bundle.
text: >
az iot ops support create-bundle --ops-service secretstore
"""

helps[
Expand Down
1 change: 1 addition & 0 deletions azext_edge/edge/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ class OpsServiceType(ListableEnum):
dataflow = "dataflow"
schemaregistry = "schemaregistry"
arccontainerstorage = "acs"
secretstore = "secretstore"

@classmethod
def list_check_services(cls):
Expand Down
3 changes: 3 additions & 0 deletions azext_edge/edge/providers/edge_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from .dataflow import DATAFLOW_API_V1B1, DataflowResourceKinds
from .meta import META_API_V1B1, MetaResourceKinds
from .arccontainerstorage import ARCCONTAINERSTORAGE_API_V1
from .secretstore import SECRETSYNC_API_V1, SECRETSTORE_API_V1

__all__ = [
"ARCCONTAINERSTORAGE_API_V1",
Expand All @@ -32,4 +33,6 @@
"DataflowResourceKinds",
"META_API_V1B1",
"MetaResourceKinds",
"SECRETSYNC_API_V1",
"SECRETSTORE_API_V1",
]
20 changes: 20 additions & 0 deletions azext_edge/edge/providers/edge_api/secretstore.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# coding=utf-8
# ----------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License file in the project root for license information.
# ----------------------------------------------------------------------------------------------

from .base import EdgeResourceApi


SECRETSYNC_API_V1 = EdgeResourceApi(
group="secret-sync.x-k8s.io",
version="v1alpha1",
moniker="secretsync",
)

SECRETSTORE_API_V1 = EdgeResourceApi(
group="secrets-store.csi.x-k8s.io",
version="v1",
moniker="secretstore",
)
77 changes: 77 additions & 0 deletions azext_edge/edge/providers/support/secretstore.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# coding=utf-8
# ----------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License file in the project root for license information.
# ----------------------------------------------------------------------------------------------

from functools import partial
from typing import Iterable, Optional

from knack.log import get_logger

from ..edge_api import EdgeResourceApi
from .base import (
DAY_IN_SECONDS,
assemble_crd_work,
process_deployments,
process_replicasets,
process_services,
process_v1_pods,
)

logger = get_logger(__name__)

SSC_DIRECTORY_PATH = "secretstore"
# TODO: Use common label once it is ready
SSC_NAMESPACE = "azure-secret-store"


def fetch_deployments():
return process_deployments(
directory_path=SSC_DIRECTORY_PATH,
namespace=SSC_NAMESPACE,
)


def fetch_replicasets():
return process_replicasets(
directory_path=SSC_DIRECTORY_PATH,
namespace=SSC_NAMESPACE,
)


def fetch_pods(since_seconds: int = DAY_IN_SECONDS):
return process_v1_pods(
directory_path=SSC_DIRECTORY_PATH,
since_seconds=since_seconds,
namespace=SSC_NAMESPACE,
)


def fetch_services():
return process_services(
directory_path=SSC_DIRECTORY_PATH,
namespace=SSC_NAMESPACE,
)


support_runtime_elements = {
"deployments": fetch_deployments,
"replicasets": fetch_replicasets,
"services": fetch_services,
}


def prepare_bundle(
log_age_seconds: int = DAY_IN_SECONDS,
apis: Optional[Iterable[EdgeResourceApi]] = None,
) -> dict:
ssc_to_run = {}

if apis:
ssc_to_run.update(assemble_crd_work(apis=apis, directory_path=SSC_DIRECTORY_PATH))

support_runtime_elements["pods"] = partial(fetch_pods, since_seconds=log_age_seconds)
ssc_to_run.update(support_runtime_elements)

return ssc_to_run
8 changes: 8 additions & 0 deletions azext_edge/edge/providers/support_bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
DATAFLOW_API_V1B1,
META_API_V1B1,
ARCCONTAINERSTORAGE_API_V1,
SECRETSYNC_API_V1,
SECRETSTORE_API_V1,
EdgeApiManager,
)

Expand All @@ -34,6 +36,7 @@
COMPAT_DATAFLOW_APIS = EdgeApiManager(resource_apis=[DATAFLOW_API_V1B1])
COMPAT_META_APIS = EdgeApiManager(resource_apis=[META_API_V1B1])
COMPAT_ARCCONTAINERSTORAGE_APIS = EdgeApiManager(resource_apis=[ARCCONTAINERSTORAGE_API_V1])
COMPAT_SECRETSTORE_APIS = EdgeApiManager(resource_apis=[SECRETSYNC_API_V1, SECRETSTORE_API_V1])


def build_bundle(
Expand All @@ -57,6 +60,7 @@ def build_bundle(
from .support.meta import prepare_bundle as prepare_meta_bundle
from .support.schemaregistry import prepare_bundle as prepare_schema_registry_bundle
from .support.arccontainerstorage import prepare_bundle as prepare_arccontainerstorage_bundle
from .support.secretstore import prepare_bundle as prepare_secretstore_bundle

def collect_default_works(
pending_work: dict,
Expand Down Expand Up @@ -102,6 +106,10 @@ def collect_default_works(
"apis": COMPAT_ARCCONTAINERSTORAGE_APIS,
"prepare_bundle": prepare_arccontainerstorage_bundle,
},
OpsServiceType.secretstore.value: {
"apis": COMPAT_SECRETSTORE_APIS,
"prepare_bundle": prepare_secretstore_bundle,
},
}

for service_moniker, api_info in api_map.items():
Expand Down
78 changes: 46 additions & 32 deletions azext_edge/tests/edge/support/create_bundle_int/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
# ----------------------------------------------------------------------------------------------

from knack.log import get_logger
from typing import Dict, List, NamedTuple, Optional, Tuple, Union
from typing import Dict, List, Optional, Tuple, Union
from os import path
from zipfile import ZipFile
import pytest
from azure.cli.core.azclierror import CLIInternalError
from azext_edge.edge.common import OpsServiceType
from azext_edge.edge.providers.edge_api.base import EdgeResourceApi
from azext_edge.edge.providers.support.arcagents import ARC_AGENTS
from ....helpers import (
Expand Down Expand Up @@ -38,13 +39,6 @@
]


class NamespaceTuple(NamedTuple):
arc: str
aio: str
acs: str
usage_system: str


def assert_file_names(files: List[str]):
"""Asserts file names."""
for full_name in files:
Expand Down Expand Up @@ -238,7 +232,12 @@ def get_file_map(
mq_traces: bool = False,
) -> Dict[str, Dict[str, List[Dict[str, str]]]]:
# Remove all files that will not be checked
arc_namespace, aio_namespace, acs_namespace, c_namespace = process_top_levels(walk_result, ops_service)
namespaces = process_top_levels(walk_result, ops_service)
arc_namespace = namespaces.get("arc")
aio_namespace = namespaces.get("aio")
acs_namespace = namespaces.get("acs")
ssc_namespace = namespaces.get("ssc")
c_namespace = namespaces.get("usage_system")

if aio_namespace:
walk_result.pop(path.join(BASE_ZIP_PATH, aio_namespace))
Expand Down Expand Up @@ -276,6 +275,16 @@ def get_file_map(

# no files for aio, skip the rest assertions
return file_map
elif ops_service == OpsServiceType.secretstore.value:
ops_path = path.join(BASE_ZIP_PATH, aio_namespace, OpsServiceType.secretstore.value)
ssc_path = path.join(BASE_ZIP_PATH, ssc_namespace, OpsServiceType.secretstore.value)
if ops_path not in walk_result:
# no crd created in aio namespace
assert len(walk_result) == 1 + expected_default_walk_result
else:
assert len(walk_result) == 2 + expected_default_walk_result
file_map[OpsServiceType.secretstore.value] = convert_file_names(walk_result[ssc_path]["files"])
file_map["__namespaces__"][OpsServiceType.secretstore.value] = ssc_namespace
elif ops_service == "deviceregistry":
if ops_path not in walk_result:
assert len(walk_result) == expected_default_walk_result
Expand All @@ -294,7 +303,7 @@ def get_file_map(
def process_top_levels(
walk_result: Dict[str, Dict[str, List[str]]],
ops_service: str,
) -> NamespaceTuple:
) -> Dict[str, Union[str, None]]:
level_0 = walk_result.pop(BASE_ZIP_PATH)
for file in ["events.yaml", "nodes.yaml", "storage-classes.yaml", "azure-clusterconfig.yaml"]:
assert file in level_0["files"]
Expand All @@ -305,6 +314,7 @@ def process_top_levels(
clusterconfig_namespace = None
arc_namespace = None
acs_namespace = None
ssc_namespace = None

def _get_namespace_determinating_files(name: str, folder: str, file_prefix: str) -> List[str]:
level1 = walk_result.get(path.join(BASE_ZIP_PATH, name, folder), {})
Expand All @@ -323,45 +333,49 @@ def _get_namespace_determinating_files(name: str, folder: str, file_prefix: str)
arc_namespace = name
elif _get_namespace_determinating_files(name=name, folder=path.join("arccontainerstorage"), file_prefix="pvc"):
acs_namespace = name
elif _get_namespace_determinating_files(
name=name, folder=OpsServiceType.secretstore.value, file_prefix="deployment"
):
ssc_namespace = name
else:
namespace = name

for namespace_folder, service in [
(clusterconfig_namespace, "clusterconfig"),
(arc_namespace, "arcagents"),
(acs_namespace, "arccontainerstorage"),
(ssc_namespace, OpsServiceType.secretstore.value),
]:
if namespace_folder:
# remove empty folders in level 1
level_1 = walk_result.pop(path.join(BASE_ZIP_PATH, namespace_folder))
assert level_1["folders"] == [service]
assert not level_1["files"]

# remove empty folders in level 2
if clusterconfig_namespace:
# remove empty billing related folders
level_1 = walk_result.pop(path.join(BASE_ZIP_PATH, clusterconfig_namespace))
assert level_1["folders"] == ["clusterconfig"]
assert not level_1["files"]
level_2 = walk_result.pop(path.join(BASE_ZIP_PATH, clusterconfig_namespace, "clusterconfig"))
assert level_2["folders"] == ["billing"]
assert not level_2["files"]

if arc_namespace:
# remove empty arc related folders
level_1 = walk_result.pop(path.join(BASE_ZIP_PATH, arc_namespace))
assert level_1["folders"] == ["arcagents"]
assert not level_1["files"]
level_2 = walk_result.pop(path.join(BASE_ZIP_PATH, arc_namespace, "arcagents"))
assert level_2["folders"] == [agent[0] for agent in ARC_AGENTS]
assert not level_2["files"]

if acs_namespace:
# remove empty acs related folders
level_1 = walk_result.pop(path.join(BASE_ZIP_PATH, acs_namespace))
assert level_1["folders"] == ["arccontainerstorage"]
assert not level_1["files"]

logger.debug("Determined the following namespaces:")
logger.debug(f"AIO namespace: {namespace}")
logger.debug(f"Usage system namespace: {clusterconfig_namespace}")
logger.debug(f"ARC namespace: {arc_namespace}")
logger.debug(f"ACS namespace: {acs_namespace}")

return NamespaceTuple(
arc=arc_namespace,
aio=namespace,
acs=acs_namespace,
usage_system=clusterconfig_namespace,
)
logger.debug(f"SSC namespace: {ssc_namespace}")

return {
"arc": arc_namespace,
"aio": namespace,
"acs": acs_namespace,
"ssc": ssc_namespace,
"usage_system": clusterconfig_namespace,
}


def run_bundle_command(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,10 @@ def test_create_bundle(init_setup, bundle_dir, mq_traces, ops_service, tracked_f
)

# Level 0 - top
namespace = process_top_levels(walk_result, ops_service)
aio_namespace = namespace.aio
namespaces = process_top_levels(walk_result, ops_service)
aio_namespace = namespaces.get("aio")
acs_namespace = namespaces.get("acs")
ssc_namespace = namespaces.get("ssc")

# Level 1
level_1 = walk_result.pop(path.join(BASE_ZIP_PATH, aio_namespace))
Expand All @@ -74,8 +76,12 @@ def test_create_bundle(init_setup, bundle_dir, mq_traces, ops_service, tracked_f
walk_result[path.join(BASE_ZIP_PATH, aio_namespace, OpsServiceType.mq.value)]["folders"] = []

# remove acs resources from walk_result from aio namespace assertion
if namespace.acs:
walk_result.pop(path.join(BASE_ZIP_PATH, namespace.acs, "arccontainerstorage"), {})
if acs_namespace:
walk_result.pop(path.join(BASE_ZIP_PATH, acs_namespace, "arccontainerstorage"), {})

# remove ssc resources in ssc namespace from walk_result from aio namespace assertion
if ssc_namespace:
walk_result.pop(path.join(BASE_ZIP_PATH, ssc_namespace, OpsServiceType.secretstore.value), {})

# Level 2 and 3 - bottom
is_billing_included = OpsServiceType.billing.value in expected_services
Expand Down Expand Up @@ -130,5 +136,12 @@ def _get_expected_services(
):
expected_services.remove(OpsServiceType.arccontainerstorage.value)

# secretstore folder will not be created if there are no secretstore resources
if (
not walk_result.get(path.join(BASE_ZIP_PATH, namespace, OpsServiceType.secretstore.value))
and OpsServiceType.secretstore.value in expected_services
):
expected_services.remove(OpsServiceType.secretstore.value)

expected_services.append("meta")
return expected_services
Loading