@kustodian/plugin-k0s 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@kustodian/plugin-k0s",
3
+ "version": "1.0.0",
4
+ "description": "k0s cluster provider plugin for Kustodian",
5
+ "type": "module",
6
+ "main": "./src/index.ts",
7
+ "types": "./src/index.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./src/index.ts",
11
+ "import": "./src/index.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "src"
16
+ ],
17
+ "scripts": {
18
+ "test": "bun test",
19
+ "test:watch": "bun test --watch",
20
+ "typecheck": "bun run tsc --noEmit"
21
+ },
22
+ "keywords": [
23
+ "kustodian",
24
+ "plugin",
25
+ "k0s",
26
+ "kubernetes",
27
+ "cluster"
28
+ ],
29
+ "author": "Luca Silverentand <luca@onezero.company>",
30
+ "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/lucasilverentand/kustodian.git",
34
+ "directory": "plugins/k0s"
35
+ },
36
+ "publishConfig": {
37
+ "registry": "https://npm.pkg.github.com"
38
+ },
39
+ "dependencies": {
40
+ "@kustodian/core": "workspace:*",
41
+ "@kustodian/nodes": "workspace:*",
42
+ "@kustodian/plugins": "workspace:*",
43
+ "yaml": "^2.8.2"
44
+ },
45
+ "devDependencies": {}
46
+ }
package/src/config.ts ADDED
@@ -0,0 +1,84 @@
1
+ import type { NodeListType, NodeType, SshConfigType } from '@kustodian/nodes';
2
+ import { get_node_ssh_config, get_primary_controller } from '@kustodian/nodes';
3
+
4
+ import {
5
+ type K0sProviderOptionsType,
6
+ type K0sctlConfigType,
7
+ type K0sctlHostType,
8
+ to_k0sctl_role,
9
+ to_k0sctl_ssh_config,
10
+ } from './types.js';
11
+
12
+ /**
13
+ * Generates a k0sctl configuration from node definitions.
14
+ */
15
+ export function generate_k0sctl_config(
16
+ node_list: NodeListType,
17
+ options: K0sProviderOptionsType = {},
18
+ ): K0sctlConfigType {
19
+ const primary_controller = get_primary_controller(node_list.nodes);
20
+ const default_ssh = options.default_ssh ?? node_list.ssh;
21
+
22
+ const hosts: K0sctlHostType[] = node_list.nodes.map((node) =>
23
+ node_to_k0sctl_host(node, default_ssh),
24
+ );
25
+
26
+ return {
27
+ apiVersion: 'k0sctl.k0sproject.io/v1beta1',
28
+ kind: 'Cluster',
29
+ metadata: {
30
+ name: node_list.cluster,
31
+ },
32
+ spec: {
33
+ k0s: build_k0s_config(primary_controller, options),
34
+ hosts,
35
+ },
36
+ };
37
+ }
38
+
39
+ /**
40
+ * Converts a node to a k0sctl host configuration.
41
+ */
42
+ export function node_to_k0sctl_host(node: NodeType, default_ssh?: SshConfigType): K0sctlHostType {
43
+ const ssh_config = get_node_ssh_config(node, default_ssh);
44
+ const role = to_k0sctl_role(node.role);
45
+
46
+ return {
47
+ role,
48
+ noTaints: role === 'controller+worker' ? true : undefined,
49
+ openSSH: to_k0sctl_ssh_config(node.address, ssh_config),
50
+ };
51
+ }
52
+
53
+ /**
54
+ * Builds the k0s configuration block.
55
+ */
56
+ function build_k0s_config(
57
+ primary_controller: NodeType | undefined,
58
+ options: K0sProviderOptionsType,
59
+ ) {
60
+ return {
61
+ version: options.k0s_version,
62
+ dynamicConfig: options.dynamic_config,
63
+ config: {
64
+ spec: {
65
+ api: primary_controller
66
+ ? {
67
+ external_address: primary_controller.address,
68
+ }
69
+ : undefined,
70
+ telemetry: {
71
+ enabled: options.telemetry_enabled ?? false,
72
+ },
73
+ },
74
+ },
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Serializes k0sctl config to YAML-compatible object.
80
+ * Removes undefined values for clean output.
81
+ */
82
+ export function serialize_k0sctl_config(config: K0sctlConfigType): unknown {
83
+ return JSON.parse(JSON.stringify(config));
84
+ }
@@ -0,0 +1,164 @@
1
+ import * as child_process from 'node:child_process';
2
+ import * as util from 'node:util';
3
+
4
+ import { Errors, type ResultType, failure, success } from '@kustodian/core';
5
+ import type { KustodianErrorType } from '@kustodian/core';
6
+
7
+ const exec_async = util.promisify(child_process.exec);
8
+
9
+ /**
10
+ * Result of a command execution.
11
+ */
12
+ export interface CommandResultType {
13
+ stdout: string;
14
+ stderr: string;
15
+ exit_code: number;
16
+ }
17
+
18
+ /**
19
+ * Options for command execution.
20
+ */
21
+ export interface ExecOptionsType {
22
+ cwd?: string | undefined;
23
+ timeout?: number | undefined;
24
+ env?: Record<string, string> | undefined;
25
+ }
26
+
27
+ /**
28
+ * Executes a shell command and returns the result.
29
+ */
30
+ export async function exec_command(
31
+ command: string,
32
+ args: string[] = [],
33
+ options: ExecOptionsType = {},
34
+ ): Promise<ResultType<CommandResultType, KustodianErrorType>> {
35
+ const full_command = [command, ...args].join(' ');
36
+
37
+ try {
38
+ const { stdout, stderr } = await exec_async(full_command, {
39
+ cwd: options.cwd,
40
+ timeout: options.timeout,
41
+ env: { ...process.env, ...options.env },
42
+ });
43
+
44
+ return success({
45
+ stdout,
46
+ stderr,
47
+ exit_code: 0,
48
+ });
49
+ } catch (error) {
50
+ if (is_exec_error(error)) {
51
+ return success({
52
+ stdout: error.stdout ?? '',
53
+ stderr: error.stderr ?? '',
54
+ exit_code: error.code ?? 1,
55
+ });
56
+ }
57
+
58
+ return failure(Errors.unknown(`Failed to execute command: ${full_command}`, error));
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Type guard for exec errors.
64
+ */
65
+ function is_exec_error(
66
+ error: unknown,
67
+ ): error is { stdout?: string; stderr?: string; code?: number } {
68
+ return (
69
+ typeof error === 'object' &&
70
+ error !== null &&
71
+ ('stdout' in error || 'stderr' in error || 'code' in error)
72
+ );
73
+ }
74
+
75
+ /**
76
+ * Checks if k0sctl is available in the system PATH.
77
+ */
78
+ export async function check_k0sctl_available(): Promise<ResultType<string, KustodianErrorType>> {
79
+ const result = await exec_command('k0sctl', ['version']);
80
+
81
+ if (!result.success) {
82
+ return result;
83
+ }
84
+
85
+ if (result.value.exit_code !== 0) {
86
+ return failure(
87
+ Errors.bootstrap_error(
88
+ 'k0sctl not found. Please install k0sctl: https://github.com/k0sproject/k0sctl',
89
+ ),
90
+ );
91
+ }
92
+
93
+ // Parse version from output (e.g., "version: v0.19.4")
94
+ const version_match = result.value.stdout.match(/version:\s*v?([\d.]+)/);
95
+ const version = version_match?.[1] ?? 'unknown';
96
+
97
+ return success(version);
98
+ }
99
+
100
+ /**
101
+ * Runs k0sctl apply with the given config file.
102
+ */
103
+ export async function k0sctl_apply(
104
+ config_path: string,
105
+ options: ExecOptionsType = {},
106
+ ): Promise<ResultType<CommandResultType, KustodianErrorType>> {
107
+ const result = await exec_command('k0sctl', ['apply', '--config', config_path], options);
108
+
109
+ if (!result.success) {
110
+ return result;
111
+ }
112
+
113
+ if (result.value.exit_code !== 0) {
114
+ return failure(Errors.bootstrap_error(`k0sctl apply failed: ${result.value.stderr}`));
115
+ }
116
+
117
+ return result;
118
+ }
119
+
120
+ /**
121
+ * Runs k0sctl kubeconfig to get the cluster kubeconfig.
122
+ */
123
+ export async function k0sctl_kubeconfig(
124
+ config_path: string,
125
+ options: ExecOptionsType = {},
126
+ ): Promise<ResultType<string, KustodianErrorType>> {
127
+ const result = await exec_command('k0sctl', ['kubeconfig', '--config', config_path], options);
128
+
129
+ if (!result.success) {
130
+ return result;
131
+ }
132
+
133
+ if (result.value.exit_code !== 0) {
134
+ return failure(Errors.bootstrap_error(`k0sctl kubeconfig failed: ${result.value.stderr}`));
135
+ }
136
+
137
+ return success(result.value.stdout);
138
+ }
139
+
140
+ /**
141
+ * Runs k0sctl reset to tear down the cluster.
142
+ */
143
+ export async function k0sctl_reset(
144
+ config_path: string,
145
+ force = false,
146
+ options: ExecOptionsType = {},
147
+ ): Promise<ResultType<CommandResultType, KustodianErrorType>> {
148
+ const args = ['reset', '--config', config_path];
149
+ if (force) {
150
+ args.push('--force');
151
+ }
152
+
153
+ const result = await exec_command('k0sctl', args, options);
154
+
155
+ if (!result.success) {
156
+ return result;
157
+ }
158
+
159
+ if (result.value.exit_code !== 0) {
160
+ return failure(Errors.bootstrap_error(`k0sctl reset failed: ${result.value.stderr}`));
161
+ }
162
+
163
+ return result;
164
+ }
package/src/index.ts ADDED
@@ -0,0 +1,38 @@
1
+ // Plugin exports
2
+ export { create_k0s_plugin, plugin } from './plugin.js';
3
+ export { plugin as default } from './plugin.js';
4
+
5
+ // Provider exports
6
+ export { create_k0s_provider, validate_k0s_config } from './provider.js';
7
+
8
+ // Config generation
9
+ export { generate_k0sctl_config, node_to_k0sctl_host, serialize_k0sctl_config } from './config.js';
10
+
11
+ // Executor
12
+ export {
13
+ check_k0sctl_available,
14
+ exec_command,
15
+ k0sctl_apply,
16
+ k0sctl_kubeconfig,
17
+ k0sctl_reset,
18
+ type CommandResultType,
19
+ type ExecOptionsType,
20
+ } from './executor.js';
21
+
22
+ // Types
23
+ export {
24
+ to_k0sctl_role,
25
+ to_k0sctl_ssh_config,
26
+ type K0sApiConfigType,
27
+ type K0sConfigSpecType,
28
+ type K0sctlConfigType,
29
+ type K0sctlHostRoleType,
30
+ type K0sctlHostType,
31
+ type K0sctlK0sConfigType,
32
+ type K0sctlMetadataType,
33
+ type K0sctlSpecType,
34
+ type K0sctlSshConfigType,
35
+ type K0sProviderOptionsType,
36
+ type K0sTelemetryConfigType,
37
+ type K0sVersionType,
38
+ } from './types.js';
package/src/plugin.ts ADDED
@@ -0,0 +1,82 @@
1
+ import { success } from '@kustodian/core';
2
+ import type {
3
+ CommandType,
4
+ HookContextType,
5
+ HookEventType,
6
+ KustodianPluginType,
7
+ PluginCommandContributionType,
8
+ PluginHookContributionType,
9
+ PluginManifestType,
10
+ } from '@kustodian/plugins';
11
+
12
+ import { create_k0s_provider } from './provider.js';
13
+ import type { K0sProviderOptionsType } from './types.js';
14
+
15
+ /**
16
+ * k0s plugin manifest.
17
+ */
18
+ const manifest: PluginManifestType = {
19
+ name: '@kustodian/plugin-k0s',
20
+ version: '0.1.0',
21
+ description: 'k0s cluster provider for Kustodian',
22
+ capabilities: ['commands', 'hooks'],
23
+ };
24
+
25
+ /**
26
+ * Creates the k0s plugin.
27
+ */
28
+ export function create_k0s_plugin(options: K0sProviderOptionsType = {}): KustodianPluginType {
29
+ const provider = create_k0s_provider(options);
30
+
31
+ return {
32
+ manifest,
33
+
34
+ async activate() {
35
+ return success(undefined);
36
+ },
37
+
38
+ async deactivate() {
39
+ return success(undefined);
40
+ },
41
+
42
+ get_commands(): PluginCommandContributionType[] {
43
+ const k0s_command: CommandType = {
44
+ name: 'k0s',
45
+ description: 'k0s cluster management commands',
46
+ subcommands: [
47
+ {
48
+ name: 'info',
49
+ description: 'Show k0s provider information',
50
+ handler: async () => {
51
+ console.log('k0s cluster provider');
52
+ console.log('Provider name:', provider.name);
53
+ return success(undefined);
54
+ },
55
+ },
56
+ ],
57
+ };
58
+ return [{ command: k0s_command }];
59
+ },
60
+
61
+ get_hooks(): PluginHookContributionType[] {
62
+ return [
63
+ {
64
+ event: 'bootstrap:before',
65
+ priority: 100,
66
+ handler: async (_event: HookEventType, ctx: HookContextType) => {
67
+ // Could add k0s-specific pre-bootstrap validation here
68
+ return success(ctx);
69
+ },
70
+ },
71
+ ];
72
+ },
73
+ };
74
+ }
75
+
76
+ /**
77
+ * Default plugin export.
78
+ * This is loaded when the plugin is discovered and imported.
79
+ */
80
+ export const plugin = create_k0s_plugin();
81
+
82
+ export default plugin;
@@ -0,0 +1,176 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as os from 'node:os';
3
+ import * as path from 'node:path';
4
+
5
+ import { Errors, type ResultType, failure, is_success, success } from '@kustodian/core';
6
+ import type { KustodianErrorType } from '@kustodian/core';
7
+ import type { NodeListType } from '@kustodian/nodes';
8
+ import { get_controllers } from '@kustodian/nodes';
9
+ import type {
10
+ BootstrapOptionsType,
11
+ ClusterProviderType,
12
+ ResetOptionsType,
13
+ } from '@kustodian/plugins';
14
+ import YAML from 'yaml';
15
+
16
+ import { generate_k0sctl_config, serialize_k0sctl_config } from './config.js';
17
+ import {
18
+ check_k0sctl_available,
19
+ k0sctl_apply,
20
+ k0sctl_kubeconfig,
21
+ k0sctl_reset,
22
+ } from './executor.js';
23
+ import type { K0sProviderOptionsType } from './types.js';
24
+
25
+ /**
26
+ * Validates k0s cluster configuration.
27
+ */
28
+ export function validate_k0s_config(node_list: NodeListType): ResultType<void, KustodianErrorType> {
29
+ const controllers = get_controllers(node_list.nodes);
30
+
31
+ if (controllers.length === 0) {
32
+ return failure(Errors.validation_error('k0s cluster requires at least one controller node'));
33
+ }
34
+
35
+ // Validate SSH configuration is present
36
+ for (const node of node_list.nodes) {
37
+ const ssh = node.ssh ?? node_list.ssh;
38
+ if (!ssh?.user) {
39
+ return failure(
40
+ Errors.validation_error(`Node '${node.name}' requires SSH user configuration`),
41
+ );
42
+ }
43
+ }
44
+
45
+ return success(undefined);
46
+ }
47
+
48
+ /**
49
+ * Writes k0sctl config to a temporary file.
50
+ */
51
+ async function write_k0sctl_config(
52
+ config: unknown,
53
+ cluster_name: string,
54
+ ): Promise<ResultType<string, KustodianErrorType>> {
55
+ const temp_dir = os.tmpdir();
56
+ const config_path = path.join(temp_dir, `k0sctl-${cluster_name}.yaml`);
57
+
58
+ try {
59
+ const yaml_content = YAML.stringify(config);
60
+ await fs.writeFile(config_path, yaml_content, 'utf-8');
61
+ return success(config_path);
62
+ } catch (error) {
63
+ return failure(Errors.file_write_error(config_path, error));
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Creates the k0s cluster provider.
69
+ */
70
+ export function create_k0s_provider(options: K0sProviderOptionsType = {}): ClusterProviderType {
71
+ let config_path: string | undefined;
72
+
73
+ return {
74
+ name: 'k0s',
75
+
76
+ validate(node_list: NodeListType): ResultType<void, KustodianErrorType> {
77
+ return validate_k0s_config(node_list);
78
+ },
79
+
80
+ async install(
81
+ node_list: NodeListType,
82
+ bootstrap_options: BootstrapOptionsType,
83
+ ): Promise<ResultType<void, KustodianErrorType>> {
84
+ // Check k0sctl is available
85
+ const k0sctl_check = await check_k0sctl_available();
86
+ if (!is_success(k0sctl_check)) {
87
+ return k0sctl_check;
88
+ }
89
+
90
+ // Generate k0sctl config
91
+ const k0sctl_config = generate_k0sctl_config(node_list, options);
92
+ const serialized = serialize_k0sctl_config(k0sctl_config);
93
+
94
+ // Write config to temp file
95
+ const write_result = await write_k0sctl_config(serialized, node_list.cluster);
96
+ if (!is_success(write_result)) {
97
+ return write_result;
98
+ }
99
+
100
+ config_path = write_result.value;
101
+
102
+ // Skip actual installation in dry run mode
103
+ if (bootstrap_options.dry_run) {
104
+ return success(undefined);
105
+ }
106
+
107
+ // Run k0sctl apply
108
+ const apply_result = await k0sctl_apply(config_path, {
109
+ timeout: bootstrap_options.timeout,
110
+ });
111
+
112
+ if (!is_success(apply_result)) {
113
+ return apply_result;
114
+ }
115
+
116
+ return success(undefined);
117
+ },
118
+
119
+ async get_kubeconfig(node_list: NodeListType): Promise<ResultType<string, KustodianErrorType>> {
120
+ // If we have a config path from install, use it
121
+ if (config_path) {
122
+ return k0sctl_kubeconfig(config_path);
123
+ }
124
+
125
+ // Otherwise, generate a new config
126
+ const k0sctl_config = generate_k0sctl_config(node_list, options);
127
+ const serialized = serialize_k0sctl_config(k0sctl_config);
128
+
129
+ const write_result = await write_k0sctl_config(serialized, node_list.cluster);
130
+ if (!is_success(write_result)) {
131
+ return write_result;
132
+ }
133
+
134
+ config_path = write_result.value;
135
+ return k0sctl_kubeconfig(config_path);
136
+ },
137
+
138
+ async reset(
139
+ node_list: NodeListType,
140
+ reset_options: ResetOptionsType,
141
+ ): Promise<ResultType<void, KustodianErrorType>> {
142
+ // Check k0sctl is available
143
+ const k0sctl_check = await check_k0sctl_available();
144
+ if (!is_success(k0sctl_check)) {
145
+ return k0sctl_check;
146
+ }
147
+
148
+ // Generate k0sctl config if we don't have one
149
+ if (!config_path) {
150
+ const k0sctl_config = generate_k0sctl_config(node_list, options);
151
+ const serialized = serialize_k0sctl_config(k0sctl_config);
152
+
153
+ const write_result = await write_k0sctl_config(serialized, node_list.cluster);
154
+ if (!is_success(write_result)) {
155
+ return write_result;
156
+ }
157
+
158
+ config_path = write_result.value;
159
+ }
160
+
161
+ // Skip actual reset in dry run mode
162
+ if (reset_options.dry_run) {
163
+ return success(undefined);
164
+ }
165
+
166
+ // Run k0sctl reset
167
+ const reset_result = await k0sctl_reset(config_path, reset_options.force ?? false);
168
+
169
+ if (!is_success(reset_result)) {
170
+ return reset_result;
171
+ }
172
+
173
+ return success(undefined);
174
+ },
175
+ };
176
+ }
package/src/types.ts ADDED
@@ -0,0 +1,132 @@
1
+ import type { SshConfigType } from '@kustodian/nodes';
2
+
3
+ /**
4
+ * k0s version configuration.
5
+ */
6
+ export interface K0sVersionType {
7
+ version?: string;
8
+ dynamic_config?: boolean;
9
+ }
10
+
11
+ /**
12
+ * k0s cluster API configuration.
13
+ */
14
+ export interface K0sApiConfigType {
15
+ external_address?: string | undefined;
16
+ sans?: string[] | undefined;
17
+ }
18
+
19
+ /**
20
+ * k0s telemetry configuration.
21
+ */
22
+ export interface K0sTelemetryConfigType {
23
+ enabled: boolean;
24
+ }
25
+
26
+ /**
27
+ * k0s configuration spec.
28
+ */
29
+ export interface K0sConfigSpecType {
30
+ api?: K0sApiConfigType | undefined;
31
+ telemetry?: K0sTelemetryConfigType | undefined;
32
+ }
33
+
34
+ /**
35
+ * k0s host role in k0sctl configuration.
36
+ */
37
+ export type K0sctlHostRoleType = 'controller' | 'worker' | 'controller+worker' | 'single';
38
+
39
+ /**
40
+ * SSH configuration for k0sctl.
41
+ */
42
+ export interface K0sctlSshConfigType {
43
+ address: string;
44
+ user: string;
45
+ keyPath?: string | undefined;
46
+ port?: number | undefined;
47
+ }
48
+
49
+ /**
50
+ * Host configuration for k0sctl.
51
+ */
52
+ export interface K0sctlHostType {
53
+ role: K0sctlHostRoleType;
54
+ noTaints?: boolean | undefined;
55
+ openSSH: K0sctlSshConfigType;
56
+ }
57
+
58
+ /**
59
+ * k0s configuration block in k0sctl.
60
+ */
61
+ export interface K0sctlK0sConfigType {
62
+ version?: string | undefined;
63
+ dynamicConfig?: boolean | undefined;
64
+ config?:
65
+ | {
66
+ spec?: K0sConfigSpecType | undefined;
67
+ }
68
+ | undefined;
69
+ }
70
+
71
+ /**
72
+ * k0sctl cluster spec.
73
+ */
74
+ export interface K0sctlSpecType {
75
+ k0s?: K0sctlK0sConfigType | undefined;
76
+ hosts: K0sctlHostType[];
77
+ }
78
+
79
+ /**
80
+ * k0sctl cluster metadata.
81
+ */
82
+ export interface K0sctlMetadataType {
83
+ name: string;
84
+ }
85
+
86
+ /**
87
+ * Complete k0sctl configuration.
88
+ */
89
+ export interface K0sctlConfigType {
90
+ apiVersion: 'k0sctl.k0sproject.io/v1beta1';
91
+ kind: 'Cluster';
92
+ metadata: K0sctlMetadataType;
93
+ spec: K0sctlSpecType;
94
+ }
95
+
96
+ /**
97
+ * k0s provider options.
98
+ */
99
+ export interface K0sProviderOptionsType {
100
+ k0s_version?: string | undefined;
101
+ telemetry_enabled?: boolean | undefined;
102
+ dynamic_config?: boolean | undefined;
103
+ default_ssh?: SshConfigType | undefined;
104
+ }
105
+
106
+ /**
107
+ * Converts internal SSH config to k0sctl SSH config format.
108
+ */
109
+ export function to_k0sctl_ssh_config(address: string, ssh?: SshConfigType): K0sctlSshConfigType {
110
+ return {
111
+ address,
112
+ user: ssh?.user ?? 'root',
113
+ keyPath: ssh?.key_path,
114
+ port: ssh?.port,
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Converts internal role to k0sctl role.
120
+ */
121
+ export function to_k0sctl_role(role: string): K0sctlHostRoleType {
122
+ switch (role) {
123
+ case 'controller':
124
+ return 'controller';
125
+ case 'worker':
126
+ return 'worker';
127
+ case 'controller+worker':
128
+ return 'controller+worker';
129
+ default:
130
+ return 'worker';
131
+ }
132
+ }