Skip to content

Commit e3e7175

Browse files
committed
feat(identifier): identify s2i built application and eap operator-managed pods
- Use image metadata to identify s2i built applications - Use 'managed-by: eap-operator' to identify EAP operator managed pods
1 parent 4ca5dd6 commit e3e7175

11 files changed

Lines changed: 316 additions & 53 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,6 @@ dist/
2222
*.swp
2323
*.swo
2424
*~
25+
26+
# Local, test specific folder
27+
test-eap-builder

README.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,19 @@ OpenShift Operator for automatic detection and labeling of Red Hat application p
33

44
## Description
55

6-
This is currently in a Proof of Concept state. The only pods that will be identified and labelled are EAP at the moment, there's a map in `identifier.go` that can be expanded upon to include other images.
6+
This is currently in a Proof of Concept state. The operator identifies and labels JBoss EAP pods from multiple deployment methods:
7+
8+
- **Direct deployments**: Pods using Red Hat EAP images (e.g., `registry.redhat.io/jboss-eap-7/...`)
9+
- **S2I builds**: Source-to-Image built applications with EAP base images
10+
- **EAP Operator-managed pods**: Pods deployed via the EAP Operator (identified by `app.kubernetes.io/managed-by: eap-operator` label)
11+
12+
The operator adds the following labels to identified pods:
13+
- `rht.comp`: Red Hat component/product name ("EAP")
14+
- `rht.pod_image`: The pod's container image name
15+
- `rht.pod_image_ver`: Version extracted from the pod's container image tag
16+
- `rht.comp_discovered`: Unix timestamp of when the pod was first discovered
17+
18+
The product detection map in `identifier.go` can be expanded to include other Red Hat middleware products.
719

820
## Getting Started
921

@@ -18,6 +30,8 @@ Your operator will need to be run with the following permissions:
1830

1931
Get, List, Watch, Patch, Update on pods.
2032

33+
Get, List, Watch on images.
34+
2135
### Running on the cluster
2236

2337
You’ll need an OpenShift cluster to run against. You can use CRC to get a local cluster for testing, or run against a remote cluster. Note: Your controller will automatically use the current context in your kubeconfig file (i.e. whatever cluster oc cluster-info shows).

cmd/main.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
// to ensure that exec-entrypoint and run can make use of them.
2727
"github.com/aptmac/app-discovery-operator/internal/controller"
2828
"github.com/aptmac/app-discovery-operator/internal/identifier"
29+
imagev1 "github.com/openshift/api/image/v1"
2930
_ "k8s.io/client-go/plugin/pkg/client/auth"
3031

3132
"k8s.io/apimachinery/pkg/runtime"
@@ -48,6 +49,7 @@ var (
4849

4950
func init() {
5051
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
52+
utilruntime.Must(imagev1.AddToScheme(scheme))
5153

5254
// +kubebuilder:scaffold:scheme
5355
}
@@ -203,6 +205,11 @@ func main() {
203205
// Create identifier
204206
productIdentifier := identifier.NewIdentifier()
205207

208+
// Try to set up OpenShift Image API support (will be nil in vanilla Kubernetes)
209+
imageInspector := identifier.NewImageInspector(mgr.GetClient())
210+
productIdentifier.SetImageInspector(imageInspector)
211+
setupLog.Info("Image inspector configured for S2I detection")
212+
206213
// Setup App Discovery controller
207214
if err = (&controller.AppDiscoveryReconciler{
208215
Client: mgr.GetClient(),

config/rbac/role.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@ rules:
99
- apiGroups: [""]
1010
resources: ["pods"]
1111
verbs: ["get", "list", "watch", "patch", "update"]
12+
- apiGroups: ["image.openshift.io"]
13+
resources: ["images"]
14+
verbs: ["get", "list", "watch"]

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ require (
4242
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
4343
github.com/modern-go/reflect2 v1.0.2 // indirect
4444
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
45+
github.com/openshift/api v0.0.0-20240124164020-e2ce40831f2e // indirect
4546
github.com/pkg/errors v0.9.1 // indirect
4647
github.com/prometheus/client_golang v1.22.0 // indirect
4748
github.com/prometheus/client_model v0.6.1 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg
9898
github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
9999
github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw=
100100
github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
101+
github.com/openshift/api v0.0.0-20240124164020-e2ce40831f2e h1:cxgCNo/R769CO23AK5TCh45H9SMUGZ8RukiF2/Qif3o=
102+
github.com/openshift/api v0.0.0-20240124164020-e2ce40831f2e/go.mod h1:CxgbWAlvu2iQB0UmKTtRu1YfepRg1/vJ64n2DlIEVz4=
101103
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
102104
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
103105
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=

internal/controller/discovery_controller.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ func (r *AppDiscoveryReconciler) Reconcile(ctx context.Context, req ctrl.Request
5050
}
5151

5252
// Identify if this is a Red Hat product
53-
match := r.Identifier.IdentifyPod(pod)
53+
match := r.Identifier.IdentifyPod(ctx, pod)
5454
if match == nil {
5555
// Not a Red Hat product, nothing to do
5656
log.V(1).Info("Pod is not a Red Hat product", "pod", pod.Name)
@@ -97,18 +97,18 @@ func (r *AppDiscoveryReconciler) labelPod(ctx context.Context, pod *corev1.Pod,
9797
pod.Labels["rht.comp"] = match.ProductName
9898
}
9999

100-
if _, exists := pod.Labels["rht.comp_ver"]; !exists {
101-
pod.Labels["rht.comp_ver"] = match.Version
100+
if _, exists := pod.Labels["rht.pod_image_ver"]; !exists {
101+
pod.Labels["rht.pod_image_ver"] = match.Version
102102
}
103103

104104
// Only set discovered timestamp if it doesn't exist (first seen time, not last modified)
105105
if _, exists := pod.Labels["rht.comp_discovered"]; !exists {
106106
pod.Labels["rht.comp_discovered"] = fmt.Sprintf("%d", match.Discovered.Unix())
107107
}
108108

109-
if _, exists := pod.Labels["rht.comp_image"]; !exists {
109+
if _, exists := pod.Labels["rht.pod_image"]; !exists {
110110
// Store the full image name (sanitized for label format)
111-
pod.Labels["rht.comp_image"] = sanitizeLabelValue(match.Image)
111+
pod.Labels["rht.pod_image"] = sanitizeLabelValue(match.Image)
112112
}
113113

114114
// Update the pod

internal/controller/discovery_controller_test.go

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -256,12 +256,12 @@ func TestReconcile_RedHatPodLabeling(t *testing.T) {
256256
}
257257

258258
// Check product and version labels
259-
if updatedPod.Labels["rht.comp"] != "jboss-eap" {
260-
t.Errorf("Expected rht.app to be 'jboss-eap', got '%s'", updatedPod.Labels["rht.comp"])
259+
if updatedPod.Labels["rht.comp"] != "EAP" {
260+
t.Errorf("Expected rht.comp to be 'EAP', got '%s'", updatedPod.Labels["rht.comp"])
261261
}
262262

263-
if updatedPod.Labels["rht.comp_ver"] != "7.4.0" {
264-
t.Errorf("Expected rht.comp_ver to be '7.4.0', got '%s'", updatedPod.Labels["rht.comp_ver"])
263+
if updatedPod.Labels["rht.pod_image_ver"] != "7.4.0" {
264+
t.Errorf("Expected rht.pod_image_ver to be '7.4.0', got '%s'", updatedPod.Labels["rht.pod_image_ver"])
265265
}
266266

267267
// Check discovered label exists (it's a timestamp, so just verify it exists)
@@ -270,8 +270,8 @@ func TestReconcile_RedHatPodLabeling(t *testing.T) {
270270
}
271271

272272
// Verify image label exists and is sanitized
273-
if _, exists := updatedPod.Labels["rht.comp_image"]; !exists {
274-
t.Error("Expected rht.comp_image label to exist")
273+
if _, exists := updatedPod.Labels["rht.pod_image"]; !exists {
274+
t.Error("Expected rht.pod_image label to exist")
275275
}
276276
}
277277

@@ -284,10 +284,10 @@ func TestReconcile_AlreadyLabeledPod(t *testing.T) {
284284
Name: "eap-pod",
285285
Namespace: "default",
286286
Labels: map[string]string{
287-
"rht.comp": "jboss-eap",
288-
"rht.comp_ver": "7.4.0",
287+
"rht.comp": "EAP",
288+
"rht.pod_image_ver": "7.4.0",
289289
"rht.comp_discovered": "1776437286", // Timestamp
290-
"rht.comp_image": "registry.redhat.io-jboss-eap-7-eap74-openjdk11-openshift-rhel8-7.4.0",
290+
"rht.pod_image": "registry.redhat.io-jboss-eap-7-eap74-openjdk11-openshift-rhel8-7.4.0",
291291
},
292292
},
293293
Spec: corev1.PodSpec{
@@ -344,7 +344,7 @@ func TestReconcile_AlreadyLabeledPod(t *testing.T) {
344344

345345
// In a real cluster, ResourceVersion would change if the pod was updated
346346
// With fake client, we just verify labels are still correct
347-
if afterPod.Labels["rht.comp"] != "jboss-eap" {
347+
if afterPod.Labels["rht.comp"] != "EAP" {
348348
t.Error("Labels should remain unchanged for already labeled pod")
349349
}
350350
}
@@ -358,7 +358,7 @@ func TestReconcile_MissingLabels(t *testing.T) {
358358
Name: "eap-pod",
359359
Namespace: "default",
360360
Labels: map[string]string{
361-
"rht.comp": "jboss-eap",
361+
"rht.comp": "EAP",
362362
// Missing version and discovered labels
363363
},
364364
},
@@ -407,7 +407,7 @@ func TestReconcile_MissingLabels(t *testing.T) {
407407
t.Fatalf("Failed to get updated pod: %v", err)
408408
}
409409

410-
if updatedPod.Labels["rht.comp_ver"] != "7.4.0" {
410+
if updatedPod.Labels["rht.pod_image_ver"] != "7.4.0" {
411411
t.Error("Expected version label to be added")
412412
}
413413

@@ -536,16 +536,16 @@ func TestReconcile_UserProvidedLabelsNotOverwritten(t *testing.T) {
536536
}
537537

538538
// Missing labels should be added
539-
if _, exists := updatedPod.Labels["rht.comp_ver"]; !exists {
540-
t.Error("Expected rht.comp_ver label to be added")
539+
if _, exists := updatedPod.Labels["rht.pod_image_ver"]; !exists {
540+
t.Error("Expected rht.pod_image_ver label to be added")
541541
}
542542

543543
if _, exists := updatedPod.Labels["rht.comp_discovered"]; !exists {
544544
t.Error("Expected rht.comp_discovered label to be added")
545545
}
546546

547-
if _, exists := updatedPod.Labels["rht.comp_image"]; !exists {
548-
t.Error("Expected rht.comp_image label to be added")
547+
if _, exists := updatedPod.Labels["rht.pod_image"]; !exists {
548+
t.Error("Expected rht.pod_image label to be added")
549549
}
550550
}
551551

internal/identifier/identifier.go

Lines changed: 85 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package identifier
22

33
import (
4+
"context"
45
"strings"
56
"time"
67

@@ -17,24 +18,38 @@ type ProductMatch struct {
1718

1819
// Identifier detects Red Hat products from pod specifications
1920
type Identifier struct {
20-
patterns map[string]string
21+
patterns map[string]string
22+
imageInspector *ImageInspector
2123
}
2224

2325
// NewIdentifier creates a new product identifier with hardcoded patterns
2426
func NewIdentifier() *Identifier {
2527
return &Identifier{
2628
patterns: map[string]string{
2729
// JBoss EAP (Enterprise Application Platform)
28-
"registry.redhat.io/jboss-eap-7": "jboss-eap",
29-
"registry.redhat.io/jboss-eap-8": "jboss-eap",
30-
"registry.redhat.io/jboss-eap/jboss-eap": "jboss-eap",
30+
"jboss-eap-7": "EAP",
31+
"jboss-eap-8": "EAP",
3132
},
33+
imageInspector: nil, // Will be set if running in OpenShift
3234
}
3335
}
3436

37+
// SetImageInspector sets the image inspector for OpenShift Image API access
38+
func (i *Identifier) SetImageInspector(inspector *ImageInspector) {
39+
i.imageInspector = inspector
40+
}
41+
3542
// IdentifyPod analyzes a pod and returns product information if it's a Red Hat product
36-
func (i *Identifier) IdentifyPod(pod *corev1.Pod) *ProductMatch {
37-
// Check all containers in the pod
43+
func (i *Identifier) IdentifyPod(ctx context.Context, pod *corev1.Pod) *ProductMatch {
44+
// First, check if this is an EAP Operator-managed pod
45+
// The EAP Operator already adds rht.comp label, but we want to add our additional labels
46+
if pod.Labels != nil {
47+
if managedBy, exists := pod.Labels["app.kubernetes.io/managed-by"]; exists && managedBy == "eap-operator" {
48+
return i.identifyOperatorManagedPod(pod)
49+
}
50+
}
51+
52+
// Second, try to identify by image name (direct deployments)
3853
for _, container := range pod.Spec.Containers {
3954
if match := i.identifyImage(container.Image); match != nil {
4055
return match
@@ -48,9 +63,44 @@ func (i *Identifier) IdentifyPod(pod *corev1.Pod) *ProductMatch {
4863
}
4964
}
5065

66+
// Fallback: Try OpenShift Image API (for S2I-built applications)
67+
// S2I-built apps have env vars in the image metadata
68+
if i.imageInspector != nil {
69+
if match := i.imageInspector.InspectPodImages(ctx, pod); match != nil {
70+
return match
71+
}
72+
}
73+
5174
return nil
5275
}
5376

77+
// identifyOperatorManagedPod handles pods managed by the EAP Operator
78+
// These pods already have rht.comp label, but we add version, image, and discovered timestamp
79+
func (i *Identifier) identifyOperatorManagedPod(pod *corev1.Pod) *ProductMatch {
80+
// Get the product name from existing label (EAP Operator sets "EAP")
81+
productName := pod.Labels["rht.comp"]
82+
if productName == "" {
83+
productName = "EAP" // Default if not set
84+
}
85+
86+
// Extract version and image from the first container
87+
var version, image string
88+
if len(pod.Spec.Containers) > 0 {
89+
image = pod.Spec.Containers[0].Image
90+
version = extractVersion(image)
91+
} else {
92+
version = "unknown"
93+
image = "unknown"
94+
}
95+
96+
return &ProductMatch{
97+
ProductName: productName,
98+
Version: version,
99+
Image: image,
100+
Discovered: pod.CreationTimestamp.Time,
101+
}
102+
}
103+
54104
// identifyImage checks if an image matches any Red Hat product pattern
55105
func (i *Identifier) identifyImage(image string) *ProductMatch {
56106
for pattern, productName := range i.patterns {
@@ -99,17 +149,43 @@ func extractVersion(image string) string {
99149
// ShouldLabel determines if a pod should be labeled
100150
// Returns true if any of the required labels are missing
101151
// Does not check label values to respect user-provided labels
152+
// For operator-managed pods, we only add our additional labels (version, image, discovered)
102153
func (i *Identifier) ShouldLabel(pod *corev1.Pod, match *ProductMatch) bool {
103154
if pod.Labels == nil {
104155
return true
105156
}
106157

107-
// Check if any required labels are missing (don't check values)
158+
// Check if this is an operator-managed pod (already has rht.comp)
159+
isOperatorManaged := false
160+
if managedBy, exists := pod.Labels["app.kubernetes.io/managed-by"]; exists && managedBy == "eap-operator" {
161+
isOperatorManaged = true
162+
}
163+
164+
if isOperatorManaged {
165+
// For operator-managed pods, check rht.comp and our additional labels
166+
// The EAP Operator sets rht.comp, but if it's deleted we should restore it
167+
labelsToCheck := []string{
168+
"rht.comp", // Product name (may be deleted by user)
169+
"rht.pod_image_ver", // Version of the pod's container image
170+
"rht.comp_discovered", // Discovery timestamp
171+
"rht.pod_image", // Pod's container image name
172+
}
173+
174+
for _, label := range labelsToCheck {
175+
if _, exists := pod.Labels[label]; !exists {
176+
return true // Missing label
177+
}
178+
}
179+
180+
return false // All labels are present
181+
}
182+
183+
// For non-operator-managed pods, check all required labels
108184
requiredLabels := []string{
109185
"rht.comp",
110-
"rht.comp_ver",
186+
"rht.pod_image_ver",
111187
"rht.comp_discovered",
112-
"rht.comp_image",
188+
"rht.pod_image",
113189
}
114190

115191
for _, label := range requiredLabels {

0 commit comments

Comments
 (0)