Skip to content

Commit

Permalink
Merge pull request #1678 from headlamp-k8s/plugin_config
Browse files Browse the repository at this point in the history
Add Plugin Settings Detail View and Plugin Config Store
  • Loading branch information
yolossn authored Feb 29, 2024
2 parents e2fd246 + 848a90c commit b159e3c
Show file tree
Hide file tree
Showing 36 changed files with 2,910 additions and 343 deletions.
18 changes: 18 additions & 0 deletions backend/cmd/headlamp.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,24 @@ func defaultKubeConfigPersistenceFile() (string, error) {
// It serves plugin static files at “/plugins/” and “/static-plugins/”.
// It disables caching and reloads plugin list base paths if not in-cluster.
func addPluginRoutes(config *HeadlampConfig, r *mux.Router) {
// Delete plugin route
// This is only available when running locally.
if !config.useInCluster {
r.HandleFunc("/plugins/{name}", func(w http.ResponseWriter, r *http.Request) {
if err := checkHeadlampBackendToken(w, r); err != nil {
return
}
pluginName := mux.Vars(r)["name"]

err := plugins.Delete(config.pluginDir, pluginName)
if err != nil {
http.Error(w, "Error deleting plugin", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}).Methods("DELETE")
}

r.HandleFunc("/plugins", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
pluginsList, err := config.cache.Get(context.Background(), plugins.PluginListKey)
Expand Down
40 changes: 40 additions & 0 deletions backend/cmd/headlamp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -511,3 +511,43 @@ func TestDrainAndCordonNode(t *testing.T) {
}
}
}

func TestDeletePlugin(t *testing.T) {
// create temp dir for plugins
tempDir, err := os.MkdirTemp("", "plugins")
require.NoError(t, err)

defer os.RemoveAll(tempDir)

// create plugin
pluginDir := tempDir + "/test-plugin"
err = os.Mkdir(pluginDir, 0o755)
require.NoError(t, err)

// create plugin file
pluginFile := pluginDir + "/main.js"
_, err = os.Create(pluginFile)
require.NoError(t, err)

cache := cache.New[interface{}]()
kubeConfigStore := kubeconfig.NewContextStore()

c := HeadlampConfig{
useInCluster: false,
kubeConfigPath: config.GetDefaultKubeConfigPath(),
cache: cache,
kubeConfigStore: kubeConfigStore,
pluginDir: tempDir,
}

handler := createHeadlampHandler(&c)

rr, err := getResponseFromRestrictedEndpoint(handler, "DELETE", "/plugins/test-plugin", nil)
require.NoError(t, err)

assert.Equal(t, http.StatusOK, rr.Code)

// check if plugin was deleted
_, err = os.Stat(pluginDir)
assert.True(t, os.IsNotExist(err))
}
27 changes: 27 additions & 0 deletions backend/pkg/plugins/plugins.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import (
"io/fs"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"time"

"github.com/fsnotify/fsnotify"
Expand Down Expand Up @@ -228,3 +230,28 @@ func HandlePluginReload(cache cache.Cache[interface{}], w http.ResponseWriter) {
}
}
}

// Delete deletes the plugin from the plugin directory.
func Delete(pluginDir, filename string) error {
absPluginDir, err := filepath.Abs(pluginDir)
if err != nil {
return err
}

absPluginPath := path.Join(absPluginDir, filename)

if !isSubdirectory(absPluginDir, absPluginPath) {
return fmt.Errorf("plugin path '%s' is not a subdirectory of '%s'", absPluginPath, absPluginDir)
}

return os.RemoveAll(absPluginPath)
}

func isSubdirectory(parentDir, dirPath string) bool {
rel, err := filepath.Rel(parentDir, dirPath)
if err != nil {
return false
}

return !strings.HasPrefix(rel, "..") && !strings.HasPrefix(rel, ".")
}
39 changes: 39 additions & 0 deletions backend/pkg/plugins/plugins_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -319,3 +319,42 @@ func TestPopulatePluginsCache(t *testing.T) {
require.True(t, ok)
require.Empty(t, pluginListArr)
}

// TestDelete checks the Delete function.
func TestDelete(t *testing.T) {
tempDir, err := os.MkdirTemp("", "testdelete")
require.NoError(t, err)

defer os.RemoveAll(tempDir) // clean up

// Create a temporary file
tempFile, err := os.CreateTemp(tempDir, "testfile")
require.NoError(t, err)
tempFile.Close() // Close the file

// Test cases
tests := []struct {
pluginDir string
pluginName string
expectErr bool
}{
{pluginDir: tempDir, pluginName: tempFile.Name(), expectErr: false}, // Existing file
{pluginDir: tempDir, pluginName: "non-existent-directory", expectErr: false}, // Non-existent file
{pluginDir: tempDir, pluginName: "../", expectErr: true}, // Directory traversal

}

for _, tt := range tests {
tt := tt
t.Run(tt.pluginName, func(t *testing.T) {
err := plugins.Delete(tt.pluginDir, tt.pluginName)
if tt.expectErr {
assert.Error(t, err, "Delete should return an error")
} else {
// check if the file exists
_, err := os.Stat(path.Join(tt.pluginDir, tt.pluginName))
assert.True(t, os.IsNotExist(err), "File should not exist")
}
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
title: "Interface: PluginSettingsDetailsProps"
linkTitle: "PluginSettingsDetailsProps"
slug: "plugin_registry.PluginSettingsDetailsProps"
---

[plugin/registry](../modules/plugin_registry.md).PluginSettingsDetailsProps

## Properties

### onDataChange

`Optional` **onDataChange**: (`data`: { [`key`: `string`]: `any` }) => `void`
`readonly` **data**: { [`key`: `string`]: `any` }

#### Defined in
[plugin/pluginsSlice.ts](https://github.com/headlamp-k8s/headlamp/blob/main/frontend/src/plugin/pluginsSlice.ts#L7)

54 changes: 54 additions & 0 deletions docs/development/api/modules/plugin_registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,13 @@ ___

[components/DetailsViewSection/DetailsViewSection.tsx:9](https://github.com/headlamp-k8s/headlamp/blob/b0236780/frontend/src/components/DetailsViewSection/DetailsViewSection.tsx#L9)

___
### PluginSettingsComponentType

Ƭ **PluginSettingsComponentType**: `React.ComponentType`<[`PluginSettingsDetailsProps`](../interfaces/plugin_registry.PluginSettingsDetailsProps.md)\> \| `ReactElement` \| typeof `React.Component` \| ``null``

#### Defined in
[plugin/pluginsSlice.ts:24](https://github.com/headlamp-k8s/headlamp/blob/main/frontend/src/plugin/pluginsSlice.ts#L24)
___

### sectionFunc
Expand Down Expand Up @@ -661,3 +668,50 @@ registerSidebarEntryFilter(entry => (entry.name === 'workloads' ? null : entry))
#### Defined in

[plugin/registry.tsx:231](https://github.com/headlamp-k8s/headlamp/blob/b0236780/frontend/src/plugin/registry.tsx#L231)

___
### registerPluginSettings

▸ **registerPluginSettings**(`name`,`component`,`displaySaveButton`): `void`

**`example`**

```tsx
import { registerPluginSettings } from "@kinvolk/headlamp-plugin/lib";
import { TextField } from "@mui/material";
function MyPluginSettingsComponent(props: PluginSettingsDetailsProps) {
const { data, onDataChange } = props;
function onChange(value: string) {
if (onDataChange) {
onDataChange({ works: value });
}
}
return (
<TextField
value={data?.works || ""}
onChange={(e) => onChange(e.target.value)}
label="Normal Input"
variant="outlined"
fullWidth
/>
);
}
const displaySaveButton = true;
// Register a plugin settings component.
registerPluginSettings(
"my-plugin",
MyPluginSettingsComponent,
displaySaveButton
);
```

#### Parameters

| Name | Type | Description |
| :------ | :------ | :------ |
| `name` | `string` | The name of the plugin. |
| `component` | `PluginSettingsComponentType` | The component to be rendered in the plugin settings page. |
| `displaySaveButton` | `boolean` | Whether to display the save button. |
9 changes: 9 additions & 0 deletions docs/development/plugins/functionality.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,12 @@ React to Headlamp events with [registerHeadlampEventCallback](../api/modules/plu

- Example plugin shows [How to show snackbars for Headlamp events](https://github.com/kinvolk/headlamp/tree/main/plugins/examples/headlamp-events).
- API reference for [registerHeadlampEventCallback](../api/modules/plugin_registry.md#registerheadlampeventcallback)


### Plugin Settings

The plugins can have user configurable settings that can be used to change the behavior of the plugin. The plugin settings can be created using [registerPluginSettings](../api/modules/plugin_registry.md#registerpluginsettings).

- Example plugin shows [How to create plugin settings and use them](https://github.com/kinvolk/headlamp/tree/main/plugins/examples/change-logo)

![screenshot of the plugin settings](./images/plugin-settings.png)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit b159e3c

Please sign in to comment.