Skip to content
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

feat: Connect to multicluster deployments and validate remoteConfigs #1141

7 changes: 7 additions & 0 deletions src/commands/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import path from 'path';
import * as constants from '../core/constants.js';
import fs from 'fs';
import {Task} from '../core/task.js';

export interface CommandHandlers {
parent: BaseCommand;
Expand Down Expand Up @@ -251,4 +252,10 @@

return dirs;
}

setupHomeDirectoryTask() {
return new Task('Setup home directory', async () => {
this.setupHomeDirectory();
});
}

Check warning on line 260 in src/commands/base.ts

View check run for this annotation

Codecov / codecov/patch

src/commands/base.ts#L257-L260

Added lines #L257 - L260 were not covered by tests
}
3 changes: 2 additions & 1 deletion src/commands/cluster/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,11 @@
const action = this.parent.commandActionBuilder(
[
this.tasks.initialize(argv, connectConfigBuilder.bind(this)),
this.tasks.setupHomeDirectory(),
this.parent.setupHomeDirectoryTask(),

Check warning on line 46 in src/commands/cluster/handlers.ts

View check run for this annotation

Codecov / codecov/patch

src/commands/cluster/handlers.ts#L46

Added line #L46 was not covered by tests
this.parent.getLocalConfig().promptLocalConfigTask(this.parent.getK8()),
this.tasks.selectContext(argv),
RemoteConfigTasks.loadRemoteConfig.bind(this)(argv),
this.tasks.readClustersFromRemoteConfig(argv),

Check warning on line 50 in src/commands/cluster/handlers.ts

View check run for this annotation

Codecov / codecov/patch

src/commands/cluster/handlers.ts#L50

Added line #L50 was not covered by tests
this.tasks.updateLocalConfig(argv),
],
{
Expand Down
123 changes: 109 additions & 14 deletions src/commands/cluster/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,15 @@
import path from 'path';
import chalk from 'chalk';
import {ListrLease} from '../../core/lease/listr_lease.js';
import {ErrorMessages} from '../../core/error_messages.js';
import {SoloError} from '../../core/errors.js';
import {type Context} from '@kubernetes/client-node';
import {RemoteConfigManager} from '../../core/config/remote/remote_config_manager.js';
import {type RemoteConfigDataWrapper} from '../../core/config/remote/remote_config_data_wrapper.js';
import {type K8} from '../../core/k8.js';
import {ListrEnquirerPromptAdapter} from '@listr2/prompt-adapter-enquirer';
import {type LocalConfig} from '../../core/config/local_config.js';
import {type Cluster} from '@kubernetes/client-node/dist/config_types.js';

export class ClusterCommandTasks {
private readonly parent: BaseCommand;
Expand All @@ -37,6 +44,80 @@
this.parent = parent;
}

testConnectionToCluster(cluster: string, localConfig: LocalConfig, parentTask: ListrTaskWrapper<Context, any, any>) {
const self = this;
return {
title: `Test connection to cluster: ${chalk.cyan(cluster)}`,
task: async (_, subTask: ListrTaskWrapper<Context, any, any>) => {
let context = localConfig.clusterContextMapping[cluster];
if (!context) {
const isQuiet = self.parent.getConfigManager().getFlag(flags.quiet);
if (isQuiet) {
context = self.parent.getK8().getKubeConfig().currentContext;

Check warning on line 56 in src/commands/cluster/tasks.ts

View check run for this annotation

Codecov / codecov/patch

src/commands/cluster/tasks.ts#L56

Added line #L56 was not covered by tests
} else {
context = await self.promptForContext(parentTask, cluster);
}

localConfig.clusterContextMapping[cluster] = context;
}
if (!(await self.parent.getK8().testClusterConnection(context, cluster))) {
subTask.title = `${subTask.title} - ${chalk.red('Cluster connection failed')}`;
throw new SoloError(`${ErrorMessages.INVALID_CONTEXT_FOR_CLUSTER_DETAILED(context, cluster)}`);
}
},
};
}

validateRemoteConfigForCluster(
cluster: string,
currentCluster: Cluster,
localConfig: LocalConfig,
currentRemoteConfig: RemoteConfigDataWrapper,
) {
const self = this;
return {
title: `Pull and validate remote configuration for cluster: ${chalk.cyan(cluster)}`,
task: async (_, subTask: ListrTaskWrapper<Context, any, any>) => {
const context = localConfig.clusterContextMapping[cluster];
self.parent.getK8().setCurrentContext(context);
const remoteConfigFromOtherCluster = await self.parent.getRemoteConfigManager().get();
if (!RemoteConfigManager.compare(currentRemoteConfig, remoteConfigFromOtherCluster)) {
throw new SoloError(ErrorMessages.REMOTE_CONFIGS_DO_NOT_MATCH(currentCluster.name, cluster));
}
},
};
}

readClustersFromRemoteConfig(argv) {
const self = this;
return {
title: 'Read clusters from remote config',
task: async (ctx, task) => {
const localConfig = this.parent.getLocalConfig();
const currentCluster = this.parent.getK8().getKubeConfig().getCurrentCluster();
const currentRemoteConfig: RemoteConfigDataWrapper = await this.parent.getRemoteConfigManager().get();
const subTasks = [];
const remoteConfigClusters = Object.keys(currentRemoteConfig.clusters);
const otherRemoteConfigClusters: string[] = remoteConfigClusters.filter(c => c !== currentCluster.name);

// Validate connections for the other clusters
for (const cluster of otherRemoteConfigClusters) {
subTasks.push(self.testConnectionToCluster(cluster, localConfig, task));
}

// Pull and validate RemoteConfigs from the other clusters
for (const cluster of otherRemoteConfigClusters) {
subTasks.push(self.validateRemoteConfigForCluster(cluster, currentCluster, localConfig, currentRemoteConfig));
}

return task.newListr(subTasks, {
concurrent: false,
rendererOptions: {collapseSubtasks: false},
});
},
};
}

updateLocalConfig(argv) {
return new Task('Update local configuration', async (ctx: any, task: ListrTaskWrapper<any, any, any>) => {
this.parent.logger.info('Compare local and remote configuration...');
Expand All @@ -48,13 +129,24 @@
const localConfig = this.parent.getLocalConfig();
const localDeployments = localConfig.deployments;
const remoteClusterList = [];
for (const cluster of Object.keys(remoteConfig.clusters)) {
if (localConfig.currentDeploymentName === remoteConfig.clusters[cluster]) {
remoteClusterList.push(cluster);

const namespace = remoteConfig.metadata.name;
localConfig.currentDeploymentName = remoteConfig.metadata.name;

if (localConfig.deployments[namespace]) {
for (const cluster of Object.keys(remoteConfig.clusters)) {
if (localConfig.currentDeploymentName === remoteConfig.clusters[cluster]) {
remoteClusterList.push(cluster);
}
}
ctx.config.clusters = remoteClusterList;
localDeployments[localConfig.currentDeploymentName].clusters = ctx.config.clusters;
} else {
const clusters = Object.keys(remoteConfig.clusters);
localDeployments[namespace] = {clusters};
ctx.config.clusters = clusters;

Check warning on line 147 in src/commands/cluster/tasks.ts

View check run for this annotation

Codecov / codecov/patch

src/commands/cluster/tasks.ts#L145-L147

Added lines #L145 - L147 were not covered by tests
}
ctx.config.clusters = remoteClusterList;
localDeployments[localConfig.currentDeploymentName].clusters = ctx.config.clusters;

localConfig.setDeployments(localDeployments);

const contexts = splitFlagInput(configManager.getFlag(flags.context));
Expand Down Expand Up @@ -170,6 +262,7 @@
const contexts = splitFlagInput(configManager.getFlag(flags.context));
const localConfig = this.parent.getLocalConfig();
let selectedContext;
let selectedCluster;

// If one or more contexts are provided use the first one
if (contexts.length) {
Expand All @@ -179,6 +272,7 @@
// If one or more clusters are provided use the first one to determine the context
// from the mapping in the LocalConfig
else if (clusters.length) {
selectedCluster = clusters[0];
selectedContext = await this.selectContextForFirstCluster(task, clusters, localConfig, isQuiet);
}

Expand All @@ -188,6 +282,7 @@
const deployment = localConfig.deployments[deploymentName];

if (deployment && deployment.clusters.length) {
selectedCluster = deployment.clusters[0];
selectedContext = await this.selectContextForFirstCluster(task, deployment.clusters, localConfig, isQuiet);
}

Expand All @@ -196,7 +291,7 @@
// Add the deployment to the LocalConfig with the currently selected cluster and context in KubeConfig
if (isQuiet) {
selectedContext = this.parent.getK8().getKubeConfig().getCurrentContext();
const selectedCluster = this.parent.getK8().getKubeConfig().getCurrentCluster().name;
selectedCluster = this.parent.getK8().getKubeConfig().getCurrentCluster().name;
localConfig.deployments[deploymentName] = {
clusters: [selectedCluster],
};
Expand All @@ -208,20 +303,26 @@

// Prompt user for clusters and contexts
else {
clusters = splitFlagInput(await flags.clusterName.prompt(task, clusters));
const promptedClusters = await flags.clusterName.prompt(task, '');
clusters = splitFlagInput(promptedClusters);

for (const cluster of clusters) {
if (!localConfig.clusterContextMapping[cluster]) {
localConfig.clusterContextMapping[cluster] = await this.promptForContext(task, cluster);
}
}

selectedCluster = clusters[0];
selectedContext = localConfig.clusterContextMapping[clusters[0]];
}
}
}

this.parent.getK8().getKubeConfig().setCurrentContext(selectedContext);
const connectionValid = await this.parent.getK8().testClusterConnection(selectedContext, selectedCluster);
if (!connectionValid) {
throw new SoloError(ErrorMessages.INVALID_CONTEXT_FOR_CLUSTER(selectedContext));
}
this.parent.getK8().setCurrentContext(selectedContext);
});
}

Expand Down Expand Up @@ -390,10 +491,4 @@
ctx => !ctx.isChartInstalled,
);
}

setupHomeDirectory() {
return new Task('Setup home directory', async () => {
this.parent.setupHomeDirectory();
});
}
}
32 changes: 20 additions & 12 deletions src/commands/deployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,44 +72,52 @@ export class DeploymentCommand extends BaseCommand {
} as Config;

ctx.config.contextCluster = Templates.parseContextCluster(ctx.config.contextClusterUnparsed);

const namespace = ctx.config.namespace;

if (!(await self.k8.hasNamespace(namespace))) {
await self.k8.createNamespace(namespace);
}

self.logger.debug('Prepared config', {config: ctx.config, cachedConfig: self.configManager.config});

return ListrLease.newAcquireLeaseTask(lease, task);
},
},
this.setupHomeDirectoryTask(),
this.localConfig.promptLocalConfigTask(self.k8),
{
title: 'Validate cluster connections',
task: async (ctx, task): Promise<Listr<Context, any, any>> => {
const subTasks = [];

for (const cluster of Object.keys(ctx.config.contextCluster)) {
for (const context of Object.keys(ctx.config.contextCluster)) {
const cluster = ctx.config.contextCluster[context];
subTasks.push({
title: `Testing connection to cluster: ${chalk.cyan(cluster)}`,
task: async (_: Context, task: ListrTaskWrapper<Context, any, any>) => {
if (!(await self.k8.testClusterConnection(cluster))) {
if (!(await self.k8.testClusterConnection(context, cluster))) {
task.title = `${task.title} - ${chalk.red('Cluster connection failed')}`;

throw new SoloError(`Cluster connection failed for: ${cluster}`);
}
},
});
}

return task.newListr(subTasks, {
concurrent: true,
concurrent: false,
rendererOptions: {collapseSubtasks: false},
});
},
},
{
title: 'Create remoteConfig in clusters',
task: async (ctx, task) => {
const subTasks = [];
for (const context of Object.keys(ctx.config.contextCluster)) {
const cluster = ctx.config.contextCluster[context];
subTasks.push(RemoteConfigTasks.createRemoteConfig.bind(this)(cluster, context, ctx.config.namespace));
}

return task.newListr(subTasks, {
concurrent: false,
rendererOptions: {collapseSubtasks: false},
});
},
},
RemoteConfigTasks.createRemoteConfig.bind(this)(),
],
{
concurrent: false,
Expand Down
1 change: 0 additions & 1 deletion src/commands/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,6 @@ export class Flags {
name: 'cluster-name',
definition: {
describe: 'Cluster name',
defaultValue: 'solo-cluster-setup',
alias: 'c',
type: 'string',
},
Expand Down
48 changes: 44 additions & 4 deletions src/core/config/remote/remote_config_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
import {ComponentsDataWrapper} from './components_data_wrapper.js';
import {RemoteConfigValidator} from './remote_config_validator.js';
import {K8} from '../../k8.js';
import type {Cluster, Namespace} from './types.js';
import type {Cluster, Context, Namespace} from './types.js';
import {SoloLogger} from '../../logging.js';
import {ConfigManager} from '../../config_manager.js';
import {LocalConfig} from '../local_config.js';
Expand All @@ -35,6 +35,7 @@
import {StatusCodes} from 'http-status-codes';
import {inject, injectable} from 'tsyringe-neo';
import {patchInject} from '../../container_helper.js';
import {ErrorMessages} from '../../error_messages.js';

interface ListrContext {
config: {contextCluster: ContextClusterStructure};
Expand Down Expand Up @@ -151,6 +152,35 @@
return true;
}

/**
* Loads the remote configuration, performs a validation and returns it
* @returns RemoteConfigDataWrapper
*/
public async get(): Promise<RemoteConfigDataWrapper> {
await this.load();
try {
await RemoteConfigValidator.validateComponents(this.remoteConfig.components, this.k8);
} catch (e) {
throw new SoloError(ErrorMessages.REMOTE_CONFIG_IS_INVALID(this.k8.getKubeConfig().getCurrentCluster().name));
}
return this.remoteConfig;
}

Check warning on line 167 in src/core/config/remote/remote_config_manager.ts

View check run for this annotation

Codecov / codecov/patch

src/core/config/remote/remote_config_manager.ts#L160-L167

Added lines #L160 - L167 were not covered by tests

public static compare(remoteConfig1: RemoteConfigDataWrapper, remoteConfig2: RemoteConfigDataWrapper): boolean {
// Compare clusters
const clusters1 = Object.keys(remoteConfig1.clusters);
const clusters2 = Object.keys(remoteConfig2.clusters);
if (clusters1.length !== clusters2.length) return false;

for (const i in clusters1) {
if (clusters1[i] !== clusters2[i]) {
return false;
}

Check warning on line 178 in src/core/config/remote/remote_config_manager.ts

View check run for this annotation

Codecov / codecov/patch

src/core/config/remote/remote_config_manager.ts#L177-L178

Added lines #L177 - L178 were not covered by tests
}

return true;
}

/* ---------- Listr Task Builders ---------- */

/**
Expand Down Expand Up @@ -197,20 +227,26 @@
*
* @returns a Listr task which creates the remote configuration.
*/
public buildCreateTask(): SoloListrTask<ListrContext> {
public buildCreateTask(cluster: Cluster, context: Context, namespace: Namespace): SoloListrTask<ListrContext> {
const self = this;

return {
title: 'Create remote config',
title: `Create remote config in cluster: ${cluster}`,

Check warning on line 234 in src/core/config/remote/remote_config_manager.ts

View check run for this annotation

Codecov / codecov/patch

src/core/config/remote/remote_config_manager.ts#L234

Added line #L234 was not covered by tests
task: async (_, task): Promise<void> => {
self.k8.setCurrentContext(context);

if (!(await self.k8.hasNamespace(namespace))) {
await self.k8.createNamespace(namespace);
}

Check warning on line 241 in src/core/config/remote/remote_config_manager.ts

View check run for this annotation

Codecov / codecov/patch

src/core/config/remote/remote_config_manager.ts#L236-L241

Added lines #L236 - L241 were not covered by tests
const localConfigExists = this.localConfig.configFileExists();
if (!localConfigExists) {
throw new SoloError("Local config doesn't exist");
}

self.unload();

Check warning on line 247 in src/core/config/remote/remote_config_manager.ts

View check run for this annotation

Codecov / codecov/patch

src/core/config/remote/remote_config_manager.ts#L247

Added line #L247 was not covered by tests
if (await self.load()) {
task.title = `${task.title} - ${chalk.red('Remote config already exists')}}`;

throw new SoloError('Remote config already exists');
}

Expand All @@ -232,6 +268,10 @@
return !!this.remoteConfig;
}

public unload() {
delete this.remoteConfig;
}

Check warning on line 273 in src/core/config/remote/remote_config_manager.ts

View check run for this annotation

Codecov / codecov/patch

src/core/config/remote/remote_config_manager.ts#L272-L273

Added lines #L272 - L273 were not covered by tests

/**
* Retrieves the ConfigMap containing the remote configuration from the Kubernetes cluster.
*
Expand Down
Loading
Loading