Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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 argocd/applications/configs/component-status.yaml
Original file line number Diff line number Diff line change
@@ -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
92 changes: 92 additions & 0 deletions argocd/applications/custom/component-status.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# 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
# Authentication required - component status contains sensitive installation information
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 (source of truth)
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 }}

# Edge Infrastructure Manager: Enabled when edgeinfra profile is loaded
# Detection: enable-edgeinfra.yaml in root-app valueFiles
# Profile enables 4 core apps: infra-core, infra-managers, infra-onboarding, infra-external
# We report the overall feature as installed if ANY infra app is enabled
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 }}
infra-core:
installed: {{ index .Values.argo.enabled "infra-core" | default false }}
infra-manager:
installed: {{ index .Values.argo.enabled "infra-managers" | default false }}
device-provisioning:
installed: {{ index .Values.argo.enabled "infra-onboarding" | default false }}

# 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 }}

# 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:
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 are part of the platform
multitenancy:
installed: {{ index .Values.argo.enabled "tenancy-manager" | default false }}
default-tenant-only:
installed: {{ index .Values.argo.enabled "defaultTenancy" | default false }}

# Detection: enable-kyverno.yaml in root-app valueFiles
kyverno:
installed: {{ index .Values.argo.enabled "kyverno" | default false }}
52 changes: 52 additions & 0 deletions argocd/applications/templates/component-status.yaml
Original file line number Diff line number Diff line change
@@ -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.1
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 }}
189 changes: 189 additions & 0 deletions e2e-tests/orchestrator/component_status_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
// 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))
}
})

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))
}
})
})
})
1 change: 1 addition & 0 deletions orch-configs/profiles/enable-platform.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading