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: support for self-hosted issuer + template refresh #362

Merged
merged 6 commits into from
Sep 17, 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
2 changes: 1 addition & 1 deletion azext_edge/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import os

VERSION = "0.7.0a7"
VERSION = "0.7.0a8"
EXTENSION_NAME = "azure-iot-ops"
EXTENSION_ROOT = os.path.dirname(os.path.abspath(__file__))
USER_AGENT = "IotOperationsCliExtension/{}".format(VERSION)
89 changes: 63 additions & 26 deletions azext_edge/edge/providers/orchestration/resources/instances.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,12 @@ def get_spc_name(instance_name: str) -> str:
return f"{instance_name}-spc"


def get_fc_name(cluster_name: str, namespace: str, instance_name: str) -> str:
return f"{cluster_name}-" + url_safe_hash_phrase(f"{cluster_name}-{namespace}-{instance_name}")[:5]
def get_fc_name(cluster_name: str, oidc_issuer: str, subject: str) -> str:
return url_safe_hash_phrase(f"{cluster_name}-{oidc_issuer}-{subject}")[:7]


def get_cred_subject(namespace: str, service_account_name: str):
return f"system:serviceaccount:{namespace}:{service_account_name}"


def get_enable_syntax(instanc_name: str, resource_group_name: str) -> str:
Expand Down Expand Up @@ -147,14 +151,18 @@ def remove_mi_user_assigned(
):
mi_resource_id_container = parse_resource_id(mi_user_assigned)
instance = self.show(name=name, resource_group_name=resource_group_name)

cluster_resource = self.get_resource_map(instance).connected_cluster.resource
custom_location = self._get_associated_cl(instance)
namespace = custom_location["properties"]["namespace"]
oidc_issuer = self._ensure_oidc_issuer(cluster_resource)

cred_subject = get_cred_subject(namespace=namespace, service_account_name=SERVICE_ACCOUNT_DATAFLOW)
if not federated_credential_name:
federated_credential_name = get_fc_name(
cluster_name=cluster_resource["name"],
namespace=custom_location["properties"]["namespace"],
instance_name=instance["name"],
oidc_issuer=oidc_issuer,
subject=cred_subject,
)
self.unfederate_msi(mi_resource_id_container, federated_credential_name)

Expand Down Expand Up @@ -191,21 +199,23 @@ def add_mi_user_assigned(
mi_resource_id_container = parse_resource_id(mi_user_assigned)
instance = self.show(name=name, resource_group_name=resource_group_name)
cluster_resource = self.get_resource_map(instance).connected_cluster.resource
self._ensure_oidc_issuer(cluster_resource)
oidc_issuer = self._ensure_oidc_issuer(cluster_resource)
custom_location = self._get_associated_cl(instance)
namespace = custom_location["properties"]["namespace"]
cred_subject = get_cred_subject(namespace=namespace, service_account_name=SERVICE_ACCOUNT_DATAFLOW)

if not federated_credential_name:
federated_credential_name = get_fc_name(
cluster_name=cluster_resource["name"],
namespace=custom_location["properties"]["namespace"],
instance_name=instance["name"],
oidc_issuer=oidc_issuer,
subject=cred_subject,
)
self.federate_msi(
mi_resource_id_container,
oidc_issuer=cluster_resource["properties"]["oidcIssuerProfile"]["issuerUrl"],
oidc_issuer=oidc_issuer,
subject=cred_subject,
federated_credential_name=federated_credential_name,
namespace=custom_location["properties"]["namespace"],
)

identity: dict = instance.get("identity", {})
if not identity or identity.get("type") == "None":
identity["type"] = "UserAssigned"
Expand Down Expand Up @@ -246,8 +256,11 @@ def enable_secretsync(
resource_map = self.get_resource_map(instance)
cluster_resource = resource_map.connected_cluster.resource
custom_location = self._get_associated_cl(instance)
namespace = custom_location["properties"]["namespace"]
cred_subject = get_cred_subject(namespace=namespace, service_account_name=SERVICE_ACCOUNT_SECRETSYNC)
oidc_issuer = self._ensure_oidc_issuer(cluster_resource)

cl_resources = resource_map.connected_cluster.get_aio_resources(custom_location_id=custom_location["id"])
self._ensure_oidc_issuer(cluster_resource)
secretsync_spc = self._find_existing_spc(cl_resources)
if secretsync_spc:
status.stop()
Expand All @@ -256,21 +269,18 @@ def enable_secretsync(
f"Use 'az iot ops secretsync show -n {instance['name']} -g {resource_group_name}' for details."
)
return

if not federated_credential_name:
federated_credential_name = (
get_fc_name(
cluster_name=cluster_resource["name"],
namespace=custom_location["properties"]["namespace"],
instance_name=instance["name"],
)
+ "-ssc"
federated_credential_name = get_fc_name(
cluster_name=cluster_resource["name"],
oidc_issuer=oidc_issuer,
subject=cred_subject,
)
self.federate_msi(
mi_resource_id_container=mi_resource_id_container,
oidc_issuer=cluster_resource["properties"]["oidcIssuerProfile"]["issuerUrl"],
oidc_issuer=oidc_issuer,
subject=cred_subject,
federated_credential_name=federated_credential_name,
namespace=custom_location["properties"]["namespace"],
service_account_name=SERVICE_ACCOUNT_SECRETSYNC,
)
spc_poller = self.ssc_mgmt_client.azure_key_vault_secret_provider_classes.begin_create_or_update(
resource_group_name=resource_group_name,
Expand Down Expand Up @@ -341,7 +351,7 @@ def _find_existing_spc(self, cl_resources: List[dict]) -> Optional[dict]:

def _attempt_keyvault_role_assignments(self, keyvault: dict, mi_user_assigned: dict) -> Optional[str]:
"""
Returns error string is role-assignment fails.
Returns error string if the role-assignment fails.
"""
target_role_ids = [KEYVAULT_ROLE_ID_SECRETS_USER, KEYVAULT_ROLE_ID_READER]
try:
Expand All @@ -361,7 +371,7 @@ def _attempt_keyvault_role_assignments(self, keyvault: dict, mi_user_assigned: d
scope=keyvault["id"],
)

def _ensure_oidc_issuer(self, cluster_resource: dict):
def _ensure_oidc_issuer(self, cluster_resource: dict) -> str:
enabled_oidc = cluster_resource["properties"].get("oidcIssuerProfile", {}).get("enabled", False)
enabled_wlif = (
cluster_resource["properties"].get("securityProfile", {}).get("workloadIdentity", {}).get("enabled", False)
Expand All @@ -385,21 +395,36 @@ def _ensure_oidc_issuer(self, cluster_resource: dict):
if any([not enabled_oidc, not enabled_wlif]):
raise ValidationError(error)

oidc_issuer_profile: dict = cluster_resource["properties"]["oidcIssuerProfile"]
issuer_url = oidc_issuer_profile.get("issuerUrl") or oidc_issuer_profile.get("selfHostedIssuerUrl")
if not issuer_url:
raise ValidationError("No issuer Url is available. Check cluster config.")
return issuer_url

def federate_msi(
self,
mi_resource_id_container: ResourceIdContainer,
oidc_issuer: str,
subject: str,
federated_credential_name: str,
namespace: str = "azure-iot-operations",
service_account_name: str = SERVICE_ACCOUNT_DATAFLOW,
):
if self._find_federated_cred(
mi_resource_id_container=mi_resource_id_container, issuer_url=oidc_issuer, subject=subject
):
logger.debug(
f"This OIDC issuer '{oidc_issuer}'\n"
f"and subject '{subject}' combo are already associated "
f"with identity '{mi_resource_id_container.resource_name}'.\n"
"No new federated credential will be created."
)
return
self.msi_mgmt_client.federated_identity_credentials.create_or_update(
resource_group_name=mi_resource_id_container.resource_group_name,
resource_name=mi_resource_id_container.resource_name,
federated_identity_credential_resource_name=federated_credential_name,
parameters={
"properties": {
"subject": f"system:serviceaccount:{namespace}:{service_account_name}",
"subject": subject,
"audiences": ["api://AzureADTokenExchange"],
"issuer": oidc_issuer,
}
Expand All @@ -416,3 +441,15 @@ def unfederate_msi(
resource_name=mi_resource_id_container.resource_name,
federated_identity_credential_resource_name=federated_credential_name,
)

def _find_federated_cred(
self, mi_resource_id_container: ResourceIdContainer, issuer_url: str, subject: str
) -> Optional[dict]:
cred_iteratable = self.msi_mgmt_client.federated_identity_credentials.list(
resource_group_name=mi_resource_id_container.resource_group_name,
resource_name=mi_resource_id_container.resource_name,
)
for cred in cred_iteratable:
cred_props: dict = cred["properties"]
if cred_props.get("issuer") == issuer_url and cred_props.get("subject") == subject:
return cred
12 changes: 8 additions & 4 deletions azext_edge/edge/providers/orchestration/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,13 @@ def copy(self) -> "TemplateBlueprint":
IOT_OPERATIONS_VERSION_MONIKER = "v0.7.0-preview"

M2_ENABLEMENT_TEMPLATE = TemplateBlueprint(
commit_id="7f8a94d145d50ca1374d5fc7b99bb8725053e785",
commit_id="0f5d0d234045e945ab7808c6aa2a44dc0ce77723",
content={
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"languageVersion": "2.0",
"contentVersion": "1.0.0.0",
"metadata": {
"_generator": {"name": "bicep", "version": "0.29.47.4906", "templateHash": "15814050749892782988"}
"_generator": {"name": "bicep", "version": "0.29.47.4906", "templateHash": "16868064799087538653"}
},
"definitions": {
"_1.AdvancedConfig": {
Expand Down Expand Up @@ -220,7 +220,7 @@ def copy(self) -> "TemplateBlueprint":
"platform": "0.7.6",
"aio": "0.7.13",
"secretSyncController": "0.6.4",
"edgeStorageAccelerator": "2.1.0-preview",
"edgeStorageAccelerator": "2.1.1-preview",
"openServiceMesh": "1.2.9",
},
"TRAINS": {
Expand Down Expand Up @@ -256,7 +256,7 @@ def copy(self) -> "TemplateBlueprint":
"akri.values.agent.host.containerRuntimeSocket": "[parameters('containerRuntimeSocket')]",
"akri.values.kubernetesDistro": "[toLower(parameters('kubernetesDistro'))]",
"mqttBroker.values.global.quickstart": "false",
"mqttBroker.values.operator.firstPartyMetricsOn": "false",
"mqttBroker.values.operator.firstPartyMetricsOn": "true",
"observability.metrics.enabled": "[format('{0}', variables('OBSERVABILITY_ENABLED'))]",
"observability.metrics.openTelemetryCollectorAddress": "[if(variables('OBSERVABILITY_ENABLED'), format('{0}', tryGet(tryGet(parameters('advancedConfig'), 'observability'), 'otelCollectorAddress')), '')]",
"observability.metrics.exportIntervalSeconds": "[format('{0}', coalesce(tryGet(tryGet(parameters('advancedConfig'), 'observability'), 'otelExportIntervalSeconds'), 60))]",
Expand Down Expand Up @@ -325,6 +325,10 @@ def copy(self) -> "TemplateBlueprint":
"autoUpgradeMinorVersion": False,
"version": "[coalesce(tryGet(tryGet(parameters('advancedConfig'), 'openServiceMesh'), 'version'), variables('VERSIONS').openServiceMesh)]",
"releaseTrain": "[coalesce(tryGet(tryGet(parameters('advancedConfig'), 'openServiceMesh'), 'train'), variables('TRAINS').openServiceMesh)]",
"configurationSettings": {
"osm.osm.enablePermissiveTrafficPolicy": "false",
"osm.osm.featureFlags.enableWASMStats": "false",
},
},
"dependsOn": ["cluster"],
},
Expand Down