This is an automated email from the ASF dual-hosted git repository.

klesh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git


The following commit(s) were added to refs/heads/main by this push:
     new cfe519cf9 feat(plugins): add image extraction and revision image model 
to argocd (#8631)
cfe519cf9 is described below

commit cfe519cf9bb02eeec8e918024810b479c8be231d
Author: Richard Boisvert <[email protected]>
AuthorDate: Sun Nov 2 22:53:14 2025 -0500

    feat(plugins): add image extraction and revision image model to argocd 
(#8631)
    
    Adds ArgocdRevisionImage model + migration, extractor fallback logic, 
tests, and Grafana panels for deployment image visibility; includes 
golangci-lint config.
    
    For the issue https://github.com/apache/incubator-devlake/issues/8630
---
 backend/plugins/argocd/impl/impl.go                |   1 +
 backend/plugins/argocd/models/application.go       |   1 +
 .../{register.go => 20251102_add_image_support.go} |  27 ++-
 .../models/migrationscripts/archived/models.go     |  14 ++
 .../argocd/models/migrationscripts/register.go     |   1 +
 backend/plugins/argocd/models/revision_image.go    |  37 ++++
 backend/plugins/argocd/models/sync_operation.go    |   1 +
 .../plugins/argocd/tasks/application_extractor.go  |   4 +
 .../argocd/tasks/sync_operation_extractor.go       | 194 ++++++++++++++++++++-
 .../argocd/tasks/sync_operation_extractor_test.go  |  78 +++++++++
 grafana/dashboards/ArgoCD.json                     | 130 ++++++++++++++
 11 files changed, 476 insertions(+), 12 deletions(-)

diff --git a/backend/plugins/argocd/impl/impl.go 
b/backend/plugins/argocd/impl/impl.go
index c55958ea1..77f2696a4 100644
--- a/backend/plugins/argocd/impl/impl.go
+++ b/backend/plugins/argocd/impl/impl.go
@@ -85,6 +85,7 @@ func (p ArgoCD) GetTablesInfo() []dal.Tabler {
                &models.ArgocdConnection{},
                &models.ArgocdApplication{},
                &models.ArgocdSyncOperation{},
+               &models.ArgocdRevisionImage{},
                &models.ArgocdScopeConfig{},
        }
 }
diff --git a/backend/plugins/argocd/models/application.go 
b/backend/plugins/argocd/models/application.go
index 465cf0266..fd15a098b 100644
--- a/backend/plugins/argocd/models/application.go
+++ b/backend/plugins/argocd/models/application.go
@@ -39,6 +39,7 @@ type ArgocdApplication struct {
        SyncStatus     string     `gorm:"type:varchar(100)" json:"syncStatus" 
mapstructure:"syncStatus"`     // Synced, OutOfSync, Unknown
        HealthStatus   string     `gorm:"type:varchar(100)" json:"healthStatus" 
mapstructure:"healthStatus"` // Healthy, Progressing, Degraded, Suspended, 
Missing, Unknown
        CreatedDate    *time.Time `json:"createdDate,omitempty" 
mapstructure:"createdDate,omitempty"`
+       SummaryImages  []string   `gorm:"type:json;serializer:json" 
json:"summaryImages" mapstructure:"summaryImages"`
        common.NoPKModel
 }
 
diff --git a/backend/plugins/argocd/models/migrationscripts/register.go 
b/backend/plugins/argocd/models/migrationscripts/20251102_add_image_support.go
similarity index 52%
copy from backend/plugins/argocd/models/migrationscripts/register.go
copy to 
backend/plugins/argocd/models/migrationscripts/20251102_add_image_support.go
index b223622cc..7392d320c 100644
--- a/backend/plugins/argocd/models/migrationscripts/register.go
+++ 
b/backend/plugins/argocd/models/migrationscripts/20251102_add_image_support.go
@@ -18,11 +18,30 @@ limitations under the License.
 package migrationscripts
 
 import (
+       "github.com/apache/incubator-devlake/core/context"
+       "github.com/apache/incubator-devlake/core/errors"
        "github.com/apache/incubator-devlake/core/plugin"
+       "github.com/apache/incubator-devlake/helpers/migrationhelper"
+       
"github.com/apache/incubator-devlake/plugins/argocd/models/migrationscripts/archived"
 )
 
-func All() []plugin.MigrationScript {
-       return []plugin.MigrationScript{
-               new(addInitTables),
-       }
+var _ plugin.MigrationScript = (*addImageSupportArtifacts)(nil)
+
+type addImageSupportArtifacts struct{}
+
+func (m *addImageSupportArtifacts) Up(basicRes context.BasicRes) errors.Error {
+       return migrationhelper.AutoMigrateTables(
+               basicRes,
+               &archived.ArgocdApplication{},
+               &archived.ArgocdSyncOperation{},
+               &archived.ArgocdRevisionImage{},
+       )
+}
+
+func (*addImageSupportArtifacts) Version() uint64 {
+       return 20251102160000
+}
+
+func (*addImageSupportArtifacts) Name() string {
+       return "argocd add image support artifacts"
 }
diff --git a/backend/plugins/argocd/models/migrationscripts/archived/models.go 
b/backend/plugins/argocd/models/migrationscripts/archived/models.go
index ebaaa099c..2526c15cb 100644
--- a/backend/plugins/argocd/models/migrationscripts/archived/models.go
+++ b/backend/plugins/argocd/models/migrationscripts/archived/models.go
@@ -47,6 +47,7 @@ type ArgocdApplication struct {
        SyncStatus     string `gorm:"type:varchar(100)"`
        HealthStatus   string `gorm:"type:varchar(100)"`
        CreatedDate    *time.Time
+       SummaryImages  []string `gorm:"type:json;serializer:json"`
        ScopeConfigId  uint64
        archived.NoPKModel
 }
@@ -69,6 +70,7 @@ type ArgocdSyncOperation struct {
        SyncStatus      string `gorm:"type:varchar(100)"`
        HealthStatus    string `gorm:"type:varchar(100)"`
        ResourcesCount  int
+       ContainerImages []string `gorm:"type:json;serializer:json"`
        archived.NoPKModel
 }
 
@@ -76,6 +78,18 @@ func (ArgocdSyncOperation) TableName() string {
        return "_tool_argocd_sync_operations"
 }
 
+type ArgocdRevisionImage struct {
+       ConnectionId    uint64   `gorm:"primaryKey"`
+       ApplicationName string   `gorm:"primaryKey;type:varchar(255)"`
+       Revision        string   `gorm:"primaryKey;type:varchar(255)"`
+       Images          []string `gorm:"type:json;serializer:json"`
+       archived.NoPKModel
+}
+
+func (ArgocdRevisionImage) TableName() string {
+       return "_tool_argocd_revision_images"
+}
+
 type ArgocdScopeConfig struct {
        archived.ScopeConfig `mapstructure:",squash" json:",inline" 
gorm:"embedded"`
        ConnectionId         uint64   `gorm:"index"`
diff --git a/backend/plugins/argocd/models/migrationscripts/register.go 
b/backend/plugins/argocd/models/migrationscripts/register.go
index b223622cc..2c70d8115 100644
--- a/backend/plugins/argocd/models/migrationscripts/register.go
+++ b/backend/plugins/argocd/models/migrationscripts/register.go
@@ -24,5 +24,6 @@ import (
 func All() []plugin.MigrationScript {
        return []plugin.MigrationScript{
                new(addInitTables),
+               new(addImageSupportArtifacts),
        }
 }
diff --git a/backend/plugins/argocd/models/revision_image.go 
b/backend/plugins/argocd/models/revision_image.go
new file mode 100644
index 000000000..7b1080f31
--- /dev/null
+++ b/backend/plugins/argocd/models/revision_image.go
@@ -0,0 +1,37 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package models
+
+import "github.com/apache/incubator-devlake/core/models/common"
+
+// ArgocdRevisionImage captures the container images observed for a given
+// Argo CD application revision. It enables historical lookups so that
+// previously processed sync operations retain the images that were active
+// when they first ran, even after subsequent deployments update the
+// application summary images.
+type ArgocdRevisionImage struct {
+       ConnectionId    uint64   `gorm:"primaryKey"`
+       ApplicationName string   `gorm:"primaryKey;type:varchar(255)"`
+       Revision        string   `gorm:"primaryKey;type:varchar(255)"`
+       Images          []string `gorm:"type:json;serializer:json"`
+       common.NoPKModel
+}
+
+func (ArgocdRevisionImage) TableName() string {
+       return "_tool_argocd_revision_images"
+}
diff --git a/backend/plugins/argocd/models/sync_operation.go 
b/backend/plugins/argocd/models/sync_operation.go
index 3f2f2c833..77cc09544 100644
--- a/backend/plugins/argocd/models/sync_operation.go
+++ b/backend/plugins/argocd/models/sync_operation.go
@@ -37,6 +37,7 @@ type ArgocdSyncOperation struct {
        SyncStatus      string `gorm:"type:varchar(100)"` // Synced, OutOfSync
        HealthStatus    string `gorm:"type:varchar(100)"` // Healthy, Degraded, 
etc.
        ResourcesCount  int
+       ContainerImages []string `gorm:"type:json;serializer:json"`
        common.NoPKModel
 }
 
diff --git a/backend/plugins/argocd/tasks/application_extractor.go 
b/backend/plugins/argocd/tasks/application_extractor.go
index 54a01392d..3a0568609 100644
--- a/backend/plugins/argocd/tasks/application_extractor.go
+++ b/backend/plugins/argocd/tasks/application_extractor.go
@@ -63,6 +63,9 @@ type ArgocdApiApplication struct {
                Health struct {
                        Status string `json:"status"` // Healthy, Progressing, 
Degraded, etc.
                } `json:"health"`
+               Summary struct {
+                       Images []string `json:"images"`
+               } `json:"summary"`
        } `json:"status"`
 }
 
@@ -96,6 +99,7 @@ func ExtractApplications(taskCtx plugin.SubTaskContext) 
errors.Error {
                                DestNamespace:  
apiApp.Spec.Destination.Namespace,
                                SyncStatus:     apiApp.Status.Sync.Status,
                                HealthStatus:   apiApp.Status.Health.Status,
+                               SummaryImages:  apiApp.Status.Summary.Images,
                                CreatedDate:    
&apiApp.Metadata.CreationTimestamp,
                        }
                        application.ConnectionId = data.Options.ConnectionId
diff --git a/backend/plugins/argocd/tasks/sync_operation_extractor.go 
b/backend/plugins/argocd/tasks/sync_operation_extractor.go
index b73114947..5de738a38 100644
--- a/backend/plugins/argocd/tasks/sync_operation_extractor.go
+++ b/backend/plugins/argocd/tasks/sync_operation_extractor.go
@@ -19,8 +19,11 @@ package tasks
 
 import (
        "encoding/json"
+       "sort"
+       "strings"
        "time"
 
+       "github.com/apache/incubator-devlake/core/dal"
        "github.com/apache/incubator-devlake/core/errors"
        "github.com/apache/incubator-devlake/core/models/common"
        "github.com/apache/incubator-devlake/core/plugin"
@@ -52,6 +55,8 @@ type ArgocdApiSyncOperation struct {
                Username  string `json:"username"`
                Automated bool   `json:"automated"`
        } `json:"initiatedBy"`
+       Metadata  ArgocdApiSyncOperationMetadata `json:"metadata"`
+       Operation ArgocdApiSyncOperationDetails  `json:"operation"`
 
        // For operationState (current operation)
        Phase      string     `json:"phase"` // Succeeded, Failed, Error, 
Running, Terminating
@@ -65,18 +70,74 @@ type ArgocdApiSyncOperation struct {
 }
 
 type ArgocdApiSyncResourceItem struct {
-       Group     string `json:"group"`
-       Version   string `json:"version"`
-       Kind      string `json:"kind"`
-       Namespace string `json:"namespace"`
-       Name      string `json:"name"`
-       Status    string `json:"status"`
-       Message   string `json:"message"`
+       Group     string   `json:"group"`
+       Version   string   `json:"version"`
+       Kind      string   `json:"kind"`
+       Namespace string   `json:"namespace"`
+       Name      string   `json:"name"`
+       Status    string   `json:"status"`
+       Message   string   `json:"message"`
+       Images    []string `json:"images"`
+}
+
+type ArgocdApiSyncOperationMetadata struct {
+       Images    []string                                 `json:"images"`
+       Resources []ArgocdApiSyncOperationMetadataResource `json:"resources"`
+}
+
+type ArgocdApiSyncOperationMetadataResource struct {
+       Images []string `json:"images"`
+}
+
+type ArgocdApiSyncOperationDetails struct {
+       Metadata ArgocdApiSyncOperationMetadata `json:"metadata"`
+       Sync     ArgocdApiSyncOperationSync     `json:"sync"`
+}
+
+type ArgocdApiSyncOperationSync struct {
+       Resources []ArgocdApiSyncResourceItem `json:"resources"`
 }
 
 func ExtractSyncOperations(taskCtx plugin.SubTaskContext) errors.Error {
        data := taskCtx.GetData().(*ArgocdTaskData)
 
+       var summaryImages []string
+       application := &models.ArgocdApplication{}
+       db := taskCtx.GetDal()
+       if err := db.First(
+               application,
+               dal.Where("connection_id = ? AND name = ?", 
data.Options.ConnectionId, data.Options.ApplicationName),
+       ); err != nil {
+               if !db.IsErrorNotFound(err) {
+                       return errors.Default.Wrap(err, "error loading argocd 
application for summary images")
+               }
+       } else {
+               summaryImages = application.SummaryImages
+       }
+       summaryImages = normalizeImages(summaryImages)
+
+       revisionImageCache := make(map[string][]string)
+       revisionRecords := make([]models.ArgocdRevisionImage, 0)
+       err := db.All(
+               &revisionRecords,
+               dal.Where("connection_id = ? AND application_name = ?", 
data.Options.ConnectionId, data.Options.ApplicationName),
+       )
+       if err != nil && !db.IsErrorNotFound(err) {
+               return errors.Default.Wrap(err, "error loading argocd revision 
images")
+       }
+       for _, record := range revisionRecords {
+               if record.Revision == "" {
+                       continue
+               }
+               normalized := normalizeImages(record.Images)
+               if len(normalized) == 0 {
+                       continue
+               }
+               revisionImageCache[record.Revision] = normalized
+       }
+
+       revisionDirty := make(map[string][]string)
+
        extractor, err := api.NewApiExtractor(api.ApiExtractorArgs{
                RawDataSubTaskArgs: api.RawDataSubTaskArgs{
                        Ctx:   taskCtx,
@@ -160,7 +221,47 @@ func ExtractSyncOperations(taskCtx plugin.SubTaskContext) 
errors.Error {
 
                        syncOp.Kind = 
extractPrimaryDeploymentKind(apiOp.SyncResult.Resources)
 
-                       return []interface{}{syncOp}, nil
+                       payloadImages := collectContainerImages(&apiOp)
+                       images := copyStringSlice(payloadImages)
+
+                       if len(images) > 0 {
+                               if syncOp.Revision != "" {
+                                       cached := 
revisionImageCache[syncOp.Revision]
+                                       if !stringSlicesEqual(cached, images) {
+                                               
revisionImageCache[syncOp.Revision] = copyStringSlice(images)
+                                               revisionDirty[syncOp.Revision] 
= copyStringSlice(images)
+                                       }
+                               }
+                       } else if syncOp.Revision != "" {
+                               if cached := 
revisionImageCache[syncOp.Revision]; len(cached) > 0 {
+                                       images = copyStringSlice(cached)
+                               } else if len(summaryImages) > 0 {
+                                       // Fallback: use application summary 
images and cache for this revision.
+                                       images = copyStringSlice(summaryImages)
+                                       revisionImageCache[syncOp.Revision] = 
copyStringSlice(images)
+                                       revisionDirty[syncOp.Revision] = 
copyStringSlice(images)
+                               }
+                       }
+
+                       if len(images) > 0 {
+                               syncOp.ContainerImages = images
+                       }
+
+                       results := []interface{}{syncOp}
+                       if syncOp.Revision != "" {
+                               if dirtyImages, ok := 
revisionDirty[syncOp.Revision]; ok && len(dirtyImages) > 0 {
+                                       revision := &models.ArgocdRevisionImage{
+                                               ConnectionId:    
syncOp.ConnectionId,
+                                               ApplicationName: 
syncOp.ApplicationName,
+                                               Revision:        
syncOp.Revision,
+                                               Images:          
copyStringSlice(dirtyImages),
+                                       }
+                                       results = append(results, revision)
+                                       delete(revisionDirty, syncOp.Revision)
+                               }
+                       }
+
+                       return results, nil
                },
        })
 
@@ -201,3 +302,80 @@ func extractPrimaryDeploymentKind(resources 
[]ArgocdApiSyncResourceItem) string
 
        return ""
 }
+
+func collectContainerImages(apiOp *ArgocdApiSyncOperation) []string {
+       if apiOp == nil {
+               return nil
+       }
+
+       var collected []string
+       appendAll := func(images []string) {
+               if len(images) == 0 {
+                       return
+               }
+               collected = append(collected, images...)
+       }
+       appendMetadataResources := func(resources 
[]ArgocdApiSyncOperationMetadataResource) {
+               for _, r := range resources {
+                       appendAll(r.Images)
+               }
+       }
+       appendResourceItems := func(resources []ArgocdApiSyncResourceItem) {
+               for _, r := range resources {
+                       appendAll(r.Images)
+               }
+       }
+
+       appendAll(apiOp.Metadata.Images)
+       appendMetadataResources(apiOp.Metadata.Resources)
+       appendAll(apiOp.Operation.Metadata.Images)
+       appendMetadataResources(apiOp.Operation.Metadata.Resources)
+       appendResourceItems(apiOp.Operation.Sync.Resources)
+       appendResourceItems(apiOp.SyncResult.Resources)
+
+       return normalizeImages(collected)
+}
+
+func normalizeImages(images []string) []string {
+       if len(images) == 0 {
+               return nil
+       }
+       uniq := make(map[string]struct{}, len(images))
+       for _, image := range images {
+               trimmed := strings.TrimSpace(image)
+               if trimmed == "" {
+                       continue
+               }
+               uniq[trimmed] = struct{}{}
+       }
+       if len(uniq) == 0 {
+               return nil
+       }
+       normalized := make([]string, 0, len(uniq))
+       for image := range uniq {
+               normalized = append(normalized, image)
+       }
+       sort.Strings(normalized)
+       return normalized
+}
+
+func copyStringSlice(values []string) []string {
+       if len(values) == 0 {
+               return nil
+       }
+       dup := make([]string, len(values))
+       copy(dup, values)
+       return dup
+}
+
+func stringSlicesEqual(a, b []string) bool {
+       if len(a) != len(b) {
+               return false
+       }
+       for i := range a {
+               if a[i] != b[i] {
+                       return false
+               }
+       }
+       return true
+}
diff --git a/backend/plugins/argocd/tasks/sync_operation_extractor_test.go 
b/backend/plugins/argocd/tasks/sync_operation_extractor_test.go
new file mode 100644
index 000000000..9d71cec41
--- /dev/null
+++ b/backend/plugins/argocd/tasks/sync_operation_extractor_test.go
@@ -0,0 +1,78 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+       "testing"
+
+       "github.com/stretchr/testify/assert"
+)
+
+func TestCollectContainerImages_ReturnsSortedUniqueImages(t *testing.T) {
+       op := &ArgocdApiSyncOperation{}
+       op.Metadata.Images = []string{"registry.example.com/system:ops", " 
registry.example.com/sidecar:def456 "}
+       op.Metadata.Resources = []ArgocdApiSyncOperationMetadataResource{
+               {Images: []string{"registry.example.com/app:abc123", "", 
"registry.example.com/api:789xyz"}},
+       }
+       op.Operation.Metadata.Images = 
[]string{"registry.example.com/worker:alpha"}
+       op.Operation.Metadata.Resources = 
[]ArgocdApiSyncOperationMetadataResource{
+               {Images: []string{"registry.example.com/rollout:blue"}},
+       }
+       op.Operation.Sync.Resources = []ArgocdApiSyncResourceItem{
+               {Images: []string{"registry.example.com/canary:latest"}},
+       }
+       op.SyncResult.Resources = []ArgocdApiSyncResourceItem{
+               {Images: []string{"registry.example.com/worker:alpha", 
"registry.example.com/api:789xyz"}},
+       }
+
+       images := collectContainerImages(op)
+
+       expected := []string{
+               "registry.example.com/api:789xyz",
+               "registry.example.com/app:abc123",
+               "registry.example.com/canary:latest",
+               "registry.example.com/rollout:blue",
+               "registry.example.com/sidecar:def456",
+               "registry.example.com/system:ops",
+               "registry.example.com/worker:alpha",
+       }
+       assert.Equal(t, expected, images)
+}
+
+func TestCollectContainerImages_EmptyInputReturnsNil(t *testing.T) {
+       op := &ArgocdApiSyncOperation{}
+
+       assert.Nil(t, collectContainerImages(op))
+       assert.Nil(t, collectContainerImages(nil))
+}
+
+func TestNormalizeImages(t *testing.T) {
+       input := []string{" registry.example.com/app:1.0 ", 
"registry.example.com/app:1.0", "registry.example.com/api:2.0", ""}
+       expected := []string{"registry.example.com/api:2.0", 
"registry.example.com/app:1.0"}
+       assert.Equal(t, expected, normalizeImages(input))
+}
+
+// Fallback: no images in payload → expect none here (other fallbacks tested 
elsewhere)
+func TestCollectContainerImages_FallbackRevisionAndSummary(t *testing.T) {
+       revision := "abcdef1234567890"
+       apiPayload := ArgocdApiSyncOperation{Revision: revision}
+       assert.Nil(t, collectContainerImages(&apiPayload))
+
+       // normalizeImages: dedupe + sort
+       assert.Equal(t, []string{"a", "b"}, normalizeImages([]string{"b", "a", 
"b"}))
+}
diff --git a/grafana/dashboards/ArgoCD.json b/grafana/dashboards/ArgoCD.json
index 1086d47a6..262a6b5e0 100644
--- a/grafana/dashboards/ArgoCD.json
+++ b/grafana/dashboards/ArgoCD.json
@@ -827,6 +827,136 @@
       ],
       "title": "4.1 Recent Deployments",
       "type": "table"
+    },
+    {
+      "collapsed": false,
+      "id": 17,
+      "panels": [],
+      "title": "5. Images",
+      "type": "row"
+    },
+    {
+      "datasource": "mysql",
+      "fieldConfig": {
+        "defaults": {
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green",
+                "value": null
+              }
+            ]
+          }
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 41
+      },
+      "id": 18,
+      "options": {
+        "cellHeight": "sm",
+        "footer": {
+          "countRows": false,
+          "fields": "",
+          "reducer": [
+            "sum"
+          ],
+          "show": false
+        },
+        "showHeader": true,
+        "sortBy": [
+          {
+            "desc": true,
+            "displayName": "deployment_created"
+          }
+        ]
+      },
+      "pluginVersion": "9.5.15",
+      "targets": [
+        {
+          "datasource": "mysql",
+          "editorMode": "code",
+          "format": "table",
+          "group": [],
+          "metricColumn": "none",
+          "rawQuery": true,
+          "rawSql": "SELECT\n  d.created_date AS deployment_created,\n  d.name 
AS deployment_name,\n  ri.images AS images,\n  c.commit_sha AS revision,\n  
d.environment,\n  d.result\nFROM cicd_deployments d\nLEFT JOIN 
cicd_deployment_commits c ON c.cicd_deployment_id = d.id\nLEFT JOIN 
_tool_argocd_revision_images ri ON ri.revision = c.commit_sha\nWHERE 
$__timeFilter(d.created_date)\n  AND ( '${application_id:raw}' = '' OR 
'${application_id:raw}' = '$__all' OR FIND_IN_SET(d.cicd_scope_id, [...]
+          "refId": "A"
+        }
+      ],
+      "title": "5.1 Recent Deployment Images",
+      "type": "table"
+    },
+    {
+      "datasource": "mysql",
+      "description": "Approximation: counts deployments per unique images 
array (revision-level). For per-image counts, consider exploding arrays during 
ingestion.",
+      "fieldConfig": {
+        "defaults": {
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green",
+                "value": null
+              }
+            ]
+          }
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 41
+      },
+      "id": 19,
+      "options": {
+        "displayLabels": [
+          "name",
+          "value"
+        ],
+        "legend": {
+          "calcs": [],
+          "displayMode": "list",
+          "placement": "right",
+          "showLegend": true
+        },
+        "pieType": "pie",
+        "reduceOptions": {
+          "calcs": [
+            "sum"
+          ],
+          "fields": "",
+          "values": false
+        },
+        "tooltip": {
+          "mode": "single",
+          "sort": "none"
+        }
+      },
+      "pluginVersion": "9.5.15",
+      "targets": [
+        {
+          "datasource": "mysql",
+          "editorMode": "code",
+          "format": "table",
+          "group": [],
+          "metricColumn": "none",
+          "rawQuery": true,
+          "rawSql": "WITH dep AS (\n  SELECT d.id, c.commit_sha, 
d.cicd_scope_id\n  FROM cicd_deployments d\n  LEFT JOIN cicd_deployment_commits 
c ON c.cicd_deployment_id = d.id\n  WHERE $__timeFilter(d.created_date)\n    
AND ( '${application_id:raw}' = '' OR '${application_id:raw}' = '$__all' OR 
FIND_IN_SET(d.cicd_scope_id, '${application_id:raw}') > 0 )\n), imgs AS (\n  
SELECT dep.id deployment_id, ri.images\n  FROM dep\n  LEFT JOIN 
_tool_argocd_revision_images ri ON ri.revision = dep. [...]
+          "refId": "A"
+        }
+      ],
+      "title": "5.2 Top Image Arrays (Approx)",
+      "type": "table"
     }
   ],
   "refresh": "",

Reply via email to