mirror of
https://github.com/komodorio/helm-dashboard.git
synced 2026-03-24 11:48:04 +00:00
Add a new "Images" tab on the release details page that extracts and displays all container images (including init containers) from the release manifest. Images are grouped by image string and show the associated resource and container name. Closes #83 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
311 lines
7.3 KiB
Go
311 lines
7.3 KiB
Go
package objects
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"io"
|
|
|
|
"github.com/joomcode/errorx"
|
|
"github.com/komodorio/helm-dashboard/v2/pkg/dashboard/utils"
|
|
"github.com/pkg/errors"
|
|
log "github.com/sirupsen/logrus"
|
|
"helm.sh/helm/v3/pkg/action"
|
|
"helm.sh/helm/v3/pkg/cli"
|
|
"helm.sh/helm/v3/pkg/release"
|
|
v1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
|
|
"k8s.io/apimachinery/pkg/util/yaml"
|
|
"k8s.io/client-go/tools/clientcmd"
|
|
//"sigs.k8s.io/yaml"
|
|
)
|
|
|
|
type DataLayer struct {
|
|
KubeContext string
|
|
StatusInfo *StatusInfo
|
|
Namespaces []string
|
|
Cache *Cache
|
|
|
|
ConfGen HelmConfigGetter
|
|
appPerContext map[string]*Application
|
|
appPerContextMx *sync.Mutex
|
|
devel bool
|
|
LocalCharts []string
|
|
}
|
|
|
|
type StatusInfo struct {
|
|
CurVer string
|
|
LatestVer string
|
|
Analytics bool
|
|
CacheHitRatio float64
|
|
ClusterMode bool
|
|
NoHealth bool
|
|
NoLatest bool
|
|
}
|
|
|
|
func NewDataLayer(ns []string, ver string, cg HelmConfigGetter, devel bool) (*DataLayer, error) {
|
|
if cg == nil {
|
|
return nil, errors.New("HelmConfigGetter can't be nil")
|
|
}
|
|
|
|
return &DataLayer{
|
|
Namespaces: ns,
|
|
Cache: NewCache(),
|
|
StatusInfo: &StatusInfo{
|
|
CurVer: ver,
|
|
Analytics: false,
|
|
},
|
|
|
|
ConfGen: cg,
|
|
appPerContext: map[string]*Application{},
|
|
appPerContextMx: new(sync.Mutex),
|
|
devel: devel,
|
|
}, nil
|
|
}
|
|
|
|
func (d *DataLayer) ListContexts() ([]KubeContext, error) {
|
|
res := []KubeContext{}
|
|
|
|
if d.StatusInfo.ClusterMode {
|
|
return res, nil
|
|
}
|
|
|
|
cfg, err := clientcmd.NewDefaultPathOptions().GetStartingConfig()
|
|
if err != nil {
|
|
return nil, errorx.Decorate(err, "failed to get kubectl config")
|
|
}
|
|
|
|
for name, ctx := range cfg.Contexts {
|
|
res = append(res, KubeContext{
|
|
IsCurrent: cfg.CurrentContext == name,
|
|
Name: name,
|
|
Cluster: ctx.Cluster,
|
|
AuthInfo: ctx.AuthInfo,
|
|
Namespace: ctx.Namespace,
|
|
})
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
func (d *DataLayer) GetStatus() *StatusInfo {
|
|
sum := float64(d.Cache.HitCount + d.Cache.MissCount)
|
|
if sum > 0 {
|
|
d.StatusInfo.CacheHitRatio = float64(d.Cache.HitCount) / sum
|
|
} else {
|
|
d.StatusInfo.CacheHitRatio = 0
|
|
}
|
|
return d.StatusInfo
|
|
}
|
|
|
|
type SectionFn = func(*release.Release, bool) (string, error)
|
|
|
|
func ParseManifests(out string) ([]*v1.Carp, error) {
|
|
dec := yaml.NewYAMLOrJSONDecoder(strings.NewReader(out), 4096)
|
|
res := make([]*v1.Carp, 0)
|
|
var tmp interface{}
|
|
for {
|
|
err := dec.Decode(&tmp)
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
|
|
if err != nil {
|
|
return res, errorx.Decorate(err, "failed to parse manifest document #%d", len(res)+1)
|
|
}
|
|
|
|
// k8s libs uses only JSON tags defined, say hello to https://github.com/go-yaml/yaml/issues/424
|
|
// we can juggle it
|
|
jsoned, err := json.Marshal(tmp)
|
|
if err != nil {
|
|
return res, err
|
|
}
|
|
|
|
var doc v1.Carp
|
|
err = json.Unmarshal(jsoned, &doc)
|
|
if err != nil {
|
|
return res, err
|
|
}
|
|
|
|
if doc.Kind == "" {
|
|
log.Warnf("Manifest piece is not k8s resource: %s", jsoned)
|
|
continue
|
|
}
|
|
|
|
res = append(res, &doc)
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
// ContainerImage represents a container image found in a release manifest.
|
|
type ContainerImage struct {
|
|
Resource string `json:"resource"`
|
|
Kind string `json:"kind"`
|
|
Container string `json:"container"`
|
|
Image string `json:"image"`
|
|
}
|
|
|
|
// ExtractImages parses a manifest and returns all container images found.
|
|
func ExtractImages(manifest string) []ContainerImage {
|
|
dec := yaml.NewYAMLOrJSONDecoder(strings.NewReader(manifest), 4096)
|
|
var images []ContainerImage
|
|
for {
|
|
var tmp map[string]interface{}
|
|
if err := dec.Decode(&tmp); err != nil {
|
|
break
|
|
}
|
|
kind, _ := tmp["kind"].(string)
|
|
metadata, _ := tmp["metadata"].(map[string]interface{})
|
|
name, _ := metadata["name"].(string)
|
|
|
|
for _, podSpec := range findPodSpecs(kind, tmp) {
|
|
containers, _ := podSpec["containers"].([]interface{})
|
|
images = extractFromContainers(images, kind, name, containers)
|
|
initContainers, _ := podSpec["initContainers"].([]interface{})
|
|
images = extractFromContainers(images, kind, name, initContainers)
|
|
}
|
|
}
|
|
return images
|
|
}
|
|
|
|
func extractFromContainers(images []ContainerImage, kind, resource string, containers []interface{}) []ContainerImage {
|
|
for _, c := range containers {
|
|
cMap, ok := c.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
img, _ := cMap["image"].(string)
|
|
cName, _ := cMap["name"].(string)
|
|
if img != "" {
|
|
images = append(images, ContainerImage{
|
|
Resource: resource,
|
|
Kind: kind,
|
|
Container: cName,
|
|
Image: img,
|
|
})
|
|
}
|
|
}
|
|
return images
|
|
}
|
|
|
|
// findPodSpecs returns the pod spec(s) from a resource, depending on kind.
|
|
func findPodSpecs(kind string, obj map[string]interface{}) []map[string]interface{} {
|
|
spec, _ := obj["spec"].(map[string]interface{})
|
|
if spec == nil {
|
|
return nil
|
|
}
|
|
switch kind {
|
|
case "Pod":
|
|
return []map[string]interface{}{spec}
|
|
case "Deployment", "StatefulSet", "DaemonSet", "ReplicaSet", "Job":
|
|
tpl, _ := spec["template"].(map[string]interface{})
|
|
if ps, ok := tpl["spec"].(map[string]interface{}); ok {
|
|
return []map[string]interface{}{ps}
|
|
}
|
|
case "CronJob":
|
|
jobTpl, _ := spec["jobTemplate"].(map[string]interface{})
|
|
jobSpec, _ := jobTpl["spec"].(map[string]interface{})
|
|
tpl, _ := jobSpec["template"].(map[string]interface{})
|
|
if ps, ok := tpl["spec"].(map[string]interface{}); ok {
|
|
return []map[string]interface{}{ps}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d *DataLayer) SetContext(ctx string) error {
|
|
if d.KubeContext != ctx {
|
|
err := d.Cache.Clear()
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to set context")
|
|
}
|
|
}
|
|
|
|
d.KubeContext = ctx
|
|
|
|
return nil
|
|
}
|
|
|
|
func (d *DataLayer) AppForCtx(ctx string) (*Application, error) {
|
|
d.appPerContextMx.Lock()
|
|
defer d.appPerContextMx.Unlock()
|
|
|
|
app, ok := d.appPerContext[ctx]
|
|
if !ok {
|
|
settings := cli.New()
|
|
settings.KubeContext = ctx
|
|
|
|
settings.SetNamespace(d.nsForCtx(ctx))
|
|
|
|
cfgGetter := func(ns string) (*action.Configuration, error) {
|
|
return d.ConfGen(settings, ns)
|
|
}
|
|
|
|
a, err := NewApplication(settings, cfgGetter, d.Namespaces, d.devel)
|
|
if err != nil {
|
|
return nil, errorx.Decorate(err, "Failed to create application for context '%s'", ctx)
|
|
}
|
|
|
|
a.Repositories.LocalCharts = d.LocalCharts
|
|
|
|
app = a
|
|
d.appPerContext[ctx] = app
|
|
}
|
|
return app, nil
|
|
}
|
|
|
|
func (d *DataLayer) nsForCtx(ctx string) string {
|
|
lst, err := d.ListContexts()
|
|
if err != nil {
|
|
log.Debugf("Failed to get contexts for NS lookup: %+v", err)
|
|
}
|
|
for _, c := range lst {
|
|
if c.Name == ctx {
|
|
return c.Namespace
|
|
}
|
|
}
|
|
log.Debugf("Strange: no context found for '%s'", ctx)
|
|
return ""
|
|
}
|
|
|
|
func (d *DataLayer) PeriodicTasks(ctx context.Context) {
|
|
if !utils.EnvAsBool("HD_NO_AUTOUPDATE", false) {
|
|
// auto-update repos
|
|
go d.loopUpdateRepos(ctx, 10*time.Minute) // TODO: parameterize interval?
|
|
}
|
|
}
|
|
|
|
func (d *DataLayer) loopUpdateRepos(ctx context.Context, interval time.Duration) {
|
|
ticker := time.NewTicker(interval)
|
|
for {
|
|
app, err := d.AppForCtx("")
|
|
if err != nil {
|
|
log.Warnf("Failed to get app object while in background repo update: %v", err)
|
|
break // no point in retrying
|
|
} else {
|
|
repos, err := app.Repositories.List()
|
|
if err != nil {
|
|
log.Warnf("Failed to get list of repos while in background update: %v", err)
|
|
}
|
|
|
|
for _, repo := range repos {
|
|
err := repo.Update()
|
|
if err != nil {
|
|
log.Warnf("Failed to update repo %s: %v", repo.Name(), err)
|
|
}
|
|
}
|
|
}
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
ticker.Stop()
|
|
return
|
|
case <-ticker.C:
|
|
continue
|
|
}
|
|
}
|
|
log.Debugf("Update repo loop done.")
|
|
}
|