Skip to content

feat: plugins: Implement automated dev-plugins workflow #3713

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
22df071
plugins: headlamp-plugin: Implement automated dev-plugins workflow
krrish-sehgal Jul 29, 2025
5ac631d
Merge branch 'main' into plugin-management
krrish-sehgal Jul 29, 2025
2dd0762
fix: plugins: headlamp-plugin: removed unused definition of configDir
krrish-sehgal Jul 29, 2025
372f514
Merge branch 'main' of https://github.com/krrish-sehgal/headlamp into…
krrish-sehgal Jul 29, 2025
75bf9d3
Merge branch 'plugin-management' of https://github.com/krrish-sehgal/…
krrish-sehgal Jul 29, 2025
00f5122
frontend: TooltipIcon: Accept ReactNode type for children
skoeva Jul 10, 2025
6c7a8ae
backend+frontend: implement three-tier plugin classification system
krrish-sehgal Jul 30, 2025
3bdf049
frontend: Pod: Add tooltip for requests and limits
skoeva Jul 10, 2025
c5c584e
frontend: ClusterSelector: Create a separate component for ClusterSel…
mahmoodalisha Jul 31, 2025
69ac2cd
Merge pull request #3612 from skoeva/pod-metrics
illume Jul 31, 2025
4b2d7b4
app: Bump version to 0.34.0
joaquimrocha Jul 31, 2025
ca837a1
chocolatey: Bump Headlamp version to 0.34.0
joaquimrocha Jul 31, 2025
f8a61e9
Merge pull request #3725 from kubernetes-sigs/hl-ci-choco-update-0.34.0
joaquimrocha Jul 31, 2025
8619266
backend: Remove unnecessary assignment in range for loop
LinPr Aug 1, 2025
68e7477
Merge pull request #3730 from LinPr/range
yolossn Aug 1, 2025
b08faf8
plugins: headlamp-plugin: Implement automated dev-plugins workflow
krrish-sehgal Jul 29, 2025
bdd74e5
fix: plugins: headlamp-plugin: removed unused definition of configDir
krrish-sehgal Jul 29, 2025
7890910
backend+frontend: implement three-tier plugin classification system
krrish-sehgal Jul 30, 2025
57a3c9e
Merge branch 'plugin-management' of https://github.com/krrish-sehgal/…
krrish-sehgal Aug 2, 2025
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
4 changes: 2 additions & 2 deletions app/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion app/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "headlamp",
"version": "0.33.0",
"version": "0.34.0",
"description": "Easy-to-use and extensible Kubernetes web UI",
"main": "electron/main.js",
"homepage": "https://github.com/kubernetes-sigs/headlamp/#readme",
Expand Down
2 changes: 1 addition & 1 deletion app/windows/chocolatey/headlamp.nuspec
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd">
<metadata>
<id>headlamp</id>
<version>0.33.0</version>
<version>0.34.0</version>
<packageSourceUrl>https://github.com/kubernetes-sigs/headlamp/tree/main/app/windows/chocolatey</packageSourceUrl>
<title>Headlamp</title>
<authors>Kinvolk</authors>
Expand Down
4 changes: 2 additions & 2 deletions app/windows/chocolatey/tools/chocolateyinstall.ps1
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
$ErrorActionPreference = 'Stop'; # stop on all errors
$toolsDir = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)"
$headlampVersion = '0.33.0'
$headlampVersion = '0.34.0'
$url = "https://github.com/kubernetes-sigs/headlamp/releases/download/v${headlampVersion}/Headlamp-${headlampVersion}-win-x64.exe"
$checksum = 'a438191fcb08b82afcc4c5cb203808b341a8117dcd21d921d94f5f01bba19980'
$checksum = 'ce4d0a5c7566ed6c97042b6e3245b91e5a3d8e8169b669a88d4d9386db7650b3'

$packageArgs = @{
packageName = $env:ChocolateyPackageName
Expand Down
16 changes: 10 additions & 6 deletions backend/cmd/headlamp.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,16 @@

r.PathPrefix("/plugins/").Handler(pluginHandler)

// Serve dev-plugins
devPluginDir := filepath.Join(filepath.Dir(config.PluginDir), "dev-plugins")
if _, err := os.Stat(devPluginDir); err == nil {
devPluginHandler := http.StripPrefix(config.BaseURL+"/dev-plugins/", http.FileServer(http.Dir(devPluginDir)))
if !config.UseInCluster {
devPluginHandler = serveWithNoCacheHeader(devPluginHandler)
}
r.PathPrefix("/dev-plugins/").Handler(devPluginHandler)

Check failure on line 300 in backend/cmd/headlamp.go

View workflow job for this annotation

GitHub Actions / build

expressions should not be cuddled with blocks (wsl)
}

if config.StaticPluginDir != "" {
staticPluginsHandler := http.StripPrefix(config.BaseURL+"/static-plugins/",
http.FileServer(http.Dir(config.StaticPluginDir)))
Expand Down Expand Up @@ -1524,8 +1534,6 @@
}

for _, context := range contexts {
context := context

if context.Error != "" {
clusters = append(clusters, Cluster{
Name: context.Name,
Expand Down Expand Up @@ -1580,8 +1588,6 @@
var setupErrors []error

for _, context := range contexts {
context := context

info := context.KubeContext.Extensions["headlamp_info"]
if info != nil {
// Convert the runtime.Unknown object to a byte slice
Expand Down Expand Up @@ -2042,8 +2048,6 @@
}

for _, context := range contexts {
context := context

// Remove the old context from the store
if err := c.KubeConfigStore.RemoveContext(clusterName); err != nil {
logger.Log(logger.LevelError, nil, err, "Removing context from the store")
Expand Down
1 change: 0 additions & 1 deletion backend/cmd/headlamp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,6 @@ func TestDynamicClusters(t *testing.T) {
}

for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
cache := cache.New[interface{}]()
kubeConfigStore := kubeconfig.NewContextStore()
Expand Down
2 changes: 0 additions & 2 deletions backend/cmd/stateless.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,6 @@ func (c *HeadlampConfig) handleStatelessReq(r *http.Request, kubeConfig string)
}

for _, context := range contexts {
context := context

info := context.KubeContext.Extensions["headlamp_info"]
if info != nil {
customObj, err := MarshalCustomObject(info, context.Name)
Expand Down
2 changes: 0 additions & 2 deletions backend/cmd/stateless_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ func TestStatelessClustersKubeConfig(t *testing.T) {
}

for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
cache := cache.New[interface{}]()
kubeConfigStore := kubeconfig.NewContextStore()
Expand Down Expand Up @@ -127,7 +126,6 @@ func TestStatelessClusterApiRequest(t *testing.T) {
}

for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
cache := cache.New[interface{}]()
kubeConfigStore := kubeconfig.NewContextStore()
Expand Down
1 change: 0 additions & 1 deletion backend/pkg/helm/charts.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ func listCharts(filter string, settings *cli.EnvSettings) ([]chartInfo, error) {
index.AddRepo(name, indexFile, true)

for _, chart := range index.All() {
chart := chart
if filter != "" {
if strings.Contains(strings.ToLower(chart.Name), strings.ToLower(filter)) {
chartInfos = append(chartInfos, chartInfo{
Expand Down
2 changes: 0 additions & 2 deletions backend/pkg/helm/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,8 +215,6 @@ func listRepositories(settings *cli.EnvSettings) ([]repositoryInfo, error) {
repositories := make([]repositoryInfo, 0, len(repoFile.Repositories))

for _, repo := range repoFile.Repositories {
repo := repo

repositories = append(repositories, repositoryInfo{
Name: repo.Name,
URL: repo.URL,
Expand Down
2 changes: 0 additions & 2 deletions backend/pkg/helm/repository_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ func checkRepoExists(t *testing.T, helmHandler *helm.Handler, repoName string) b
require.NoError(t, err)

for _, repo := range listRepoResponse.Repositories {
repo := repo
if repo.Name == repoName {
return true
}
Expand Down Expand Up @@ -186,7 +185,6 @@ func TestUpdateRepo(t *testing.T) {
assert.NoError(t, err)

for _, repo := range listRepoResponse.Repositories {
repo := repo
if repo.Name == "headlamp_test_repo" {
assert.Equal(t, "https://kubernetes-sigs-update-url.github.io/headlamp/", repo.URL)
}
Expand Down
2 changes: 0 additions & 2 deletions backend/pkg/kubeconfig/watcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,6 @@ func LoadAndWatchFiles(kubeConfigStore ContextStore, paths string, source int, i

func addFilesToWatcher(watcher *fsnotify.Watcher, paths []string) {
for _, path := range paths {
path := path

// if path is relative, make it absolute
if !filepath.IsAbs(path) {
absPath, err := filepath.Abs(path)
Expand Down
1 change: 0 additions & 1 deletion backend/pkg/logger/logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ func TestLog(t *testing.T) {

// Call the Log function
for _, test := range tests {
test := test // Assign test to a local variable
t.Run(test.name, func(t *testing.T) {
logger.Log(test.level, test.str, test.err, test.msg)

Expand Down
78 changes: 69 additions & 9 deletions backend/pkg/plugins/plugins.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,44 +99,92 @@
}

// generateSeparatePluginPaths takes the staticPluginDir and pluginDir and returns separate lists of plugin paths.
func generateSeparatePluginPaths(staticPluginDir, pluginDir string) ([]string, []string, error) {
// Returns (staticPlugins, devPlugins, catalogPlugins, error) with proper priority ordering.
func generateSeparatePluginPaths(staticPluginDir, pluginDir string) ([]string, []string, []string, error) {
var pluginListURLStatic []string

if staticPluginDir != "" {
var err error

pluginListURLStatic, err = pluginBasePathListForDir(staticPluginDir, "static-plugins")
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
}

// Get development plugins (highest priority)
// dev-plugins is a sibling directory to plugins, not a subdirectory
devPluginDir := filepath.Join(filepath.Dir(pluginDir), "dev-plugins")
pluginListURLDev, err := pluginBasePathListForDir(devPluginDir, "dev-plugins")
if err != nil && !os.IsNotExist(err) {

Check failure on line 119 in backend/pkg/plugins/plugins.go

View workflow job for this annotation

GitHub Actions / build

only one cuddle assignment allowed before if statement (wsl)
return nil, nil, nil, err
}

// Get catalog/installed plugins
pluginListURL, err := pluginBasePathListForDir(pluginDir, "plugins")
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}

return pluginListURLStatic, pluginListURL, nil
return pluginListURLStatic, pluginListURLDev, pluginListURL, nil
}

// GeneratePluginPaths generates a concatenated list of plugin paths from the staticPluginDir and pluginDir.

Check failure on line 132 in backend/pkg/plugins/plugins.go

View workflow job for this annotation

GitHub Actions / build

Comment should end in a period (godot)
// Priority order: dev-plugins > plugins > static-plugins (dev overrides catalog, catalog overrides static)
func GeneratePluginPaths(staticPluginDir, pluginDir string) ([]string, error) {
pluginListURLStatic, pluginListURL, err := generateSeparatePluginPaths(staticPluginDir, pluginDir)
pluginListURLStatic, pluginListURLDev, pluginListURL, err := generateSeparatePluginPaths(staticPluginDir, pluginDir)
if err != nil {
return nil, err
}

// Concatenate the static and user plugin lists.
// Create a map to track plugin names and ensure proper priority
pluginMap := make(map[string]string)
var finalPluginList []string

Check failure on line 142 in backend/pkg/plugins/plugins.go

View workflow job for this annotation

GitHub Actions / build

Consider pre-allocating `finalPluginList` (prealloc)

// Add static plugins first (lowest priority)
if pluginListURLStatic != nil {

Check failure on line 145 in backend/pkg/plugins/plugins.go

View workflow job for this annotation

GitHub Actions / build

S1031: unnecessary nil check around range (gosimple)
pluginListURL = append(pluginListURLStatic, pluginListURL...)
for _, pluginPath := range pluginListURLStatic {
pluginName := getPluginNameFromPath(pluginPath)
pluginMap[pluginName] = pluginPath
}
}

// Add catalog plugins (medium priority) - can override static
if pluginListURL != nil {

Check failure on line 153 in backend/pkg/plugins/plugins.go

View workflow job for this annotation

GitHub Actions / build

S1031: unnecessary nil check around range (gosimple)
for _, pluginPath := range pluginListURL {
pluginName := getPluginNameFromPath(pluginPath)
pluginMap[pluginName] = pluginPath
}
}

// Add dev plugins (highest priority) - can override both catalog and static
if pluginListURLDev != nil {

Check failure on line 161 in backend/pkg/plugins/plugins.go

View workflow job for this annotation

GitHub Actions / build

S1031: unnecessary nil check around range (gosimple)
for _, pluginPath := range pluginListURLDev {
pluginName := getPluginNameFromPath(pluginPath)
pluginMap[pluginName] = pluginPath
}
}

return pluginListURL, nil
// Convert map back to slice
for _, pluginPath := range pluginMap {
finalPluginList = append(finalPluginList, pluginPath)
}

return finalPluginList, nil
}

// getPluginNameFromPath extracts the plugin name from a plugin path

Check failure on line 176 in backend/pkg/plugins/plugins.go

View workflow job for this annotation

GitHub Actions / build

Comment should end in a period (godot)
func getPluginNameFromPath(pluginPath string) string {
parts := strings.Split(pluginPath, "/")
if len(parts) >= 2 {
return parts[len(parts)-1] // Return the last part (plugin name)
}
return pluginPath

Check failure on line 182 in backend/pkg/plugins/plugins.go

View workflow job for this annotation

GitHub Actions / build

return statements should not be cuddled if block has more than two lines (wsl)
}

// ListPlugins lists the plugins in the static and user-added plugin directories.
func ListPlugins(staticPluginDir, pluginDir string) error {
staticPlugins, userPlugins, err := generateSeparatePluginPaths(staticPluginDir, pluginDir)
staticPlugins, devPlugins, userPlugins, err := generateSeparatePluginPaths(staticPluginDir, pluginDir)
if err != nil {
logger.Log(logger.LevelError, nil, err, "listing plugins")
return fmt.Errorf("listing plugins: %w", err)
Expand Down Expand Up @@ -173,6 +221,18 @@
fmt.Println("No static plugins found.")
}

if len(devPlugins) > 0 {
devPluginDir := filepath.Join(pluginDir, "dev-plugins")
fmt.Printf("\nDevelopment Plugins (%s):\n", devPluginDir)

for _, plugin := range devPlugins {
pluginName := getPluginName(filepath.Join(devPluginDir, plugin))
fmt.Println(" -", pluginName, "(dev)")
}
} else {
fmt.Printf("No development plugins found.")
}

if len(userPlugins) > 0 {
fmt.Printf("\nUser-added Plugins (%s):\n", pluginDir)

Expand Down
1 change: 0 additions & 1 deletion backend/pkg/plugins/plugins_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,6 @@ func TestDelete(t *testing.T) {
}

for _, tt := range tests {
tt := tt
t.Run(tt.pluginName, func(t *testing.T) {
err := plugins.Delete(tt.pluginDir, tt.pluginName)
if tt.expectErr {
Expand Down
1 change: 0 additions & 1 deletion backend/pkg/portforward/internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ func TestPortforwardKeyGenerator(t *testing.T) {
}

for _, tt := range tests {
tt := tt
testname := tt.name
t.Run(testname, func(t *testing.T) {
key := portforwardKeyGenerator(tt.p)
Expand Down
Loading
Loading