Skip to content

Commit a533976

Browse files
committed
feat: duplicate contexts
Signed-off-by: Philippe Martin <[email protected]>
1 parent 15fc149 commit a533976

File tree

5 files changed

+111
-0
lines changed

5 files changed

+111
-0
lines changed

packages/channels/src/interface/contexts-api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,5 @@ export const ContextsApi = Symbol.for('ContextsApi');
2121
export interface ContextsApi {
2222
setCurrentContext(contextName: string): Promise<void>;
2323
deleteContext(contextName: string): Promise<void>;
24+
duplicateContext(contextName: string): Promise<void>;
2425
}

packages/extension/src/manager/contexts-manager.spec.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,3 +591,55 @@ test('deleteContext the current context asks for confirmation, and do nothing if
591591
const kubeconfigFile = fsScreenshot['/path/to/kube/config'];
592592
expect(kubeconfigFile).toEqual('{}');
593593
});
594+
595+
test('should duplicate context from config', async () => {
596+
const kubeConfigPath = '/path/to/kube/config';
597+
vol.fromJSON({
598+
[kubeConfigPath]: '{}',
599+
});
600+
vi.mocked(kubernetes.getKubeconfig).mockReturnValue({
601+
path: kubeConfigPath,
602+
} as Uri);
603+
const contextsManager = new ContextsManager();
604+
const kubeConfig = new KubeConfig();
605+
const kubeconfigFileContent = `{
606+
clusters: [
607+
{
608+
name: 'cluster1',
609+
cluster: {
610+
server: 'https://cluster1.example.com',
611+
},
612+
},
613+
],
614+
users: [
615+
{
616+
name: 'user1',
617+
},
618+
],
619+
contexts: [
620+
{
621+
name: 'context1',
622+
context: {
623+
cluster: 'cluster1',
624+
user: 'user1',
625+
},
626+
},
627+
],
628+
"current-context": "context1"
629+
}`;
630+
kubeConfig.loadFromString(kubeconfigFileContent);
631+
await contextsManager.update(kubeConfig);
632+
633+
expect(contextsManager.getKubeConfig().getContexts()).toHaveLength(1);
634+
635+
await contextsManager.duplicateContext(kubeConfig.contexts[0].name);
636+
let contexts = contextsManager.getKubeConfig().getContexts();
637+
expect(contexts.length).toBe(2);
638+
639+
expect(contexts[1].name).toBe('context1-1');
640+
641+
await contextsManager.duplicateContext(kubeConfig.contexts[0].name);
642+
contexts = contextsManager.getKubeConfig().getContexts();
643+
expect(contexts.length).toBe(3);
644+
expect(contexts[2].name).toBe('context1-2');
645+
});

packages/extension/src/manager/contexts-manager.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,41 @@ export class ContextsManager implements ContextsApi {
7474
await this.deleteContextInternal(contextName);
7575
}
7676

77+
findNewContextName(kubeConfig: KubeConfig, contextName: string): string {
78+
let counter = 1;
79+
let newName = `${contextName}-${counter}`;
80+
// Keep creating new name by adding 1 to name until not existing name is found
81+
while (kubeConfig.contexts.find(context => context.name === newName)) {
82+
counter += 1;
83+
newName = `${contextName}-${counter}`;
84+
}
85+
return newName;
86+
}
87+
88+
async duplicateContext(contextName: string): Promise<void> {
89+
const newConfig = new KubeConfig();
90+
const kubeConfig = this.getKubeConfig();
91+
const newName = this.findNewContextName(kubeConfig, contextName);
92+
const originalContext = kubeConfig.contexts.find(context => context.name === contextName);
93+
if (!originalContext) return;
94+
95+
newConfig.loadFromOptions({
96+
clusters: kubeConfig.clusters,
97+
users: kubeConfig.users,
98+
currentContext: kubeConfig.currentContext,
99+
contexts: [
100+
...kubeConfig.contexts,
101+
{
102+
...originalContext,
103+
name: newName,
104+
},
105+
],
106+
});
107+
108+
await this.update(newConfig);
109+
await this.saveKubeConfig();
110+
}
111+
77112
async deleteContextInternal(contextName: string): Promise<void> {
78113
try {
79114
this.#currentKubeConfig = this.removeContext(this.#currentKubeConfig, contextName);

packages/webview/src/component/ContextCard.svelte

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { Cluster, User } from '@kubernetes/client-node';
33
import ContextCardLine from '/@/component/ContextCardLine.svelte';
44
import SetCurrentContextAction from '/@/component/actions/SetCurrentContextAction.svelte';
55
import DeleteContextAction from '/@/component/actions/DeleteContextAction.svelte';
6+
import DuplicateContextAction from '/@/component/actions/DuplicateContextAction.svelte';
67
78
interface Props {
89
cluster: Cluster;
@@ -31,6 +32,7 @@ const { cluster, user, name, namespace, currentContext, icon }: Props = $props()
3132
{#if !currentContext}
3233
<SetCurrentContextAction name={name} />
3334
{/if}
35+
<DuplicateContextAction name={name} />
3436
<DeleteContextAction name={name} />
3537
</div>
3638
</div>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<script lang="ts">
2+
import { faCopy } from '@fortawesome/free-solid-svg-icons';
3+
import IconButton from '/@/component/button/IconButton.svelte';
4+
import { Remote } from '/@/remote/remote';
5+
import { getContext } from 'svelte';
6+
import { API_CONTEXTS } from '@kubernetes-contexts/channels';
7+
8+
interface Props {
9+
name: string;
10+
}
11+
const { name }: Props = $props();
12+
13+
const remote = getContext<Remote>(Remote);
14+
const contextsApi = remote.getProxy(API_CONTEXTS);
15+
16+
async function duplicateContext(): Promise<void> {
17+
await contextsApi.duplicateContext(name);
18+
}
19+
</script>
20+
21+
<IconButton title="Duplicate Context" icon={faCopy} onClick={duplicateContext} />

0 commit comments

Comments
 (0)