Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 6 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -249,10 +249,10 @@ run-backend:
@echo "**** Warning: Running with Helm and dynamic-clusters endpoints enabled. ****"

ifeq ($(UNIXSHELL),true)
HEADLAMP_BACKEND_TOKEN=headlamp HEADLAMP_CONFIG_ENABLE_HELM=true HEADLAMP_CONFIG_ENABLE_DYNAMIC_CLUSTERS=true ./backend/headlamp-server -dev -proxy-urls https://artifacthub.io/* -listen-addr=localhost
HEADLAMP_BACKEND_TOKEN=headlamp HEADLAMP_CONFIG_ENABLE_HELM=true HEADLAMP_CONFIG_ENABLE_DYNAMIC_CLUSTERS=true HEADLAMP_CONFIG_ALLOW_KUBECONFIG_REMOVAL=true ./backend/headlamp-server -dev -proxy-urls https://artifacthub.io/* -listen-addr=localhost
else
@echo "**** Running on Windows without bash or zsh. ****"
@cmd /c "set HEADLAMP_BACKEND_TOKEN=headlamp&& set HEADLAMP_CONFIG_ENABLE_HELM=true&& set HEADLAMP_CONFIG_ENABLE_DYNAMIC_CLUSTERS=true&& backend\headlamp-server -dev -proxy-urls https://artifacthub.io/* -listen-addr=localhost"
@cmd /c "set HEADLAMP_BACKEND_TOKEN=headlamp&& set HEADLAMP_CONFIG_ENABLE_HELM=true&& set HEADLAMP_CONFIG_ENABLE_DYNAMIC_CLUSTERS=true&& set HEADLAMP_CONFIG_ALLOW_KUBECONFIG_REMOVAL=true&& backend\headlamp-server -dev -proxy-urls https://artifacthub.io/* -listen-addr=localhost"
endif

run-dev:
Expand All @@ -266,10 +266,11 @@ ifeq ($(UNIXSHELL),true)
HEADLAMP_CONFIG_METRICS_ENABLED=true \
HEADLAMP_CONFIG_ENABLE_HELM=true \
HEADLAMP_CONFIG_ENABLE_DYNAMIC_CLUSTERS=true \
HEADLAMP_CONFIG_ALLOW_KUBECONFIG_REMOVAL=true \
./backend/headlamp-server -dev -proxy-urls https://artifacthub.io/* -listen-addr=localhost
else
@echo "**** Running on Windows without bash or zsh. ****"
@cmd /c "set HEADLAMP_BACKEND_TOKEN=headlamp&& set HEADLAMP_CONFIG_METRICS_ENABLED=true&& set HEADLAMP_CONFIG_ENABLE_HELM=true&& set HEADLAMP_CONFIG_ENABLE_DYNAMIC_CLUSTERS=true&& backend\headlamp-server -dev -proxy-urls https://artifacthub.io/* -listen-addr=localhost"
@cmd /c "set HEADLAMP_BACKEND_TOKEN=headlamp&& set HEADLAMP_CONFIG_METRICS_ENABLED=true&& set HEADLAMP_CONFIG_ENABLE_HELM=true&& set HEADLAMP_CONFIG_ENABLE_DYNAMIC_CLUSTERS=true&& set HEADLAMP_CONFIG_ALLOW_KUBECONFIG_REMOVAL=true&& backend\headlamp-server -dev -proxy-urls https://artifacthub.io/* -listen-addr=localhost"
endif

run-backend-with-traces:
Expand All @@ -279,10 +280,11 @@ ifeq ($(UNIXSHELL),true)
HEADLAMP_CONFIG_TRACING_ENABLED=true \
HEADLAMP_CONFIG_ENABLE_HELM=true \
HEADLAMP_CONFIG_ENABLE_DYNAMIC_CLUSTERS=true \
HEADLAMP_CONFIG_ALLOW_KUBECONFIG_REMOVAL=true \
./backend/headlamp-server -dev -proxy-urls https://artifacthub.io/* -listen-addr=localhost
else
@echo "**** Running on Windows without bash or zsh. ****"
@cmd /c "set HEADLAMP_BACKEND_TOKEN=headlamp&& set HEADLAMP_CONFIG_TRACING_ENABLED=true&& set HEADLAMP_CONFIG_ENABLE_HELM=true&& set HEADLAMP_CONFIG_ENABLE_DYNAMIC_CLUSTERS=true&& backend\headlamp-server -dev -proxy-urls https://artifacthub.io/* -listen-addr=localhost"
@cmd /c "set HEADLAMP_BACKEND_TOKEN=headlamp&& set HEADLAMP_CONFIG_TRACING_ENABLED=true&& set HEADLAMP_CONFIG_ENABLE_HELM=true&& set HEADLAMP_CONFIG_ENABLE_DYNAMIC_CLUSTERS=true&& set HEADLAMP_CONFIG_ALLOW_KUBECONFIG_REMOVAL=true&& backend\headlamp-server -dev -proxy-urls https://artifacthub.io/* -listen-addr=localhost"
endif

run-frontend:
Expand Down
8 changes: 8 additions & 0 deletions app/electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,14 @@ async function startServer(flags: string[] = []): Promise<ChildProcessWithoutNul
process.env.HEADLAMP_CONFIG_ENABLE_HELM = 'true';
process.env.HEADLAMP_CONFIG_ENABLE_DYNAMIC_CLUSTERS = 'true';

// In headless mode the app runs in the browser (not Electron), so the
// frontend's isElectron() check won't apply. We explicitly enable
// kubeconfig removal here because headless shares the same single-user
// security context as the desktop app.
if (isHeadlessMode) {
process.env.HEADLAMP_CONFIG_ALLOW_KUBECONFIG_REMOVAL = 'true';
}

// Pass a token to the backend that can be used for auth on some routes
process.env.HEADLAMP_BACKEND_TOKEN = backendToken;

Expand Down
7 changes: 6 additions & 1 deletion backend/cmd/headlamp.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ const (
type clientConfig struct {
Clusters []Cluster `json:"clusters"`
IsDynamicClusterEnabled bool `json:"isDynamicClusterEnabled"`
AllowKubeconfigRemoval bool `json:"allowKubeconfigRemoval"`
}

type OauthConfig struct {
Expand Down Expand Up @@ -1750,7 +1751,11 @@ func parseClusterFromKubeConfig(kubeConfigs []string) ([]Cluster, []error) {
func (c *HeadlampConfig) getConfig(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")

clientConfig := clientConfig{c.getClusters(), c.EnableDynamicClusters}
clientConfig := clientConfig{
Clusters: c.getClusters(),
IsDynamicClusterEnabled: c.EnableDynamicClusters,
AllowKubeconfigRemoval: c.AllowKubeconfigRemoval,
}

if err := json.NewEncoder(w).Encode(&clientConfig); err != nil {
logger.Log(logger.LevelError, nil, err, "encoding config")
Expand Down
6 changes: 5 additions & 1 deletion backend/cmd/stateless.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,11 @@ func (c *HeadlampConfig) parseKubeConfig(w http.ResponseWriter, r *http.Request)
return
}

clientConfig := clientConfig{contexts, c.EnableDynamicClusters}
clientConfig := clientConfig{
Clusters: contexts,
IsDynamicClusterEnabled: c.EnableDynamicClusters,
AllowKubeconfigRemoval: c.AllowKubeconfigRemoval,
}

if err := json.NewEncoder(w).Encode(&clientConfig); err != nil {
logger.Log(logger.LevelError, nil, err, "encoding config")
Expand Down
2 changes: 2 additions & 0 deletions backend/pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ type Config struct {
CacheEnabled bool `koanf:"cache-enabled"`
EnableHelm bool `koanf:"enable-helm"`
EnableDynamicClusters bool `koanf:"enable-dynamic-clusters"`
AllowKubeconfigRemoval bool `koanf:"allow-kubeconfig-removal"`
ListenAddr string `koanf:"listen-addr"`
WatchPluginsChanges bool `koanf:"watch-plugins-changes"`
Port uint `koanf:"port"`
Expand Down Expand Up @@ -433,6 +434,7 @@ func addGeneralFlags(f *flag.FlagSet) {
f.Bool("insecure-ssl", false, "Accept/Ignore all server SSL certificates")
f.String("log-level", "info", "Set backend log verbosity. Options: debug, info (default), warn, error")
f.Bool("enable-dynamic-clusters", false, "Enable dynamic clusters, which stores stateless clusters in the frontend.")
f.Bool("allow-kubeconfig-removal", false, "Allow users to remove clusters from their kubeconfig via the UI")
// Note: When running in-cluster and if not explicitly set, this flag defaults to false.
f.Bool("watch-plugins-changes", true, "Reloads plugins when there are changes to them or their directory")

Expand Down
49 changes: 25 additions & 24 deletions backend/pkg/headlampconfig/headlampConfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,28 +39,29 @@ type HeadlampConfig struct {
}

type HeadlampCFG struct {
UseInCluster bool
InClusterContextName string
ListenAddr string
CacheEnabled bool
DevMode bool
Insecure bool
EnableHelm bool
EnableDynamicClusters bool
WatchPluginsChanges bool
Port uint
KubeConfigPath string
SkippedKubeContexts string
StaticDir string
PluginDir string
UserPluginDir string
StaticPluginDir string
KubeConfigStore kubeconfig.ContextStore
Telemetry *telemetry.Telemetry
Metrics *telemetry.Metrics
BaseURL string
ProxyURLs []string
TLSCertPath string
TLSKeyPath string
SessionTTL int
UseInCluster bool
InClusterContextName string
ListenAddr string
CacheEnabled bool
DevMode bool
Insecure bool
EnableHelm bool
EnableDynamicClusters bool
AllowKubeconfigRemoval bool
WatchPluginsChanges bool
Port uint
KubeConfigPath string
SkippedKubeContexts string
StaticDir string
PluginDir string
UserPluginDir string
StaticPluginDir string
KubeConfigStore kubeconfig.ContextStore
Telemetry *telemetry.Telemetry
Metrics *telemetry.Metrics
BaseURL string
ProxyURLs []string
TLSCertPath string
TLSKeyPath string
SessionTTL int
}
9 changes: 6 additions & 3 deletions frontend/src/components/App/Home/ClusterContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ export default function ClusterContextMenu({ cluster }: ClusterContextMenuProps)
const [openConfirmDialog, setOpenConfirmDialog] = React.useState<string | null>(null);
const dialogs = useTypedSelector(state => state.clusterProvider.dialogs);
const menuItems = useTypedSelector(state => state.clusterProvider.menuItems);
const isDynamicClusterEnabled = useTypedSelector(state => state.config.isDynamicClusterEnabled);
const allowKubeconfigRemoval = useTypedSelector(state => state.config.allowKubeconfigRemoval);

const kubeconfigOrigin = cluster.meta_data?.origin?.kubeconfig;
const deleteFromKubeconfig = cluster.meta_data?.source === 'kubeconfig';
Expand Down Expand Up @@ -154,9 +156,10 @@ export default function ClusterContextMenu({ cluster }: ClusterContextMenuProps)
<ListItemText>{t('translation|Settings')}</ListItemText>
</MenuItem>
{(!menuItems || menuItems.length === 0) &&
helpers.isElectron() &&
(cluster.meta_data?.source === 'dynamic_cluster' ||
cluster.meta_data?.source === 'kubeconfig') && (
((cluster.meta_data?.source === 'dynamic_cluster' &&
(helpers.isElectron() || isDynamicClusterEnabled)) ||
(cluster.meta_data?.source === 'kubeconfig' &&
(helpers.isElectron() || allowKubeconfigRemoval))) && (
<MenuItem
onClick={() => {
setOpenConfirmDialog('deleteDynamic');
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/project/NewProjectPopup.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ const makeStore = () => {
timezone: 'UTC',
useEvict: true,
},
isDynamicClusterEnabled: false,
allowKubeconfigRemoval: false,
},
projects: {
headerActions: {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ const makeStore = () => {
'cluster-a': { name: 'cluster-a' },
'cluster-b': { name: 'cluster-b' },
} as any,
isDynamicClusterEnabled: true,
allowKubeconfigRemoval: false,
settings: {
tableRowsPerPageOptions: [15, 25, 50],
timezone: 'UTC',
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/helpers/getHeadlampAPIHeaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,12 @@
*
* The app also sets HEADLAMP_BACKEND_TOKEN in the headlamp-server environment,
* which the server checks to validate requests containing this same token.
*
* For development, when running the frontend separately from the backend,
* the token can be initialized from the REACT_APP_HEADLAMP_BACKEND_TOKEN
* environment variable to authenticate API requests.
*/
let backendToken: string | null = null;
let backendToken: string | null = import.meta.env.REACT_APP_HEADLAMP_BACKEND_TOKEN || null;

/**
* Sets the backend token to use when making API calls from Headlamp when running as an app.
Expand Down
30 changes: 30 additions & 0 deletions frontend/src/redux/configSlice.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,36 @@ describe('configSlice', () => {
expect(nextState.clusters).toEqual(clusters);
});

it('should handle setConfig with isDynamicClusterEnabled', () => {
const clusters: ConfigState['clusters'] = {
'cluster-1': { name: 'cluster-1' } as Cluster,
};
const nextState = configReducer(
initialState,
setConfig({ clusters, isDynamicClusterEnabled: true })
);
expect(nextState.clusters).toEqual(clusters);
expect(nextState.isDynamicClusterEnabled).toBe(true);
});

it('should preserve isDynamicClusterEnabled when setConfig is called without it', () => {
let state = configReducer(
initialState,
setConfig({ clusters: {}, isDynamicClusterEnabled: true })
);

expect(state.isDynamicClusterEnabled).toBe(true);

const newClusters: ConfigState['clusters'] = {
'cluster-1': { name: 'cluster-1' } as Cluster,
};

state = configReducer(state, setConfig({ clusters: newClusters }));

expect(state.clusters).toEqual(newClusters);
expect(state.isDynamicClusterEnabled).toBe(true);
});

it('should handle setStatelessConfig', () => {
const statelessClusters: ConfigState['statelessClusters'] = {
'stateless-1': { name: 'stateless-1' } as Cluster,
Expand Down
28 changes: 27 additions & 1 deletion frontend/src/redux/configSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,17 @@ export interface ConfigState {
allClusters: {
[clusterName: string]: Cluster;
} | null;
/**
* Whether dynamic clusters are enabled.
* When true, users can add and delete clusters dynamically.
*/
isDynamicClusterEnabled: boolean;
/**
* Whether users are allowed to remove clusters from their kubeconfig.
* When true, the UI will show options to delete kubeconfig-sourced clusters.
* Defaults to false to prevent accidental removal in company-deployed environments.
*/
allowKubeconfigRemoval: boolean;
/**
* Settings is a map of settings names to settings values.
*/
Expand Down Expand Up @@ -71,6 +82,8 @@ export const initialState: ConfigState = {
clusters: null,
statelessClusters: null,
allClusters: null,
isDynamicClusterEnabled: false,
allowKubeconfigRemoval: false,
settings: {
tableRowsPerPageOptions:
storedSettings.tableRowsPerPageOptions || defaultTableRowsPerPageOptions,
Expand All @@ -89,8 +102,21 @@ const configSlice = createSlice({
* @param state - The current state.
* @param action - The payload action containing the config.
*/
setConfig(state, action: PayloadAction<{ clusters: ConfigState['clusters'] }>) {
setConfig(
state,
action: PayloadAction<{
clusters: ConfigState['clusters'];
isDynamicClusterEnabled?: boolean;
allowKubeconfigRemoval?: boolean;
}>
) {
state.clusters = action.payload.clusters;
if (action.payload.isDynamicClusterEnabled !== undefined) {
state.isDynamicClusterEnabled = action.payload.isDynamicClusterEnabled;
}
if (action.payload.allowKubeconfigRemoval !== undefined) {
state.allowKubeconfigRemoval = action.payload.allowKubeconfigRemoval;
}
},
/**
* Save the config. To both the store, and localStorage.
Expand Down
Loading