diff --git a/src/commands/cluster/configs.ts b/src/commands/cluster/configs.ts index 2aea57685..d1f9ece95 100644 --- a/src/commands/cluster/configs.ts +++ b/src/commands/cluster/configs.ts @@ -20,6 +20,7 @@ import {Flags as flags} from '../flags.js'; import * as constants from '../../core/constants.js'; import {ListrEnquirerPromptAdapter} from '@listr2/prompt-adapter-enquirer'; import {SoloError} from '../../core/errors.js'; +import {type Namespace} from '../../core/config/remote/types.js'; export const CONNECT_CONFIGS_NAME = 'connectConfig'; @@ -123,3 +124,13 @@ export interface ClusterResetConfigClass { clusterName: string; clusterSetupNamespace: string; } + +export interface SelectClusterContextContext { + config: { + quiet: boolean; + namespace: Namespace; + clusterName: string; + context: string; + clusters: string[]; + }; +} diff --git a/src/commands/cluster/handlers.ts b/src/commands/cluster/handlers.ts index b1dd0b6c6..a36c475a2 100644 --- a/src/commands/cluster/handlers.ts +++ b/src/commands/cluster/handlers.ts @@ -45,9 +45,9 @@ export class ClusterCommandHandlers implements CommandHandlers { this.tasks.initialize(argv, connectConfigBuilder.bind(this)), this.tasks.setupHomeDirectory(), this.parent.getLocalConfig().promptLocalConfigTask(this.parent.getK8()), - this.tasks.selectContext(argv), + this.tasks.selectContext(), RemoteConfigTasks.loadRemoteConfig.bind(this)(argv), - this.tasks.updateLocalConfig(argv), + this.tasks.updateLocalConfig(), ], { concurrent: false, diff --git a/src/commands/cluster/tasks.ts b/src/commands/cluster/tasks.ts index fca930eff..0ff4e27bd 100644 --- a/src/commands/cluster/tasks.ts +++ b/src/commands/cluster/tasks.ts @@ -26,6 +26,10 @@ import chalk from 'chalk'; import {ListrLease} from '../../core/lease/listr_lease.js'; import {type K8} from '../../core/k8.js'; import {ListrEnquirerPromptAdapter} from '@listr2/prompt-adapter-enquirer'; +import type {SoloListrTask, SoloListrTaskWrapper} from '../../types/index.js'; +import type {SelectClusterContextContext} from './configs.js'; +import type {Namespace} from '../../core/config/remote/types.js'; +import type {LocalConfig} from '../../core/config/local_config.js'; export class ClusterCommandTasks { private readonly parent: BaseCommand; @@ -37,54 +41,62 @@ export class ClusterCommandTasks { this.parent = parent; } - updateLocalConfig(argv) { - return new Task('Update local configuration', async (ctx: any, task: ListrTaskWrapper) => { - this.parent.logger.info('Compare local and remote configuration...'); - const configManager = this.parent.getConfigManager(); - const isQuiet = configManager.getFlag(flags.quiet); - - await this.parent.getRemoteConfigManager().modify(async remoteConfig => { - // Update current deployment with cluster list from remoteConfig - 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); - } - } - ctx.config.clusters = remoteClusterList; - localDeployments[localConfig.currentDeploymentName].clusters = ctx.config.clusters; - localConfig.setDeployments(localDeployments); - - const contexts = splitFlagInput(configManager.getFlag(flags.context)); - - for (let i = 0; i < ctx.config.clusters.length; i++) { - const cluster = ctx.config.clusters[i]; - const context = contexts[i]; - - // If a context is provided use it to update the mapping - if (context) { - localConfig.clusterContextMapping[cluster] = context; - } else if (!localConfig.clusterContextMapping[cluster]) { - // In quiet mode use the currently selected context to update the mapping - if (isQuiet) { - localConfig.clusterContextMapping[cluster] = this.parent.getK8().getKubeConfig().getCurrentContext(); + updateLocalConfig(): SoloListrTask { + return { + title: 'Update local configuration', + task: async (ctx, task) => { + this.parent.logger.info('Compare local and remote configuration...'); + const configManager = this.parent.getConfigManager(); + const isQuiet = configManager.getFlag(flags.quiet); + + await this.parent.getRemoteConfigManager().modify(async remoteConfig => { + // Update current deployment with a cluster list from remoteConfig + const localConfig = this.parent.getLocalConfig(); + const localDeployments = localConfig.deployments; + const remoteClusterList: string[] = []; + 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; + localConfig.setDeployments(localDeployments); + + const contexts = splitFlagInput(configManager.getFlag(flags.context)); + + for (let i = 0; i < ctx.config.clusters.length; i++) { + const cluster = ctx.config.clusters[i]; + const context = contexts[i]; + + // If a context is provided, use it to update the mapping + if (context) { + localConfig.clusterContextMapping[cluster] = context; + } else if (!localConfig.clusterContextMapping[cluster]) { + // In quiet mode, use the currently selected context to update the mapping + if (isQuiet) { + localConfig.clusterContextMapping[cluster] = this.parent.getK8().getKubeConfig().getCurrentContext(); + } - // Prompt the user to select a context if mapping value is missing - else { - localConfig.clusterContextMapping[cluster] = await this.promptForContext(task, cluster); + // Prompt the user to select a context if mapping value is missing + else { + localConfig.clusterContextMapping[cluster] = await this.promptForContext(task, cluster); + } } } - } - this.parent.logger.info('Update local configuration...'); - await localConfig.write(); - }); - }); + this.parent.logger.info('Update local configuration...'); + await localConfig.write(); + }); + }, + }; } - private async getSelectedContext(task, selectedCluster, localConfig, isQuiet) { + private async getSelectedContext( + task: SoloListrTaskWrapper, + selectedCluster: string, + localConfig: LocalConfig, + isQuiet: boolean, + ) { let selectedContext; if (isQuiet) { selectedContext = this.parent.getK8().getKubeConfig().getCurrentContext(); @@ -95,7 +107,7 @@ export class ClusterCommandTasks { return selectedContext; } - private async promptForContext(task, cluster) { + private async promptForContext(task: SoloListrTaskWrapper, cluster: string) { const kubeContexts = this.parent.getK8().getContexts(); return flags.context.prompt( task, @@ -104,14 +116,19 @@ export class ClusterCommandTasks { ); } - private async selectContextForFirstCluster(task, clusters, localConfig, isQuiet) { + private async selectContextForFirstCluster( + task: SoloListrTaskWrapper, + clusters: string[], + localConfig: LocalConfig, + isQuiet: boolean, + ) { const selectedCluster = clusters[0]; if (localConfig.clusterContextMapping[selectedCluster]) { return localConfig.clusterContextMapping[selectedCluster]; } - // If cluster does not exist in LocalConfig mapping prompt the user to select a context or use the current one + // If a cluster does not exist in LocalConfig mapping prompt the user to select a context or use the current one else { return this.getSelectedContext(task, selectedCluster, localConfig, isQuiet); } @@ -160,69 +177,72 @@ export class ClusterCommandTasks { ); } - selectContext(argv) { - return new Task('Read local configuration settings', async (ctx: any, task: ListrTaskWrapper) => { - this.parent.logger.info('Read local configuration settings...'); - const configManager = this.parent.getConfigManager(); - const isQuiet = configManager.getFlag(flags.quiet); - const deploymentName: string = configManager.getFlag(flags.namespace); - let clusters = splitFlagInput(configManager.getFlag(flags.clusterName)); - const contexts = splitFlagInput(configManager.getFlag(flags.context)); - const localConfig = this.parent.getLocalConfig(); - let selectedContext; - - // If one or more contexts are provided use the first one - if (contexts.length) { - selectedContext = contexts[0]; - } - - // 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) { - selectedContext = await this.selectContextForFirstCluster(task, clusters, localConfig, isQuiet); - } + selectContext(): SoloListrTask { + return { + title: 'Read local configuration settings', + task: async (_, task) => { + this.parent.logger.info('Read local configuration settings...'); + const configManager = this.parent.getConfigManager(); + const isQuiet = configManager.getFlag(flags.quiet); + const deploymentName: string = configManager.getFlag(flags.namespace); + let clusters = splitFlagInput(configManager.getFlag(flags.clusterName)); + const contexts = splitFlagInput(configManager.getFlag(flags.context)); + const localConfig = this.parent.getLocalConfig(); + let selectedContext: string; - // If a deployment name is provided get the clusters associated with the deployment from the LocalConfig - // and select the context from the mapping, corresponding to the first deployment cluster - else if (deploymentName) { - const deployment = localConfig.deployments[deploymentName]; + // If one or more contexts are provided, use the first one + if (contexts.length) { + selectedContext = contexts[0]; + } - if (deployment && deployment.clusters.length) { - selectedContext = await this.selectContextForFirstCluster(task, deployment.clusters, localConfig, isQuiet); + // 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) { + selectedContext = await this.selectContextForFirstCluster(task, clusters, localConfig, isQuiet); } - // The provided deployment does not exist in the LocalConfig - else { - // 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; - localConfig.deployments[deploymentName] = { - clusters: [selectedCluster], - }; - - if (!localConfig.clusterContextMapping[selectedCluster]) { - localConfig.clusterContextMapping[selectedCluster] = selectedContext; - } + // If a deployment name is provided, get the clusters associated with the deployment from the LocalConfig + // and select the context from the mapping, corresponding to the first deployment cluster + else if (deploymentName) { + const deployment = localConfig.deployments[deploymentName]; + + if (deployment && deployment.clusters.length) { + selectedContext = await this.selectContextForFirstCluster(task, deployment.clusters, localConfig, isQuiet); } - // Prompt user for clusters and contexts + // The provided deployment does not exist in the LocalConfig else { - clusters = splitFlagInput(await flags.clusterName.prompt(task, clusters)); - - for (const cluster of clusters) { - if (!localConfig.clusterContextMapping[cluster]) { - localConfig.clusterContextMapping[cluster] = await this.promptForContext(task, cluster); + // 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; + localConfig.deployments[deploymentName] = { + clusters: [selectedCluster], + }; + + if (!localConfig.clusterContextMapping[selectedCluster]) { + localConfig.clusterContextMapping[selectedCluster] = selectedContext; } } - selectedContext = localConfig.clusterContextMapping[clusters[0]]; + // Prompt user for clusters and contexts + else { + clusters = splitFlagInput(await flags.clusterName.prompt(task, clusters)); + + for (const cluster of clusters) { + if (!localConfig.clusterContextMapping[cluster]) { + localConfig.clusterContextMapping[cluster] = await this.promptForContext(task, cluster); + } + } + + selectedContext = localConfig.clusterContextMapping[clusters[0]]; + } } } - } - this.parent.getK8().getKubeConfig().setCurrentContext(selectedContext); - }); + this.parent.getK8().getKubeConfig().setCurrentContext(selectedContext); + }, + }; } initialize(argv: any, configInit: ConfigBuilder) { diff --git a/src/commands/deployment.ts b/src/commands/deployment.ts index 6187aadc0..31716fdd8 100644 --- a/src/commands/deployment.ts +++ b/src/commands/deployment.ts @@ -14,7 +14,7 @@ * limitations under the License. * */ -import {Listr, type ListrTaskWrapper} from 'listr2'; +import {Listr} from 'listr2'; import {SoloError} from '../core/errors.js'; import {BaseCommand} from './base.js'; import {Flags as flags} from './flags.js'; @@ -23,15 +23,26 @@ import {Templates} from '../core/templates.js'; import chalk from 'chalk'; import {RemoteConfigTasks} from '../core/config/remote/remote_config_tasks.js'; import {ListrLease} from '../core/lease/listr_lease.js'; +import {ClusterCommandTasks} from './cluster/tasks.js'; import type {Namespace} from '../core/config/remote/types.js'; -import {type ContextClusterStructure} from '../types/config_types.js'; -import {type CommandFlag} from '../types/flag_types.js'; -import {type CommandBuilder} from '../types/aliases.js'; +import type {ContextClusterStructure} from '../types/config_types.js'; +import type {CommandFlag} from '../types/flag_types.js'; +import type {CommandBuilder} from '../types/aliases.js'; +import type {Opts} from '../types/command_types.js'; export class DeploymentCommand extends BaseCommand { + readonly tasks: ClusterCommandTasks; + + constructor(opts: Opts) { + super(opts); + + this.tasks = new ClusterCommandTasks(this, this.k8); + } + private static get DEPLOY_FLAGS_LIST(): CommandFlag[] { return [ flags.quiet, + flags.context, flags.namespace, flags.userEmailAddress, flags.deploymentClusters, @@ -44,6 +55,7 @@ export class DeploymentCommand extends BaseCommand { const lease = await self.leaseManager.create(); interface Config { + context: string; namespace: Namespace; contextClusterUnparsed: string; contextCluster: ContextClusterStructure; @@ -56,7 +68,7 @@ export class DeploymentCommand extends BaseCommand { [ { title: 'Initialize', - task: async (ctx, task): Promise> => { + task: async (ctx, task) => { self.configManager.update(argv); self.logger.debug('Updated config with argv', {config: self.configManager.config}); @@ -85,31 +97,25 @@ export class DeploymentCommand extends BaseCommand { }, }, this.localConfig.promptLocalConfigTask(self.k8), + RemoteConfigTasks.createRemoteConfig.bind(this)(), + this.tasks.selectContext(), { - title: 'Validate cluster connections', - task: async (ctx, task): Promise> => { - const subTasks = []; - - for (const cluster of Object.keys(ctx.config.contextCluster)) { - subTasks.push({ - title: `Testing connection to cluster: ${chalk.cyan(cluster)}`, - task: async (_: Context, task: ListrTaskWrapper) => { - if (!(await self.k8.testClusterConnection(cluster))) { - task.title = `${task.title} - ${chalk.red('Cluster connection failed')}`; - - throw new SoloError(`Cluster connection failed for: ${cluster}`); - } - }, - }); + title: 'Validate context', + task: async (ctx, task) => { + ctx.config.context = ctx.config.context ?? self.configManager.getFlag(flags.context); + const availableContexts = self.k8.getContextNames(); + + if (availableContexts.includes(ctx.config.context)) { + task.title += ` - context: ${chalk.green(ctx.config.context)} is valid`; + return; } - return task.newListr(subTasks, { - concurrent: true, - rendererOptions: {collapseSubtasks: false}, - }); + throw new SoloError( + `Context with name ${ctx.config.context} not found, available contexts include ${availableContexts.join(', ')}`, + ); }, }, - RemoteConfigTasks.createRemoteConfig.bind(this)(), + this.tasks.updateLocalConfig(), ], { concurrent: false, diff --git a/src/core/config/local_config.ts b/src/core/config/local_config.ts index 679c1921d..697e524f8 100644 --- a/src/core/config/local_config.ts +++ b/src/core/config/local_config.ts @@ -206,16 +206,18 @@ export class LocalConfig implements LocalConfigData { if (parsedContexts.length < parsedClusters.length) { if (!isQuiet) { - const promptedContexts = []; + const promptedContexts: string[] = []; for (const cluster of parsedClusters) { const kubeContexts = k8.getContexts(); - const context = await flags.context.prompt( + const context: string = await flags.context.prompt( task, kubeContexts.map(c => c.name), cluster, ); self.clusterContextMapping[cluster] = context; promptedContexts.push(context); + + self.configManager.setFlag(flags.context, context); } self.configManager.setFlag(flags.context, promptedContexts.join(',')); } else { @@ -229,12 +231,15 @@ export class LocalConfig implements LocalConfigData { for (let i = 0; i < parsedClusters.length; i++) { const cluster = parsedClusters[i]; self.clusterContextMapping[cluster] = parsedContexts[i]; + + self.configManager.setFlag(flags.context, parsedContexts[i]); } } self.userEmailAddress = userEmailAddress; self.deployments = deployments; self.currentDeploymentName = deploymentName; + self.validate(); await self.write(); }, diff --git a/test/unit/commands/cluster.test.ts b/test/unit/commands/cluster.test.ts index a8a8dffef..6b5135ba6 100644 --- a/test/unit/commands/cluster.test.ts +++ b/test/unit/commands/cluster.test.ts @@ -147,7 +147,8 @@ describe('ClusterCommand unit tests', () => { const getBaseCommandOpts = ( sandbox: sinon.SinonSandbox, remoteConfig: any = {}, - // @ts-ignore + + // @ts-expect-error - TS2344: Type CommandFlag does not satisfy the constraint string | number | symbol stubbedFlags: Record[] = [], ) => { const loggerStub = sandbox.createStubInstance(SoloLogger); @@ -206,9 +207,13 @@ describe('ClusterCommand unit tests', () => { describe('updateLocalConfig', () => { async function runUpdateLocalConfigTask(opts) { command = new ClusterCommand(opts); + tasks = new ClusterCommandTasks(command, opts.k8); + + // @ts-expect-error - TS2554: Expected 0 arguments, but got 1. const taskObj = tasks.updateLocalConfig({}); - await taskObj.task({config: {}}, sandbox.stub() as unknown as ListrTaskWrapper); + + await taskObj.task({config: {}} as any, sandbox.stub() as unknown as ListrTaskWrapper); return command; } @@ -246,7 +251,7 @@ describe('ClusterCommand unit tests', () => { }, }; const opts = getBaseCommandOpts(sandbox, remoteConfig, []); - command = await runUpdateLocalConfigTask(opts); // @ts-ignore + command = await runUpdateLocalConfigTask(opts); localConfig = new LocalConfig(filePath); expect(localConfig.currentDeploymentName).to.equal('deployment'); @@ -264,7 +269,7 @@ describe('ClusterCommand unit tests', () => { }, }; const opts = getBaseCommandOpts(sandbox, remoteConfig, [[flags.context, 'provided-context']]); - command = await runUpdateLocalConfigTask(opts); // @ts-ignore + command = await runUpdateLocalConfigTask(opts); localConfig = new LocalConfig(filePath); expect(localConfig.currentDeploymentName).to.equal('deployment'); @@ -286,7 +291,7 @@ describe('ClusterCommand unit tests', () => { const opts = getBaseCommandOpts(sandbox, remoteConfig, [ [flags.context, 'provided-context-2,provided-context-3,provided-context-4'], ]); - command = await runUpdateLocalConfigTask(opts); // @ts-ignore + command = await runUpdateLocalConfigTask(opts); localConfig = new LocalConfig(filePath); expect(localConfig.currentDeploymentName).to.equal('deployment'); @@ -307,7 +312,7 @@ describe('ClusterCommand unit tests', () => { }, }; const opts = getBaseCommandOpts(sandbox, remoteConfig, [[flags.quiet, true]]); - command = await runUpdateLocalConfigTask(opts); // @ts-ignore + command = await runUpdateLocalConfigTask(opts); localConfig = new LocalConfig(filePath); expect(localConfig.currentDeploymentName).to.equal('deployment'); @@ -328,7 +333,7 @@ describe('ClusterCommand unit tests', () => { }; const opts = getBaseCommandOpts(sandbox, remoteConfig, []); - command = await runUpdateLocalConfigTask(opts); // @ts-ignore + command = await runUpdateLocalConfigTask(opts); localConfig = new LocalConfig(filePath); expect(localConfig.currentDeploymentName).to.equal('deployment'); @@ -344,9 +349,13 @@ describe('ClusterCommand unit tests', () => { describe('selectContext', () => { async function runSelectContextTask(opts) { command = new ClusterCommand(opts); + tasks = new ClusterCommandTasks(command, opts.k8); + + // @ts-expect-error - TS2554: Expected 0 arguments, but got 1 const taskObj = tasks.selectContext({}); - await taskObj.task({config: {}}, sandbox.stub() as unknown as ListrTaskWrapper); + + await taskObj.task({config: {}} as any, sandbox.stub() as unknown as ListrTaskWrapper); return command; } @@ -380,21 +389,21 @@ describe('ClusterCommand unit tests', () => { [flags.context, 'provided-context-1,provided-context-2,provided-context-3'], ]); - command = await runSelectContextTask(opts); // @ts-ignore + command = await runSelectContextTask(opts); expect(command.getK8().getKubeConfig().setCurrentContext).to.have.been.calledWith('provided-context-1'); }); it('should use local config mapping to connect to first provided cluster', async () => { const opts = getBaseCommandOpts(sandbox, {}, [[flags.clusterName, 'cluster-2,cluster-3']]); - command = await runSelectContextTask(opts); // @ts-ignore + command = await runSelectContextTask(opts); expect(command.getK8().getKubeConfig().setCurrentContext).to.have.been.calledWith('context-2'); }); it('should prompt for context if selected cluster is not found in local config mapping', async () => { const opts = getBaseCommandOpts(sandbox, {}, [[flags.clusterName, 'cluster-3']]); - command = await runSelectContextTask(opts); // @ts-ignore + command = await runSelectContextTask(opts); expect(command.getK8().getKubeConfig().setCurrentContext).to.have.been.calledWith('context-3'); }); @@ -404,21 +413,21 @@ describe('ClusterCommand unit tests', () => { [flags.quiet, true], ]); - command = await runSelectContextTask(opts); // @ts-ignore + command = await runSelectContextTask(opts); expect(command.getK8().getKubeConfig().setCurrentContext).to.have.been.calledWith('context-from-kubeConfig'); }); it('should use context from local config mapping for the first cluster from the selected deployment', async () => { const opts = getBaseCommandOpts(sandbox, {}, [[flags.namespace, 'deployment-2']]); - command = await runSelectContextTask(opts); // @ts-ignore + command = await runSelectContextTask(opts); expect(command.getK8().getKubeConfig().setCurrentContext).to.have.been.calledWith('context-2'); }); it('should prompt for context if selected deployment is found in local config but the context is not', async () => { const opts = getBaseCommandOpts(sandbox, {}, [[flags.namespace, 'deployment-3']]); - command = await runSelectContextTask(opts); // @ts-ignore + command = await runSelectContextTask(opts); expect(command.getK8().getKubeConfig().setCurrentContext).to.have.been.calledWith('context-3'); }); @@ -428,14 +437,14 @@ describe('ClusterCommand unit tests', () => { [flags.quiet, true], ]); - command = await runSelectContextTask(opts); // @ts-ignore + command = await runSelectContextTask(opts); expect(command.getK8().getKubeConfig().setCurrentContext).to.have.been.calledWith('context-from-kubeConfig'); }); it('should prompt for clusters and contexts if selected deployment is not found in local config', async () => { const opts = getBaseCommandOpts(sandbox, {}, [[flags.namespace, 'deployment-4']]); - command = await runSelectContextTask(opts); // @ts-ignore + command = await runSelectContextTask(opts); expect(command.getK8().getKubeConfig().setCurrentContext).to.have.been.calledWith('context-3'); }); @@ -445,7 +454,7 @@ describe('ClusterCommand unit tests', () => { [flags.quiet, true], ]); - command = await runSelectContextTask(opts); // @ts-ignore + command = await runSelectContextTask(opts); expect(command.getK8().getKubeConfig().setCurrentContext).to.have.been.calledWith('context-from-kubeConfig'); }); });