Loading src/main/java/org/etsi/osl/cridge/simulation/SimulatedArgoCdK8aasResources.java 0 → 100644 +292 −0 Original line number Diff line number Diff line package org.etsi.osl.cridge.simulation; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import io.fabric8.kubernetes.api.model.GenericKubernetesResource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; /** * The ArgoCD K8aaS special case carved out of {@link SimulatedKubernetesClientResource}: given a * simulated deploy of an ArgoCD {@code Application} CR whose Helm chart provisions a * Terraform-controller-backed Kubernetes cluster, fabricate the planning resources for the Kubernetes * objects that deployment eventually leaves in {@code spec.destination.namespace}. * * <p>Kept separate from {@link SimulatedKubernetesClientResource} because it is a single, narrow * pattern-match that has nothing to do with the generic "reply to a CR deploy/patch/delete dry-run" * contract that class implements - so each has one reason to change and this can be tested on its * own. {@link SimulatedKubernetesClientResource#deployCR} calls * {@link #reportNamespaceSideEffectResourcesIfApplicable} once, unconditionally; the applicability * check lives here. * * <p>K8aaS pattern: an ArgoCD {@code Application} CR whose Helm chart provisions a * Terraform-controller-backed Kubernetes cluster into {@code spec.destination.namespace} always * ends up populating that same namespace once the real cluster comes up - production's * {@code NamespaceWatcher} would eventually mirror what lands there into the catalog * ({@code KubernetesSecret2OpensliceResource}/{@code KubernetesConfigMap2OpensliceResource}) the * moment it appears for real. Nothing ever appears for real in a simulation, so this class * fabricates that eventual outcome as extra planning resources whenever it recognizes this shape. * * <p>Scoped specifically to {@link #ARGOCD_K8AAS_CHART}, not every ArgoCD {@code Application}: an * Application deploying some other chart (an ordinary workload, say) never provisions a cluster and * so never produces these as a side effect - fabricating them for it would be wrong, not merely * imprecise. */ @Component public class SimulatedArgoCdK8aasResources { private static final Logger logger = LoggerFactory.getLogger( "org.etsi.osl.cridge" ); private static final String ARGOCD_API_VERSION = "argoproj.io/v1alpha1"; private static final String ARGOCD_KIND_APPLICATION = "Application"; private static final String ARGOCD_K8AAS_CHART = "provision-k8s-cluster-argocd-tf-controller"; /** * The resources this chart really does leave behind in {@code spec.destination.namespace}, taken * from the actual catalog entries a completed K8aaS deployment produces: the Terraform controller * writes the {@code kubeconf} and {@code ssh-key-pair} Secrets for the provisioned cluster, and * Kubernetes itself auto-creates the {@code kube-root-ca.crt} ConfigMap in every namespace. Their * <em>names, kinds and data keys</em> are deterministic - which is exactly what makes simulating * them honest; only the data <em>values</em> are unknowable ahead of time, and those are never * fabricated (see {@link #namespacedResourceCharacteristicsFor}). * * <p>Category/description/version mirror what production's own * {@code KubernetesSecret}/{@code KubernetesConfigMap} ({@code org.etsi.osl.model.k8s}) set on the * real catalog entries. */ private static final List<SimulatedNamespacedResource> K8AAS_NAMESPACE_RESOURCES = List.of( new SimulatedNamespacedResource("kubeconf", "Secret", "secrets", "Secret/Kubernetes/v1", "secret", List.of("kubeconf")), new SimulatedNamespacedResource("ssh-key-pair", "Secret", "secrets", "Secret/Kubernetes/v1", "secret", List.of("ssh-privatekey", "ssh-publickey")), new SimulatedNamespacedResource("kube-root-ca.crt", "ConfigMap", "configmaps", "ConfigMap/Kubernetes/v1", "configMap", List.of("ca.crt"))); /** The Kubernetes API version of all of the above - they are all core/v1 objects. */ private static final String K8S_CORE_API_VERSION = "v1"; /** * Stands in for the value of a data key that will only exist once a real deployment has happened. * Deliberately self-describing rather than plausible-looking: a plan may show <em>that</em> a * kubeconfig/keypair/CA bundle will be there, but anything resembling actual credential material * would be a fabrication, and worse, could be mistaken downstream for a usable value. */ private static final String SIMULATED_VALUE_PLACEHOLDER = "(simulated - value exists only after a real deployment)"; /** * One resource a simulated K8aaS deployment leaves behind in the destination namespace. * * @param name the Kubernetes object's own name, e.g. {@code kubeconf} * @param kind {@code Secret} / {@code ConfigMap} * @param apiGroup the plural form production reports as the {@code apiGroup} characteristic * @param category the TMF Resource category production assigns, e.g. {@code Secret/Kubernetes/v1} * @param descriptionNoun how production words this kind in its description ("secret"/"configMap") * @param dataKeys the object's own {@code data} keys, which production mirrors one-to-one as * extra characteristics - the part that genuinely differs between these three * ({@code kubeconf}; {@code ssh-privatekey}/{@code ssh-publickey}; * {@code ca.crt}), fixed by what the chart and Kubernetes always write */ private record SimulatedNamespacedResource(String name, String kind, String apiGroup, String category, String descriptionNoun, List<String> dataKeys) { } @Autowired private PlanningCallbackClient planningCallbackClient; @Value("${cridge.simulation.simulated-deploy-delay-ms:500}") private long simulatedDeployDelayMs; private final ExecutorService callbackExecutor = Executors.newCachedThreadPool(); /** * Reports the resources listed in {@link #K8AAS_NAMESPACE_RESOURCES} - one planning resource each, * named/described/categorized exactly as the real catalog entries will be once the deployment * happens for real - but only when {@code gkr} is an ArgoCD {@code Application} using the K8aaS * chart. Any other resource is a no-op. * * <p>Deploy-only, deliberately: a callback with no {@code org.etsi.osl.resourceId} always creates a * brand-new {@code ExpectedResource} in OSOM's plan (see * {@code org.etsi.osl.osom.tentative.PlanningResourceCallbackHandler}), so firing this on every * {@code patchCR} of the same Application would keep inventing duplicate rows rather than updating * the ones already there. {@code deleteCR} is left alone too - the callback contract is * create/update only, there is no teardown signal for a resource that was never really tracked. */ public void reportNamespaceSideEffectResourcesIfApplicable(Map<String, Object> headers, GenericKubernetesResource gkr) { if (!isArgoCdApplication(gkr) || !isProvisionK8sClusterChart(gkr)) { return; } Object namespaceObj = gkr.get("spec", "destination", "namespace"); if (namespaceObj == null || namespaceObj.toString().isBlank()) { logger.debug("ArgoCD Application {} has no spec.destination.namespace - not simulating the " + "resources it would leave there", appNameOf(gkr)); return; } String destinationNamespace = namespaceObj.toString(); String cluster = str(headers, "currentContextCluster"); String masterURL = str(headers, "clusterMasterURL"); String serviceOrderId = str(headers, "org.etsi.osl.serviceOrderId"); String serviceId = str(headers, "org.etsi.osl.serviceId"); callbackExecutor.submit(() -> { // Same timing as the Application's own callback #2: these only exist, for real, once the // cluster the Application deploys is actually up. sleepQuietly(simulatedDeployDelayMs); for (SimulatedNamespacedResource r : K8AAS_NAMESPACE_RESOURCES) { planningCallbackClient.reportSideEffectResource(serviceOrderId, serviceId, namespacedResourceNameFor(r.name(), destinationNamespace, cluster, masterURL), r.category(), namespacedResourceDescriptionFor(r, destinationNamespace, cluster, masterURL), K8S_CORE_API_VERSION, namespacedResourceCharacteristicsFor(headers, destinationNamespace, r)); } }); } /** * Mirrors the characteristic <em>names</em> production's real * {@code KubernetesSecret.toResourceCreate()}/{@code KubernetesConfigMap.toResourceCreate()} would * populate (see {@code NamespaceWatcher}) - including each object's own data keys, which are the * part that genuinely differs between the three ({@code kubeconf}; {@code ssh-privatekey}/ * {@code ssh-publickey}; {@code ca.crt}) and are just as deterministic as the object names * themselves. * * <p>Their <em>values</em>, though, get {@link #SIMULATED_VALUE_PLACEHOLDER}: nothing real is ever * created in a simulation, so emitting credential-shaped content would be actively misleading rather * than merely a placeholder. For the same reason the whole-object dumps ({@code data}, {@code json}, * {@code metadata}) are left out entirely - unlike a key name, there is no honest partial form of * them - as is {@code UID}, which a real object only gets from the Kubernetes API server at creation * time. * * <p>The {@code org.etsi.osl.statusCheckFieldName} / {@code *CheckValue*} state-check characteristics, and * {@code org.etsi.osl.resourceId} (the Application's own resourceId - production's real K8aaS * secrets carry this same id, not a distinct id of the secret's own), are genuinely available, not * fabricated: OSOM sends them as headers on the Application's own deploy request (see the * class-level architecture note) - so echoing whatever the request actually carried is honest * propagation, same as {@code currentContextCluster} below. */ private Map<String, String> namespacedResourceCharacteristicsFor(Map<String, Object> headers, String namespace, SimulatedNamespacedResource resource) { Map<String, String> characteristics = new LinkedHashMap<>(); characteristics.put("Kind", resource.kind()); characteristics.put("apiGroup", resource.apiGroup()); characteristics.put("org.etsi.osl.namespace", namespace); characteristics.put("fullResourceName", ""); putIfPresent(characteristics, "currentContextCluster", str(headers, "currentContextCluster")); putIfPresent(characteristics, "clusterMasterURL", str(headers, "clusterMasterURL")); putIfPresent(characteristics, "org.etsi.osl.serviceOrderId", str(headers, "org.etsi.osl.serviceOrderId")); putIfPresent(characteristics, "org.etsi.osl.serviceId", str(headers, "org.etsi.osl.serviceId")); putIfPresent(characteristics, "org.etsi.osl.prefixName", str(headers, "org.etsi.osl.prefixName")); // The Application's own resourceId header, not the (deliberately null) top-level resourceId param // above - production's real K8aaS secrets carry this same id as a characteristic (it is the // Application/cluster resource's id, not a distinct id of the secret's own), so echo it here too. putIfPresent(characteristics, "org.etsi.osl.resourceId", str(headers, "org.etsi.osl.resourceId")); putIfPresent(characteristics, "org.etsi.osl.statusCheckFieldName", str(headers, "org.etsi.osl.statusCheckFieldName")); putIfPresent(characteristics, "org.etsi.osl.statusCheckValueStandby", str(headers, "org.etsi.osl.statusCheckValueStandby")); putIfPresent(characteristics, "org.etsi.osl.statusCheckValueAlarm", str(headers, "org.etsi.osl.statusCheckValueAlarm")); putIfPresent(characteristics, "org.etsi.osl.statusCheckValueAvailable", str(headers, "org.etsi.osl.statusCheckValueAvailable")); putIfPresent(characteristics, "org.etsi.osl.statusCheckValueReserved", str(headers, "org.etsi.osl.statusCheckValueReserved")); putIfPresent(characteristics, "org.etsi.osl.statusCheckValueUnknown", str(headers, "org.etsi.osl.statusCheckValueUnknown")); putIfPresent(characteristics, "org.etsi.osl.statusCheckValueSuspended", str(headers, "org.etsi.osl.statusCheckValueSuspended")); // ITU-T X.731 state / health check values (production's real K8aaS secrets carry these too). for (String checkValueHeader : new String[] { "org.etsi.osl.healthCheckValueUp", "org.etsi.osl.healthCheckValuePending", "org.etsi.osl.healthCheckValueDown", "org.etsi.osl.healthCheckValueHeld", "org.etsi.osl.healthCheckValueGone", "org.etsi.osl.operStateCheckValueEnable", "org.etsi.osl.operStateCheckValueDisable", "org.etsi.osl.adminStateCheckValueLocked", "org.etsi.osl.adminStateCheckValueUnlocked", "org.etsi.osl.adminStateCheckValueShutdown", "org.etsi.osl.usageStateCheckValueIdle", "org.etsi.osl.usageStateCheckValueActive", "org.etsi.osl.usageStateCheckValueBusy" }) { putIfPresent(characteristics, checkValueHeader, str(headers, checkValueHeader)); } // Last, as production does: the object's own data keys, one characteristic each. resource.dataKeys().forEach(key -> characteristics.put(key, SIMULATED_VALUE_PLACEHOLDER)); return characteristics; } private static void putIfPresent(Map<String, String> map, String key, String value) { if (value != null && !value.isBlank()) { map.put(key, value); } } private boolean isArgoCdApplication(GenericKubernetesResource gkr) { return ARGOCD_API_VERSION.equals(gkr.getApiVersion()) && ARGOCD_KIND_APPLICATION.equals(gkr.getKind()); } /** * See {@link #ARGOCD_K8AAS_CHART}. Reads {@code spec.source.chart} via fabric8's nested-path * accessor rather than a raw-text search over {@code crspec} - the CR is already parsed for the * {@code apiVersion}/{@code kind} check above, and matching the actual field is robust to * formatting (quoting, spacing) that a plain substring search over the raw YAML/JSON would be * sensitive to. */ private boolean isProvisionK8sClusterChart(GenericKubernetesResource gkr) { Object chart = gkr.get("spec", "source", "chart"); return chart != null && ARGOCD_K8AAS_CHART.equals(chart.toString()); } private static String appNameOf(GenericKubernetesResource gkr) { return gkr.getMetadata() != null && gkr.getMetadata().getName() != null ? gkr.getMetadata().getName() : "argocd-application"; } /** * Reproduces production's naming for a mirrored namespaced resource exactly (see * {@code NamespaceWatcher}: {@code "{objectName}@{namespace}@{cluster}@{masterURL}"}), so a plan's * row carries the same name the real catalog entry will have once the deployment happens for real. */ private static String namespacedResourceNameFor(String objectName, String namespace, String cluster, String masterURL) { StringBuilder name = new StringBuilder(objectName).append('@').append(namespace); if (cluster != null && !cluster.isBlank()) { name.append('@').append(cluster); } if (masterURL != null && !masterURL.isBlank()) { name.append('@').append(masterURL); } return name.toString(); } /** * Reproduces production's description wording for a mirrored namespaced resource (see * {@code NamespaceWatcher}: {@code "A secret in namespace %s on %s"} over a * {@code "{Kind}@{apiVersion}@{cluster}@{masterURL}"} base). */ private static String namespacedResourceDescriptionFor(SimulatedNamespacedResource resource, String namespace, String cluster, String masterURL) { String base = String.format("%s@%s@%s@%s", resource.kind(), K8S_CORE_API_VERSION, cluster != null ? cluster : "", masterURL != null ? masterURL : ""); return String.format("A %s in namespace %s on %s", resource.descriptionNoun(), namespace, base); } private static String str(Map<String, Object> headers, String key) { Object v = headers.get(key); return v != null ? v.toString() : null; } private static void sleepQuietly(long millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } Loading
src/main/java/org/etsi/osl/cridge/simulation/SimulatedArgoCdK8aasResources.java 0 → 100644 +292 −0 Original line number Diff line number Diff line package org.etsi.osl.cridge.simulation; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import io.fabric8.kubernetes.api.model.GenericKubernetesResource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; /** * The ArgoCD K8aaS special case carved out of {@link SimulatedKubernetesClientResource}: given a * simulated deploy of an ArgoCD {@code Application} CR whose Helm chart provisions a * Terraform-controller-backed Kubernetes cluster, fabricate the planning resources for the Kubernetes * objects that deployment eventually leaves in {@code spec.destination.namespace}. * * <p>Kept separate from {@link SimulatedKubernetesClientResource} because it is a single, narrow * pattern-match that has nothing to do with the generic "reply to a CR deploy/patch/delete dry-run" * contract that class implements - so each has one reason to change and this can be tested on its * own. {@link SimulatedKubernetesClientResource#deployCR} calls * {@link #reportNamespaceSideEffectResourcesIfApplicable} once, unconditionally; the applicability * check lives here. * * <p>K8aaS pattern: an ArgoCD {@code Application} CR whose Helm chart provisions a * Terraform-controller-backed Kubernetes cluster into {@code spec.destination.namespace} always * ends up populating that same namespace once the real cluster comes up - production's * {@code NamespaceWatcher} would eventually mirror what lands there into the catalog * ({@code KubernetesSecret2OpensliceResource}/{@code KubernetesConfigMap2OpensliceResource}) the * moment it appears for real. Nothing ever appears for real in a simulation, so this class * fabricates that eventual outcome as extra planning resources whenever it recognizes this shape. * * <p>Scoped specifically to {@link #ARGOCD_K8AAS_CHART}, not every ArgoCD {@code Application}: an * Application deploying some other chart (an ordinary workload, say) never provisions a cluster and * so never produces these as a side effect - fabricating them for it would be wrong, not merely * imprecise. */ @Component public class SimulatedArgoCdK8aasResources { private static final Logger logger = LoggerFactory.getLogger( "org.etsi.osl.cridge" ); private static final String ARGOCD_API_VERSION = "argoproj.io/v1alpha1"; private static final String ARGOCD_KIND_APPLICATION = "Application"; private static final String ARGOCD_K8AAS_CHART = "provision-k8s-cluster-argocd-tf-controller"; /** * The resources this chart really does leave behind in {@code spec.destination.namespace}, taken * from the actual catalog entries a completed K8aaS deployment produces: the Terraform controller * writes the {@code kubeconf} and {@code ssh-key-pair} Secrets for the provisioned cluster, and * Kubernetes itself auto-creates the {@code kube-root-ca.crt} ConfigMap in every namespace. Their * <em>names, kinds and data keys</em> are deterministic - which is exactly what makes simulating * them honest; only the data <em>values</em> are unknowable ahead of time, and those are never * fabricated (see {@link #namespacedResourceCharacteristicsFor}). * * <p>Category/description/version mirror what production's own * {@code KubernetesSecret}/{@code KubernetesConfigMap} ({@code org.etsi.osl.model.k8s}) set on the * real catalog entries. */ private static final List<SimulatedNamespacedResource> K8AAS_NAMESPACE_RESOURCES = List.of( new SimulatedNamespacedResource("kubeconf", "Secret", "secrets", "Secret/Kubernetes/v1", "secret", List.of("kubeconf")), new SimulatedNamespacedResource("ssh-key-pair", "Secret", "secrets", "Secret/Kubernetes/v1", "secret", List.of("ssh-privatekey", "ssh-publickey")), new SimulatedNamespacedResource("kube-root-ca.crt", "ConfigMap", "configmaps", "ConfigMap/Kubernetes/v1", "configMap", List.of("ca.crt"))); /** The Kubernetes API version of all of the above - they are all core/v1 objects. */ private static final String K8S_CORE_API_VERSION = "v1"; /** * Stands in for the value of a data key that will only exist once a real deployment has happened. * Deliberately self-describing rather than plausible-looking: a plan may show <em>that</em> a * kubeconfig/keypair/CA bundle will be there, but anything resembling actual credential material * would be a fabrication, and worse, could be mistaken downstream for a usable value. */ private static final String SIMULATED_VALUE_PLACEHOLDER = "(simulated - value exists only after a real deployment)"; /** * One resource a simulated K8aaS deployment leaves behind in the destination namespace. * * @param name the Kubernetes object's own name, e.g. {@code kubeconf} * @param kind {@code Secret} / {@code ConfigMap} * @param apiGroup the plural form production reports as the {@code apiGroup} characteristic * @param category the TMF Resource category production assigns, e.g. {@code Secret/Kubernetes/v1} * @param descriptionNoun how production words this kind in its description ("secret"/"configMap") * @param dataKeys the object's own {@code data} keys, which production mirrors one-to-one as * extra characteristics - the part that genuinely differs between these three * ({@code kubeconf}; {@code ssh-privatekey}/{@code ssh-publickey}; * {@code ca.crt}), fixed by what the chart and Kubernetes always write */ private record SimulatedNamespacedResource(String name, String kind, String apiGroup, String category, String descriptionNoun, List<String> dataKeys) { } @Autowired private PlanningCallbackClient planningCallbackClient; @Value("${cridge.simulation.simulated-deploy-delay-ms:500}") private long simulatedDeployDelayMs; private final ExecutorService callbackExecutor = Executors.newCachedThreadPool(); /** * Reports the resources listed in {@link #K8AAS_NAMESPACE_RESOURCES} - one planning resource each, * named/described/categorized exactly as the real catalog entries will be once the deployment * happens for real - but only when {@code gkr} is an ArgoCD {@code Application} using the K8aaS * chart. Any other resource is a no-op. * * <p>Deploy-only, deliberately: a callback with no {@code org.etsi.osl.resourceId} always creates a * brand-new {@code ExpectedResource} in OSOM's plan (see * {@code org.etsi.osl.osom.tentative.PlanningResourceCallbackHandler}), so firing this on every * {@code patchCR} of the same Application would keep inventing duplicate rows rather than updating * the ones already there. {@code deleteCR} is left alone too - the callback contract is * create/update only, there is no teardown signal for a resource that was never really tracked. */ public void reportNamespaceSideEffectResourcesIfApplicable(Map<String, Object> headers, GenericKubernetesResource gkr) { if (!isArgoCdApplication(gkr) || !isProvisionK8sClusterChart(gkr)) { return; } Object namespaceObj = gkr.get("spec", "destination", "namespace"); if (namespaceObj == null || namespaceObj.toString().isBlank()) { logger.debug("ArgoCD Application {} has no spec.destination.namespace - not simulating the " + "resources it would leave there", appNameOf(gkr)); return; } String destinationNamespace = namespaceObj.toString(); String cluster = str(headers, "currentContextCluster"); String masterURL = str(headers, "clusterMasterURL"); String serviceOrderId = str(headers, "org.etsi.osl.serviceOrderId"); String serviceId = str(headers, "org.etsi.osl.serviceId"); callbackExecutor.submit(() -> { // Same timing as the Application's own callback #2: these only exist, for real, once the // cluster the Application deploys is actually up. sleepQuietly(simulatedDeployDelayMs); for (SimulatedNamespacedResource r : K8AAS_NAMESPACE_RESOURCES) { planningCallbackClient.reportSideEffectResource(serviceOrderId, serviceId, namespacedResourceNameFor(r.name(), destinationNamespace, cluster, masterURL), r.category(), namespacedResourceDescriptionFor(r, destinationNamespace, cluster, masterURL), K8S_CORE_API_VERSION, namespacedResourceCharacteristicsFor(headers, destinationNamespace, r)); } }); } /** * Mirrors the characteristic <em>names</em> production's real * {@code KubernetesSecret.toResourceCreate()}/{@code KubernetesConfigMap.toResourceCreate()} would * populate (see {@code NamespaceWatcher}) - including each object's own data keys, which are the * part that genuinely differs between the three ({@code kubeconf}; {@code ssh-privatekey}/ * {@code ssh-publickey}; {@code ca.crt}) and are just as deterministic as the object names * themselves. * * <p>Their <em>values</em>, though, get {@link #SIMULATED_VALUE_PLACEHOLDER}: nothing real is ever * created in a simulation, so emitting credential-shaped content would be actively misleading rather * than merely a placeholder. For the same reason the whole-object dumps ({@code data}, {@code json}, * {@code metadata}) are left out entirely - unlike a key name, there is no honest partial form of * them - as is {@code UID}, which a real object only gets from the Kubernetes API server at creation * time. * * <p>The {@code org.etsi.osl.statusCheckFieldName} / {@code *CheckValue*} state-check characteristics, and * {@code org.etsi.osl.resourceId} (the Application's own resourceId - production's real K8aaS * secrets carry this same id, not a distinct id of the secret's own), are genuinely available, not * fabricated: OSOM sends them as headers on the Application's own deploy request (see the * class-level architecture note) - so echoing whatever the request actually carried is honest * propagation, same as {@code currentContextCluster} below. */ private Map<String, String> namespacedResourceCharacteristicsFor(Map<String, Object> headers, String namespace, SimulatedNamespacedResource resource) { Map<String, String> characteristics = new LinkedHashMap<>(); characteristics.put("Kind", resource.kind()); characteristics.put("apiGroup", resource.apiGroup()); characteristics.put("org.etsi.osl.namespace", namespace); characteristics.put("fullResourceName", ""); putIfPresent(characteristics, "currentContextCluster", str(headers, "currentContextCluster")); putIfPresent(characteristics, "clusterMasterURL", str(headers, "clusterMasterURL")); putIfPresent(characteristics, "org.etsi.osl.serviceOrderId", str(headers, "org.etsi.osl.serviceOrderId")); putIfPresent(characteristics, "org.etsi.osl.serviceId", str(headers, "org.etsi.osl.serviceId")); putIfPresent(characteristics, "org.etsi.osl.prefixName", str(headers, "org.etsi.osl.prefixName")); // The Application's own resourceId header, not the (deliberately null) top-level resourceId param // above - production's real K8aaS secrets carry this same id as a characteristic (it is the // Application/cluster resource's id, not a distinct id of the secret's own), so echo it here too. putIfPresent(characteristics, "org.etsi.osl.resourceId", str(headers, "org.etsi.osl.resourceId")); putIfPresent(characteristics, "org.etsi.osl.statusCheckFieldName", str(headers, "org.etsi.osl.statusCheckFieldName")); putIfPresent(characteristics, "org.etsi.osl.statusCheckValueStandby", str(headers, "org.etsi.osl.statusCheckValueStandby")); putIfPresent(characteristics, "org.etsi.osl.statusCheckValueAlarm", str(headers, "org.etsi.osl.statusCheckValueAlarm")); putIfPresent(characteristics, "org.etsi.osl.statusCheckValueAvailable", str(headers, "org.etsi.osl.statusCheckValueAvailable")); putIfPresent(characteristics, "org.etsi.osl.statusCheckValueReserved", str(headers, "org.etsi.osl.statusCheckValueReserved")); putIfPresent(characteristics, "org.etsi.osl.statusCheckValueUnknown", str(headers, "org.etsi.osl.statusCheckValueUnknown")); putIfPresent(characteristics, "org.etsi.osl.statusCheckValueSuspended", str(headers, "org.etsi.osl.statusCheckValueSuspended")); // ITU-T X.731 state / health check values (production's real K8aaS secrets carry these too). for (String checkValueHeader : new String[] { "org.etsi.osl.healthCheckValueUp", "org.etsi.osl.healthCheckValuePending", "org.etsi.osl.healthCheckValueDown", "org.etsi.osl.healthCheckValueHeld", "org.etsi.osl.healthCheckValueGone", "org.etsi.osl.operStateCheckValueEnable", "org.etsi.osl.operStateCheckValueDisable", "org.etsi.osl.adminStateCheckValueLocked", "org.etsi.osl.adminStateCheckValueUnlocked", "org.etsi.osl.adminStateCheckValueShutdown", "org.etsi.osl.usageStateCheckValueIdle", "org.etsi.osl.usageStateCheckValueActive", "org.etsi.osl.usageStateCheckValueBusy" }) { putIfPresent(characteristics, checkValueHeader, str(headers, checkValueHeader)); } // Last, as production does: the object's own data keys, one characteristic each. resource.dataKeys().forEach(key -> characteristics.put(key, SIMULATED_VALUE_PLACEHOLDER)); return characteristics; } private static void putIfPresent(Map<String, String> map, String key, String value) { if (value != null && !value.isBlank()) { map.put(key, value); } } private boolean isArgoCdApplication(GenericKubernetesResource gkr) { return ARGOCD_API_VERSION.equals(gkr.getApiVersion()) && ARGOCD_KIND_APPLICATION.equals(gkr.getKind()); } /** * See {@link #ARGOCD_K8AAS_CHART}. Reads {@code spec.source.chart} via fabric8's nested-path * accessor rather than a raw-text search over {@code crspec} - the CR is already parsed for the * {@code apiVersion}/{@code kind} check above, and matching the actual field is robust to * formatting (quoting, spacing) that a plain substring search over the raw YAML/JSON would be * sensitive to. */ private boolean isProvisionK8sClusterChart(GenericKubernetesResource gkr) { Object chart = gkr.get("spec", "source", "chart"); return chart != null && ARGOCD_K8AAS_CHART.equals(chart.toString()); } private static String appNameOf(GenericKubernetesResource gkr) { return gkr.getMetadata() != null && gkr.getMetadata().getName() != null ? gkr.getMetadata().getName() : "argocd-application"; } /** * Reproduces production's naming for a mirrored namespaced resource exactly (see * {@code NamespaceWatcher}: {@code "{objectName}@{namespace}@{cluster}@{masterURL}"}), so a plan's * row carries the same name the real catalog entry will have once the deployment happens for real. */ private static String namespacedResourceNameFor(String objectName, String namespace, String cluster, String masterURL) { StringBuilder name = new StringBuilder(objectName).append('@').append(namespace); if (cluster != null && !cluster.isBlank()) { name.append('@').append(cluster); } if (masterURL != null && !masterURL.isBlank()) { name.append('@').append(masterURL); } return name.toString(); } /** * Reproduces production's description wording for a mirrored namespaced resource (see * {@code NamespaceWatcher}: {@code "A secret in namespace %s on %s"} over a * {@code "{Kind}@{apiVersion}@{cluster}@{masterURL}"} base). */ private static String namespacedResourceDescriptionFor(SimulatedNamespacedResource resource, String namespace, String cluster, String masterURL) { String base = String.format("%s@%s@%s@%s", resource.kind(), K8S_CORE_API_VERSION, cluster != null ? cluster : "", masterURL != null ? masterURL : ""); return String.format("A %s in namespace %s on %s", resource.descriptionNoun(), namespace, base); } private static String str(Map<String, Object> headers, String key) { Object v = headers.get(key); return v != null ? v.toString() : null; } private static void sleepQuietly(long millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }