diff --git a/argocd/applications/configs/component-status.yaml b/argocd/applications/configs/component-status.yaml new file mode 100644 index 000000000..87a627c1c --- /dev/null +++ b/argocd/applications/configs/component-status.yaml @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: 2025 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +# Component status service configuration +# This will be populated by the installer based on which features are enabled + +resources: null diff --git a/argocd/applications/custom/component-status.tpl b/argocd/applications/custom/component-status.tpl new file mode 100644 index 000000000..11702d642 --- /dev/null +++ b/argocd/applications/custom/component-status.tpl @@ -0,0 +1,149 @@ +# SPDX-FileCopyrightText: 2025 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +image: + registry: {{.Values.argo.containerRegistryURL }} + repository: common/component-status +imagePullSecrets: + {{- with .Values.argo.imagePullSecrets }} + {{- toYaml . | nindent 2 }} + {{- end }} + +{{- with .Values.argo.resources.componentStatus }} +resources: + {{- toYaml . | nindent 2 }} +{{- end }} + +# Traefik IngressRoute configuration +# Priority must be > 30 to override nexus-api-gw's generic /v route (priority 30) +traefikRoute: + enabled: true + matchHost: "Host(`api.{{ .Values.argo.clusterDomain }}`)" + matchPath: "PathPrefix(`/v1/orchestrator`)" + priority: 40 + namespace: orch-gateway + secretName: tls-orch + middlewares: + - validate-jwt + - secure-headers +{{- if .Values.argo.traefik }} + tlsOption: {{ .Values.argo.traefik.tlsOption | default "" | quote }} +{{- end }} + +# Component status configuration +# This configuration reflects which features are ACTUALLY installed in the orchestrator +# Detection method - Checks which profile files are loaded in root-app +componentStatus: + schema-version: "1.0" + orchestrator: + version: {{ .Values.argo.orchestratorVersion | default .Chart.Version | quote }} + features: + # Application Orchestration - Enabled when app-orch profile is loaded + # Detection - enable-app-orch.yaml in root-app valueFiles + application-orchestration: + installed: {{ index .Values.argo.enabled "app-orch-catalog" | default false }} + + # Cluster Orchestration - Enabled when cluster-orch profile is loaded + # Detection - enable-cluster-orch.yaml in root-app valueFiles + cluster-orchestration: + installed: {{ index .Values.argo.enabled "cluster-manager" | default false }} + + # Cluster management core API and lifecycle operations + cluster-management: + installed: {{ index .Values.argo.enabled "cluster-manager" | default false }} + + # CAPI (Cluster API) integration for declarative cluster management + capi: + installed: {{ index .Values.argo.enabled "capi-operator" | default false }} + + # Infrastructure provider for Intel platforms + intel-provider: + installed: {{ index .Values.argo.enabled "intel-infra-provider" | default false }} + + # Edge Infrastructure Manager - Enabled when edge-infra profile is loaded + # Detection - enable-edgeinfra.yaml in root-app valueFiles + # EIM is NOT broken down at app level (all workflows need core+managers+onboarding) + # Instead, different APIs/managers/configs enable different workflow-level capabilities + # Hierarchical fallback - CLI checks sub-feature first, falls back to parent if not found + edge-infrastructure-manager: + installed: {{ or (index .Values.argo.enabled "infra-core") (index .Values.argo.enabled "infra-managers") (index .Values.argo.enabled "infra-onboarding") (index .Values.argo.enabled "infra-external") | default false }} + + # Day2 - Day 2 operations (maintenance, updates, troubleshooting) + # Detection - maintenance-manager is configured in infra-managers + day2: + installed: {{ if hasKey .Values.argo "infra-managers" }}{{ $infraManagers := index .Values.argo "infra-managers" }}{{ if hasKey $infraManagers "maintenance-manager" }}true{{ else }}false{{ end }}{{ else }}false{{ end }} + + # Onboarding - Device discovery, registration, and enrollment workflow + # Detection - onboarding-manager is configured and enabled in infra-onboarding + onboarding: + installed: {{ if hasKey .Values.argo "infra-onboarding" }}{{ $infraOnboarding := index .Values.argo "infra-onboarding" }}{{ if hasKey $infraOnboarding "onboarding-manager" }}{{ $onboardingMgr := index $infraOnboarding "onboarding-manager" }}{{ $onboardingMgr.enabled | default false }}{{ else }}false{{ end }}{{ else }}false{{ end }} + + # OOB (Out-of-Band) - vPRO/AMT management capabilities + # Detection - AMT is configured in infra-external (vPRO/AMT managers deployed) + oob: + installed: {{ if and (index .Values.argo.enabled "infra-external" | default false) (hasKey .Values.argo "infra-external") }}{{ $infraExternal := index .Values.argo "infra-external" }}{{ if hasKey $infraExternal "import" }}{{ if hasKey $infraExternal.import "amt" }}{{ $infraExternal.import.amt.enabled | default false }}{{ else }}false{{ end }}{{ else }}false{{ end }}{{ else }}false{{ end }} + + # Provisioning - Automatic OS provisioning workflow + # Detection - autoProvision is enabled in infra-managers (os-resource-manager handles automatic provisioning) + provisioning: + installed: {{ if and (index .Values.argo.enabled "infra-managers" | default false) (hasKey .Values.argo "infra-managers") }}{{ $infraManagers := index .Values.argo "infra-managers" }}{{ if hasKey $infraManagers "autoProvision" }}{{ $infraManagers.autoProvision.enabled | default false }}{{ else }}false{{ end }}{{ else }}false{{ end }} + + # Observability - Enabled when o11y profile is loaded + # Detection - enable-o11y.yaml in root-app valueFiles + observability: + installed: {{ index .Values.argo.enabled "orchestrator-observability" | default false }} + + # Metrics collection and monitoring for orchestrator components + orchestrator-monitoring: + installed: {{ index .Values.argo.enabled "orchestrator-observability" | default false }} + + # Metrics collection and monitoring for edge nodes + edge-node-monitoring: + installed: {{ index .Values.argo.enabled "edgenode-observability" | default false }} + + # Pre-built dashboards for orchestrator metrics + orchestrator-dashboards: + installed: {{ index .Values.argo.enabled "orchestrator-dashboards" | default false }} + + # Pre-built dashboards for edge node metrics + edge-node-dashboards: + installed: {{ index .Values.argo.enabled "edgenode-dashboards" | default false }} + + # Alerting and monitoring rules + alerting: + installed: {{ index .Values.argo.enabled "alerting-monitor" | default false }} + + # Web UI - Enabled when full-ui profile is loaded + # Detection - enable-full-ui.yaml in root-app valueFiles + web-ui: + installed: {{ or (index .Values.argo.enabled "web-ui-root") (index .Values.argo.enabled "web-ui-app-orch") (index .Values.argo.enabled "web-ui-cluster-orch") (index .Values.argo.enabled "web-ui-infra") | default false }} + orchestrator-ui-root: + installed: {{ index .Values.argo.enabled "web-ui-root" | default false }} + application-orchestration-ui: + installed: {{ index .Values.argo.enabled "web-ui-app-orch" | default false }} + cluster-orchestration-ui: + installed: {{ index .Values.argo.enabled "web-ui-cluster-orch" | default false }} + infrastructure-ui: + installed: {{ index .Values.argo.enabled "web-ui-infra" | default false }} + + # Multitenancy - Tenancy services (tenancy-manager, tenancy-api-mapping, tenancy-datamodel) + # are always deployed as part of root-app, so multitenancy is always enabled + # The default-tenant-only sub-feature indicates single-tenant mode (when defaultTenancy profile is loaded) + multitenancy: + installed: true + default-tenant-only: + installed: {{ index .Values.argo.enabled "defaultTenancy" | default false }} + + # Kyverno - Policy engine for Kubernetes admission control and governance + # Detection - enable-kyverno.yaml in root-app valueFiles + kyverno: + installed: {{ index .Values.argo.enabled "kyverno" | default false }} + + # Kyverno policy engine core + policy-engine: + installed: {{ index .Values.argo.enabled "kyverno" | default false }} + + # Pre-defined security and governance policies + policies: + installed: {{ index .Values.argo.enabled "kyverno-policy" | default false }} diff --git a/argocd/applications/templates/component-status.yaml b/argocd/applications/templates/component-status.yaml new file mode 100644 index 000000000..d20271868 --- /dev/null +++ b/argocd/applications/templates/component-status.yaml @@ -0,0 +1,52 @@ +# SPDX-FileCopyrightText: 2025 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 + +{{- $appName := "component-status" }} +{{- $namespace := "orch-platform" }} +{{- $syncWave := "2000" }} +--- +{{- if (index .Values.argo.enabled $appName) }} +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + annotations: + argocd.argoproj.io/sync-wave: "{{ $syncWave }}" + name: {{$appName}} + namespace: {{ required "A valid namespace entry required!" .Values.argo.namespace }} + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: {{ required "A valid projectName entry required!" .Values.argo.project }} + sources: + - repoURL: {{ required "A valid chartRepoURL entry required!" .Values.argo.chartRepoURL }} + chart: common/charts/{{$appName}} + targetRevision: 26.0.3 + helm: + releaseName: {{$appName}} + valuesObject: + {{- $customFile := printf "custom/%s.tpl" $appName }} + {{- $customConfig := tpl (.Files.Get $customFile) . | fromYaml }} + {{- $baseFile := printf "configs/%s.yaml" $appName }} + {{- $baseConfig := .Files.Get $baseFile|fromYaml}} + {{- $overwrite := (get .Values.postCustomTemplateOverwrite $appName ) | default dict }} + {{- mergeOverwrite $baseConfig $customConfig $overwrite | toYaml | nindent 10 }} + destination: + namespace: {{$namespace}} + server: {{ required "A valid targetServer entry required!" .Values.argo.targetServer }} + syncPolicy: + {{- if .Values.argo.autosync }} + automated: + prune: true + selfHeal: true + retry: + limit: 5 + backoff: + duration: 5s + maxDuration: 3m0s + factor: 2 + {{- end }} + syncOptions: + - CreateNamespace=true + - ApplyOutOfSyncOnly=true +{{- end }} diff --git a/e2e-tests/orchestrator/component_status_test.go b/e2e-tests/orchestrator/component_status_test.go new file mode 100644 index 000000000..726140af8 --- /dev/null +++ b/e2e-tests/orchestrator/component_status_test.go @@ -0,0 +1,525 @@ +// SPDX-FileCopyrightText: 2025 Intel Corporation +// +// SPDX-License-Identifier: Apache-2.0 + +package orchestrator_test + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +const ( + componentStatusLabel = "component-status" +) + +type ComponentStatus struct { + SchemaVersion string `json:"schema-version"` + Orchestrator OrchestratorStatus `json:"orchestrator"` +} + +type OrchestratorStatus struct { + Version string `json:"version"` + Features map[string]Feature `json:"features"` +} + +type Feature struct { + Installed bool `json:"installed"` + SubFeatures map[string]Feature `json:",inline"` +} + +var _ = Describe("Component Status Service", Label(componentStatusLabel), func() { + var cli *http.Client + var token string + + BeforeEach(func() { + cli = &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: tlsConfig, + }, + } + fmt.Printf("serviceDomain: %v\n", serviceDomain) + // Get authentication token - component status contains sensitive information + user := fmt.Sprintf("%s-api-user", "sample-project") + token = getKeycloakJWT(cli, user) + }) + + Describe("Component Status API", Label(componentStatusLabel), func() { + componentStatusURL := "https://api." + serviceDomainWithPort + "/v1/orchestrator" + + It("should be accessible over HTTPS with valid authentication", func() { + req, err := http.NewRequest(http.MethodGet, componentStatusURL, nil) + Expect(err).ToNot(HaveOccurred()) + req.Header.Add("Authorization", "Bearer "+token) + + resp, err := cli.Do(req) + Expect(err).ToNot(HaveOccurred()) + defer resp.Body.Close() + + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + }) + + It("should return valid JSON with correct schema", func() { + req, err := http.NewRequest(http.MethodGet, componentStatusURL, nil) + Expect(err).ToNot(HaveOccurred()) + req.Header.Add("Authorization", "Bearer "+token) + + resp, err := cli.Do(req) + Expect(err).ToNot(HaveOccurred()) + defer resp.Body.Close() + + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + + body, err := io.ReadAll(resp.Body) + Expect(err).ToNot(HaveOccurred()) + + var status ComponentStatus + err = json.Unmarshal(body, &status) + Expect(err).ToNot(HaveOccurred()) + + // Verify schema version is present + Expect(status.SchemaVersion).ToNot(BeEmpty()) + Expect(status.SchemaVersion).To(Equal("1.0")) + + // Verify orchestrator section exists + Expect(status.Orchestrator.Version).ToNot(BeEmpty()) + + // Verify features section exists + Expect(status.Orchestrator.Features).ToNot(BeNil()) + }) + + It("should return expected feature flags", func() { + req, err := http.NewRequest(http.MethodGet, componentStatusURL, nil) + Expect(err).ToNot(HaveOccurred()) + req.Header.Add("Authorization", "Bearer "+token) + + resp, err := cli.Do(req) + Expect(err).ToNot(HaveOccurred()) + defer resp.Body.Close() + + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + + body, err := io.ReadAll(resp.Body) + Expect(err).ToNot(HaveOccurred()) + + var status ComponentStatus + err = json.Unmarshal(body, &status) + Expect(err).ToNot(HaveOccurred()) + + // Check that expected top-level features are present + expectedFeatures := []string{ + "application-orchestration", + "cluster-orchestration", + "edge-infrastructure-manager", + "observability", + "multitenancy", + "web-ui", + "kyverno", + } + + for _, feature := range expectedFeatures { + _, exists := status.Orchestrator.Features[feature] + Expect(exists).To(BeTrue(), fmt.Sprintf("Feature %s should be present", feature)) + } + + // Verify sub-features for cluster-orchestration + clusterOrch := status.Orchestrator.Features["cluster-orchestration"] + Expect(clusterOrch.SubFeatures).ToNot(BeNil(), "cluster-orchestration should have sub-features") + expectedClusterSubFeatures := []string{"cluster-management", "capi", "intel-provider"} + for _, subFeature := range expectedClusterSubFeatures { + _, exists := clusterOrch.SubFeatures[subFeature] + Expect(exists).To(BeTrue(), fmt.Sprintf("cluster-orchestration sub-feature %s should be present", subFeature)) + } + + // Verify sub-features for observability + observability := status.Orchestrator.Features["observability"] + Expect(observability.SubFeatures).ToNot(BeNil(), "observability should have sub-features") + expectedObservabilitySubFeatures := []string{"orchestrator-monitoring", "edge-node-monitoring", "orchestrator-dashboards", "edge-node-dashboards", "alerting"} + for _, subFeature := range expectedObservabilitySubFeatures { + _, exists := observability.SubFeatures[subFeature] + Expect(exists).To(BeTrue(), fmt.Sprintf("observability sub-feature %s should be present", subFeature)) + } + + // Verify sub-features for kyverno + kyverno := status.Orchestrator.Features["kyverno"] + Expect(kyverno.SubFeatures).ToNot(BeNil(), "kyverno should have sub-features") + expectedKyvernoSubFeatures := []string{"policy-engine", "policies"} + for _, subFeature := range expectedKyvernoSubFeatures { + _, exists := kyverno.SubFeatures[subFeature] + Expect(exists).To(BeTrue(), fmt.Sprintf("kyverno sub-feature %s should be present", subFeature)) + } + }) + + It("should have proper Content-Type header", func() { + req, err := http.NewRequest(http.MethodGet, componentStatusURL, nil) + Expect(err).ToNot(HaveOccurred()) + req.Header.Add("Authorization", "Bearer "+token) + + resp, err := cli.Do(req) + Expect(err).ToNot(HaveOccurred()) + defer resp.Body.Close() + + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + Expect(resp.Header.Get("Content-Type")).To(ContainSubstring("application/json")) + }) + + It("should return 404 for non-existent paths", func() { + req, err := http.NewRequest(http.MethodGet, "https://api."+serviceDomainWithPort+"/v1/orchestrator/nonexistent", nil) + Expect(err).ToNot(HaveOccurred()) + req.Header.Add("Authorization", "Bearer "+token) + + resp, err := cli.Do(req) + Expect(err).ToNot(HaveOccurred()) + defer resp.Body.Close() + + Expect(resp.StatusCode).To(Equal(http.StatusNotFound)) + }) + + It("should support GET method only", func() { + // Test POST should fail + req, err := http.NewRequest(http.MethodPost, componentStatusURL, nil) + Expect(err).ToNot(HaveOccurred()) + req.Header.Add("Authorization", "Bearer "+token) + + resp, err := cli.Do(req) + Expect(err).ToNot(HaveOccurred()) + defer resp.Body.Close() + + Expect(resp.StatusCode).To(Equal(http.StatusMethodNotAllowed)) + }) + }) + + Describe("Health and Readiness endpoints", Label(componentStatusLabel), func() { + It("should have /healthz endpoint", func() { + req, err := http.NewRequest(http.MethodGet, "https://api."+serviceDomainWithPort+"/v1/orchestrator/healthz", nil) + if err != nil { + Skip("Health endpoint may not be exposed externally") + } + + resp, err := cli.Do(req) + if err != nil { + Skip("Health endpoint may not be exposed externally") + } + defer resp.Body.Close() + + // If accessible, should return 200 + if resp.StatusCode == http.StatusOK { + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + } + }) + }) + + Describe("Feature Sub-Feature Validation", Label(componentStatusLabel), func() { + var status ComponentStatus + var componentStatusURL string + + BeforeEach(func() { + componentStatusURL = "https://api." + serviceDomainWithPort + "/v1/orchestrator" + + req, err := http.NewRequest(http.MethodGet, componentStatusURL, nil) + Expect(err).ToNot(HaveOccurred()) + req.Header.Add("Authorization", "Bearer "+token) + + resp, err := cli.Do(req) + Expect(err).ToNot(HaveOccurred()) + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + Expect(err).ToNot(HaveOccurred()) + + err = json.Unmarshal(body, &status) + Expect(err).ToNot(HaveOccurred()) + }) + + Context("Edge Infrastructure Manager workflows", func() { + It("should validate EIM sub-features exist", func() { + eim, exists := status.Orchestrator.Features["edge-infrastructure-manager"] + Expect(exists).To(BeTrue(), "edge-infrastructure-manager feature should exist") + + expectedEIMSubFeatures := []string{"day2", "onboarding", "oob", "provisioning"} + for _, subFeature := range expectedEIMSubFeatures { + _, exists := eim.SubFeatures[subFeature] + Expect(exists).To(BeTrue(), fmt.Sprintf("EIM sub-feature %s should be present", subFeature)) + } + }) + + It("should have day2 workflow detection based on maintenance-manager", func() { + eim := status.Orchestrator.Features["edge-infrastructure-manager"] + day2, exists := eim.SubFeatures["day2"] + Expect(exists).To(BeTrue(), "day2 sub-feature should exist") + // Installed status depends on maintenance-manager deployment + Expect(day2.Installed).To(Or(BeTrue(), BeFalse())) + }) + + It("should have onboarding workflow detection based on onboarding-manager", func() { + eim := status.Orchestrator.Features["edge-infrastructure-manager"] + onboarding, exists := eim.SubFeatures["onboarding"] + Expect(exists).To(BeTrue(), "onboarding sub-feature should exist") + // Installed status depends on onboarding-manager.enabled + Expect(onboarding.Installed).To(Or(BeTrue(), BeFalse())) + }) + + It("should have oob workflow detection based on AMT managers", func() { + eim := status.Orchestrator.Features["edge-infrastructure-manager"] + oob, exists := eim.SubFeatures["oob"] + Expect(exists).To(BeTrue(), "oob sub-feature should exist") + // Installed status depends on infra-external.amt configuration + Expect(oob.Installed).To(Or(BeTrue(), BeFalse())) + }) + + It("should have provisioning workflow detection based on autoProvision configuration", func() { + eim := status.Orchestrator.Features["edge-infrastructure-manager"] + provisioning, exists := eim.SubFeatures["provisioning"] + Expect(exists).To(BeTrue(), "provisioning sub-feature should exist") + // Installed status depends on infra-managers.autoProvision.enabled + Expect(provisioning.Installed).To(Or(BeTrue(), BeFalse())) + }) + + It("should allow onboarding and provisioning to be independent", func() { + eim := status.Orchestrator.Features["edge-infrastructure-manager"] + onboarding := eim.SubFeatures["onboarding"] + provisioning := eim.SubFeatures["provisioning"] + + // Onboarding can be enabled without provisioning + // This validates they are truly independent workflows + if onboarding.Installed && !provisioning.Installed { + // Manual device registration without auto-provisioning + Expect(true).To(BeTrue()) + } + if !onboarding.Installed && provisioning.Installed { + // Auto-provisioning without manual registration + Expect(true).To(BeTrue()) + } + }) + }) + + Context("Cluster Orchestration capabilities", func() { + It("should validate cluster-orch sub-features exist", func() { + clusterOrch, exists := status.Orchestrator.Features["cluster-orchestration"] + Expect(exists).To(BeTrue(), "cluster-orchestration feature should exist") + + expectedSubFeatures := []string{"cluster-management", "capi", "intel-provider"} + for _, subFeature := range expectedSubFeatures { + _, exists := clusterOrch.SubFeatures[subFeature] + Expect(exists).To(BeTrue(), fmt.Sprintf("cluster-orch sub-feature %s should be present", subFeature)) + } + }) + + It("should detect cluster-management based on cluster-manager", func() { + clusterOrch := status.Orchestrator.Features["cluster-orchestration"] + clusterMgmt, exists := clusterOrch.SubFeatures["cluster-management"] + Expect(exists).To(BeTrue(), "cluster-management sub-feature should exist") + + // If cluster-orch is installed, cluster-management should match + if clusterOrch.Installed { + Expect(clusterMgmt.Installed).To(Equal(clusterOrch.Installed)) + } + }) + + It("should detect CAPI integration", func() { + clusterOrch := status.Orchestrator.Features["cluster-orchestration"] + capi, exists := clusterOrch.SubFeatures["capi"] + Expect(exists).To(BeTrue(), "capi sub-feature should exist") + Expect(capi.Installed).To(Or(BeTrue(), BeFalse())) + }) + + It("should detect Intel infrastructure provider", func() { + clusterOrch := status.Orchestrator.Features["cluster-orchestration"] + intelProvider, exists := clusterOrch.SubFeatures["intel-provider"] + Expect(exists).To(BeTrue(), "intel-provider sub-feature should exist") + Expect(intelProvider.Installed).To(Or(BeTrue(), BeFalse())) + }) + }) + + Context("Observability monitoring capabilities", func() { + It("should validate observability sub-features exist", func() { + obs, exists := status.Orchestrator.Features["observability"] + Expect(exists).To(BeTrue(), "observability feature should exist") + + expectedSubFeatures := []string{ + "orchestrator-monitoring", + "edge-node-monitoring", + "orchestrator-dashboards", + "edge-node-dashboards", + "alerting", + } + for _, subFeature := range expectedSubFeatures { + _, exists := obs.SubFeatures[subFeature] + Expect(exists).To(BeTrue(), fmt.Sprintf("observability sub-feature %s should be present", subFeature)) + } + }) + + It("should detect orchestrator monitoring independently from edge node monitoring", func() { + obs := status.Orchestrator.Features["observability"] + orchMon := obs.SubFeatures["orchestrator-monitoring"] + edgeMon := obs.SubFeatures["edge-node-monitoring"] + + // These can be enabled independently + Expect(orchMon.Installed).To(Or(BeTrue(), BeFalse())) + Expect(edgeMon.Installed).To(Or(BeTrue(), BeFalse())) + }) + + It("should detect dashboard availability", func() { + obs := status.Orchestrator.Features["observability"] + orchDash := obs.SubFeatures["orchestrator-dashboards"] + edgeDash := obs.SubFeatures["edge-node-dashboards"] + + Expect(orchDash.Installed).To(Or(BeTrue(), BeFalse())) + Expect(edgeDash.Installed).To(Or(BeTrue(), BeFalse())) + }) + + It("should detect alerting capabilities", func() { + obs := status.Orchestrator.Features["observability"] + alerting, exists := obs.SubFeatures["alerting"] + Expect(exists).To(BeTrue(), "alerting sub-feature should exist") + Expect(alerting.Installed).To(Or(BeTrue(), BeFalse())) + }) + }) + + Context("Kyverno policy management", func() { + It("should validate kyverno sub-features exist", func() { + kyverno, exists := status.Orchestrator.Features["kyverno"] + Expect(exists).To(BeTrue(), "kyverno feature should exist") + + expectedSubFeatures := []string{"policy-engine", "policies"} + for _, subFeature := range expectedSubFeatures { + _, exists := kyverno.SubFeatures[subFeature] + Expect(exists).To(BeTrue(), fmt.Sprintf("kyverno sub-feature %s should be present", subFeature)) + } + }) + + It("should detect policy engine and policies independently", func() { + kyverno := status.Orchestrator.Features["kyverno"] + engine := kyverno.SubFeatures["policy-engine"] + policies := kyverno.SubFeatures["policies"] + + // Policy engine is the core, policies are optional + if policies.Installed { + // If policies are installed, engine must be installed + Expect(engine.Installed).To(BeTrue(), "policy-engine must be installed if policies are installed") + } + }) + }) + + Context("Web UI components", func() { + It("should validate web-ui sub-features exist", func() { + webUI, exists := status.Orchestrator.Features["web-ui"] + Expect(exists).To(BeTrue(), "web-ui feature should exist") + + expectedSubFeatures := []string{ + "orchestrator-ui-root", + "application-orchestration-ui", + "cluster-orchestration-ui", + "infrastructure-ui", + } + for _, subFeature := range expectedSubFeatures { + _, exists := webUI.SubFeatures[subFeature] + Expect(exists).To(BeTrue(), fmt.Sprintf("web-ui sub-feature %s should be present", subFeature)) + } + }) + + It("should allow independent UI component deployment", func() { + webUI := status.Orchestrator.Features["web-ui"] + + orchUIRoot := webUI.SubFeatures["orchestrator-ui-root"] + appUI := webUI.SubFeatures["application-orchestration-ui"] + clusterUI := webUI.SubFeatures["cluster-orchestration-ui"] + infraUI := webUI.SubFeatures["infrastructure-ui"] + + // Each UI component can be enabled/disabled independently + Expect(orchUIRoot.Installed).To(Or(BeTrue(), BeFalse())) + Expect(appUI.Installed).To(Or(BeTrue(), BeFalse())) + Expect(clusterUI.Installed).To(Or(BeTrue(), BeFalse())) + Expect(infraUI.Installed).To(Or(BeTrue(), BeFalse())) + }) + }) + + Context("Multitenancy configuration", func() { + It("should validate multitenancy sub-features", func() { + mt, exists := status.Orchestrator.Features["multitenancy"] + Expect(exists).To(BeTrue(), "multitenancy feature should exist") + + // Multitenancy is always installed + Expect(mt.Installed).To(BeTrue(), "multitenancy should always be installed") + + defaultOnly, exists := mt.SubFeatures["default-tenant-only"] + Expect(exists).To(BeTrue(), "default-tenant-only sub-feature should exist") + Expect(defaultOnly.Installed).To(Or(BeTrue(), BeFalse())) + }) + }) + + Context("Application Orchestration", func() { + It("should exist as a top-level feature", func() { + appOrch, exists := status.Orchestrator.Features["application-orchestration"] + Expect(exists).To(BeTrue(), "application-orchestration feature should exist") + + // App-orch is a cohesive feature without sub-features + // All components work together as one deployment capability + Expect(appOrch.Installed).To(Or(BeTrue(), BeFalse())) + }) + }) + }) + + Describe("Feature State Consistency", Label(componentStatusLabel), func() { + var status ComponentStatus + var componentStatusURL string + + BeforeEach(func() { + componentStatusURL = "https://api." + serviceDomainWithPort + "/v1/orchestrator" + + req, err := http.NewRequest(http.MethodGet, componentStatusURL, nil) + Expect(err).ToNot(HaveOccurred()) + req.Header.Add("Authorization", "Bearer "+token) + + resp, err := cli.Do(req) + Expect(err).ToNot(HaveOccurred()) + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + Expect(err).ToNot(HaveOccurred()) + + err = json.Unmarshal(body, &status) + Expect(err).ToNot(HaveOccurred()) + }) + + It("should have consistent orchestrator version format", func() { + version := status.Orchestrator.Version + Expect(version).ToNot(BeEmpty(), "version should not be empty") + // Version should follow pattern: v2026.0.0 or v2026.0.0-dev- + Expect(version).To(MatchRegexp(`^v?\d{4}\.\d+\.\d+(-[a-z]+-[a-f0-9]+)?$`), + fmt.Sprintf("version format should be valid: %s", version)) + }) + + It("should maintain parent-child feature relationships", func() { + // If parent is disabled, children should also be disabled + for featureName, feature := range status.Orchestrator.Features { + if !feature.Installed { + for subName, subFeature := range feature.SubFeatures { + Expect(subFeature.Installed).To(BeFalse(), + fmt.Sprintf("sub-feature %s.%s should be disabled when parent is disabled", + featureName, subName)) + } + } + } + }) + + It("should have all boolean installed fields", func() { + // Verify all features have installed field as boolean + for featureName, feature := range status.Orchestrator.Features { + Expect(feature.Installed).To(Or(BeTrue(), BeFalse()), + fmt.Sprintf("feature %s should have boolean installed field", featureName)) + + for subName, subFeature := range feature.SubFeatures { + Expect(subFeature.Installed).To(Or(BeTrue(), BeFalse()), + fmt.Sprintf("sub-feature %s.%s should have boolean installed field", + featureName, subName)) + } + } + }) + }) +}) diff --git a/orch-configs/profiles/enable-platform.yaml b/orch-configs/profiles/enable-platform.yaml index 63a76d686..2bebf9a06 100644 --- a/orch-configs/profiles/enable-platform.yaml +++ b/orch-configs/profiles/enable-platform.yaml @@ -11,6 +11,7 @@ argo: ingress-nginx: true cert-manager: true certificate-file-server: true + component-status: true copy-app-gitea-cred-to-fleet: true copy-ca-cert-boots-to-gateway: true copy-ca-cert-boots-to-infra: true