Skip to content

Commit c9711c5

Browse files
authored
feat: update solo context connect to connect to single remote cluster (#993)
Signed-off-by: Ivo Yankov <[email protected]>
1 parent 043efcf commit c9711c5

23 files changed

+579
-186
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ An opinionated CLI tool to deploy and manage standalone test networks.
2222

2323
### Hardware Requirements
2424

25-
To run a three-node network, you will need to set up Docker Desktop with at least 8GB of memory and 4 CPUs.
25+
To run a three-node network, you will need to set up Docker Desktop with at least 8GB of memory and 4 CPUs.
2626

2727
![alt text](/docs/content/User/DockerDesktop.png)
2828

docs/content/User/Env.md

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,3 @@ User can configure the following environment variables to customize the behavior
3535
| `RELAY_PODS_READY_MAX_ATTEMPTS` | The maximum number of attempts to check if relay pods are ready. | `100` |
3636
| `RELAY_PODS_READY_DELAY` | The interval between attempts to check if relay pods are ready, in the unit of milliseconds. | `120` |
3737
| `NETWORK_DESTROY_WAIT_TIMEOUT` | The period of time to wait for network to be destroyed, in the unit of milliseconds. | `60000` |
38-
39-
40-
41-
42-
43-
44-

docs/content/User/FAQ.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ You can run `solo account init` anytime after `solo node start`
44

55
### Where can I find the default account keys ?
66

7-
It is the well known default genesis key [Link](https://github.com/hashgraph/hedera-services/blob/develop/hedera-node/data/onboard/GenesisPrivKey.txt)
7+
It is the well known default genesis key [Link](https://github.com/hashgraph/hedera-services/blob/develop/hedera-node/data/onboard/GenesisPrivKey.txt)
88

99
### How do I get the key for an account?
1010

1111
Use the following command to get account balance and private key of the account `0.0.1007`:
12+
1213
```bash
1314
# get account info of 0.0.1007 and also show the private key
1415
solo account get --account-id 0.0.1007 -n solo-e2e --private-key

docs/content/User/GetStarted.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ For Hedera extended users:
2121
* [Using Environment Variables](Env.md)
2222

2323
FAQ:
24+
2425
* [Frequently Asked Questions](FAQ.md)
2526

2627
For curious mind:

src/commands/base.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,10 @@ export abstract class BaseCommand extends ShellRunner {
9696
return valuesArg;
9797
}
9898

99+
getConfigManager(): ConfigManager {
100+
return this.configManager;
101+
}
102+
99103
/**
100104
* Dynamically builds a class with properties from the provided list of flags
101105
* and extra properties, will keep track of which properties are used. Call
@@ -185,6 +189,10 @@ export abstract class BaseCommand extends ShellRunner {
185189
return this.localConfig;
186190
}
187191

192+
getRemoteConfigManager() {
193+
return this.remoteConfigManager;
194+
}
195+
188196
abstract close(): Promise<void>;
189197

190198
commandActionBuilder(actionTasks: any, options: any, errorString: string, lease: Lease | null) {

src/commands/context/configs.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* Copyright (C) 2024 Hedera Hashgraph, LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the ""License"");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an ""AS IS"" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
18+
import {type NodeAlias} from '../../types/aliases.js';
19+
20+
export const CONNECT_CONFIGS_NAME = 'connectConfig';
21+
22+
export const connectConfigBuilder = async function (argv, ctx, task) {
23+
const config = this.getConfig(CONNECT_CONFIGS_NAME, argv.flags, [
24+
'currentDeploymentName',
25+
]) as ContextConnectConfigClass;
26+
27+
// set config in the context for later tasks to use
28+
ctx.config = config;
29+
30+
return ctx.config;
31+
};
32+
33+
export interface ContextConnectConfigClass {
34+
app: string;
35+
cacheDir: string;
36+
devMode: boolean;
37+
namespace: string;
38+
nodeAlias: NodeAlias;
39+
context: string;
40+
clusterName: string;
41+
}

src/commands/context/flags.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,12 @@ import {Flags as flags} from '../flags.js';
2020
export const USE_FLAGS = {
2121
requiredFlags: [],
2222
requiredFlagsWithDisabledPrompt: [],
23-
optionalFlags: [flags.devMode, flags.quiet, flags.clusterName, flags.context, flags.force, flags.namespace],
23+
optionalFlags: [
24+
flags.devMode,
25+
flags.quiet,
26+
flags.clusterName,
27+
flags.context,
28+
flags.namespace,
29+
flags.userEmailAddress,
30+
],
2431
};

src/commands/context/handlers.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,32 @@ import {type ContextCommandTasks} from './tasks.js';
1919
import * as helpers from '../../core/helpers.js';
2020
import * as constants from '../../core/constants.js';
2121
import * as ContextFlags from './flags.js';
22+
import {RemoteConfigTasks} from '../../core/config/remote/remote_config_tasks.js';
23+
import type {RemoteConfigManager} from '../../core/config/remote/remote_config_manager.js';
24+
import {connectConfigBuilder} from './configs.js';
2225

2326
export class ContextCommandHandlers implements CommandHandlers {
2427
readonly parent: BaseCommand;
2528
readonly tasks: ContextCommandTasks;
29+
public readonly remoteConfigManager: RemoteConfigManager;
30+
private getConfig: any;
2631

27-
constructor(parent: BaseCommand, tasks: ContextCommandTasks) {
32+
constructor(parent: BaseCommand, tasks: ContextCommandTasks, remoteConfigManager: RemoteConfigManager) {
2833
this.parent = parent;
2934
this.tasks = tasks;
35+
this.remoteConfigManager = remoteConfigManager;
36+
this.getConfig = parent.getConfig.bind(parent);
3037
}
3138

3239
async connect(argv: any) {
3340
argv = helpers.addFlagsToArgv(argv, ContextFlags.USE_FLAGS);
3441

3542
const action = this.parent.commandActionBuilder(
3643
[
37-
this.tasks.initialize(argv),
38-
this.parent.getLocalConfig().promptLocalConfigTask(),
44+
this.tasks.initialize(argv, connectConfigBuilder.bind(this)),
45+
this.parent.getLocalConfig().promptLocalConfigTask(this.parent.getK8()),
46+
this.tasks.selectContext(argv),
47+
RemoteConfigTasks.loadRemoteConfig.bind(this)(argv),
3948
this.tasks.updateLocalConfig(argv),
4049
],
4150
{

src/commands/context/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export class ContextCommand extends BaseCommand {
3131
constructor(opts: Opts) {
3232
super(opts);
3333

34-
this.handlers = new ContextCommandHandlers(this, new ContextCommandTasks(this));
34+
this.handlers = new ContextCommandHandlers(this, new ContextCommandTasks(this), this.remoteConfigManager);
3535
}
3636

3737
getCommandDefinition() {

src/commands/context/tasks.ts

Lines changed: 134 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@
1515
*
1616
*/
1717
import {Task} from '../../core/task.js';
18-
import {Templates} from '../../core/templates.js';
1918
import {Flags as flags} from '../flags.js';
2019
import type {ListrTaskWrapper} from 'listr2';
20+
import type {ConfigBuilder} from '../../types/aliases.js';
2121
import {type BaseCommand} from '../base.js';
22+
import {splitFlagInput} from '../../core/helpers.js';
2223

2324
export class ContextCommandTasks {
2425
private readonly parent: BaseCommand;
@@ -29,62 +30,150 @@ export class ContextCommandTasks {
2930

3031
updateLocalConfig(argv) {
3132
return new Task('Update local configuration', async (ctx: any, task: ListrTaskWrapper<any, any, any>) => {
32-
this.parent.logger.info('Updating local configuration...');
33+
this.parent.logger.info('Compare local and remote configuration...');
34+
const configManager = this.parent.getConfigManager();
35+
const isQuiet = configManager.getFlag(flags.quiet);
3336

34-
const isQuiet = !!argv[flags.quiet.name];
37+
await this.parent.getRemoteConfigManager().modify(async remoteConfig => {
38+
// Update current deployment with cluster list from remoteConfig
39+
const localConfig = this.parent.getLocalConfig();
40+
const localDeployments = localConfig.deployments;
41+
const remoteClusterList = [];
42+
for (const cluster of Object.keys(remoteConfig.clusters)) {
43+
if (localConfig.currentDeploymentName === remoteConfig.clusters[cluster]) {
44+
remoteClusterList.push(cluster);
45+
}
46+
}
47+
ctx.config.clusters = remoteClusterList;
48+
localDeployments[localConfig.currentDeploymentName].clusters = ctx.config.clusters;
49+
localConfig.setDeployments(localDeployments);
3550

36-
let currentDeploymentName = argv[flags.namespace.name];
37-
let clusters = Templates.parseClusterAliases(argv[flags.clusterName.name]);
38-
let contextName = argv[flags.context.name];
51+
const contexts = splitFlagInput(configManager.getFlag(flags.context));
3952

40-
const kubeContexts = await this.parent.getK8().getContexts();
53+
for (let i = 0; i < ctx.config.clusters.length; i++) {
54+
const cluster = ctx.config.clusters[i];
55+
const context = contexts[i];
4156

42-
if (isQuiet) {
43-
const currentCluster = await this.parent.getK8().getKubeConfig().getCurrentCluster();
44-
if (!clusters.length) clusters = [currentCluster.name];
45-
if (!contextName) contextName = await this.parent.getK8().getKubeConfig().getCurrentContext();
57+
// If a context is provided use it to update the mapping
58+
if (context) {
59+
localConfig.clusterContextMapping[cluster] = context;
60+
} else if (!localConfig.clusterContextMapping[cluster]) {
61+
// In quiet mode use the currently selected context to update the mapping
62+
if (isQuiet) {
63+
localConfig.clusterContextMapping[cluster] = this.parent.getK8().getKubeConfig().getCurrentContext();
64+
}
4665

47-
if (!currentDeploymentName) {
48-
const selectedContext = kubeContexts.find(e => e.name === contextName);
49-
currentDeploymentName = selectedContext && selectedContext.namespace ? selectedContext.namespace : 'default';
50-
}
51-
} else {
52-
if (!clusters.length) {
53-
const prompt = flags.clusterName.prompt;
54-
const unparsedClusterAliases = await prompt(task, clusters);
55-
clusters = Templates.parseClusterAliases(unparsedClusterAliases);
56-
}
57-
if (!contextName) {
58-
const prompt = flags.context.prompt;
59-
contextName = await prompt(
60-
task,
61-
kubeContexts.map(c => c.name),
62-
);
63-
}
64-
if (!currentDeploymentName) {
65-
const prompt = flags.namespace.prompt;
66-
currentDeploymentName = await prompt(task, currentDeploymentName);
66+
// Prompt the user to select a context if mapping value is missing
67+
else {
68+
localConfig.clusterContextMapping[cluster] = await this.promptForContext(task, cluster);
69+
}
70+
}
6771
}
72+
this.parent.logger.info('Update local configuration...');
73+
await localConfig.write();
74+
});
75+
});
76+
}
77+
78+
private async getSelectedContext(task, selectedCluster, localConfig, isQuiet) {
79+
let selectedContext;
80+
if (isQuiet) {
81+
selectedContext = this.parent.getK8().getKubeConfig().getCurrentContext();
82+
} else {
83+
selectedContext = await this.promptForContext(task, selectedCluster);
84+
localConfig.clusterContextMapping[selectedCluster] = selectedContext;
85+
}
86+
return selectedContext;
87+
}
88+
89+
private async promptForContext(task, cluster) {
90+
const kubeContexts = this.parent.getK8().getContexts();
91+
return flags.context.prompt(
92+
task,
93+
kubeContexts.map(c => c.name),
94+
cluster,
95+
);
96+
}
97+
98+
private async selectContextForFirstCluster(task, clusters, localConfig, isQuiet) {
99+
const selectedCluster = clusters[0];
100+
101+
if (localConfig.clusterContextMapping[selectedCluster]) {
102+
return localConfig.clusterContextMapping[selectedCluster];
103+
}
104+
105+
// If cluster does not exist in LocalConfig mapping prompt the user to select a context or use the current one
106+
else {
107+
return this.getSelectedContext(task, selectedCluster, localConfig, isQuiet);
108+
}
109+
}
110+
111+
selectContext(argv) {
112+
return new Task('Read local configuration settings', async (ctx: any, task: ListrTaskWrapper<any, any, any>) => {
113+
this.parent.logger.info('Read local configuration settings...');
114+
const configManager = this.parent.getConfigManager();
115+
const isQuiet = configManager.getFlag(flags.quiet);
116+
const deploymentName: string = configManager.getFlag(flags.namespace);
117+
let clusters = splitFlagInput(configManager.getFlag(flags.clusterName));
118+
const contexts = splitFlagInput(configManager.getFlag(flags.context));
119+
const localConfig = this.parent.getLocalConfig();
120+
let selectedContext;
121+
122+
// If one or more contexts are provided use the first one
123+
if (contexts.length) {
124+
selectedContext = contexts[0];
68125
}
69126

70-
// Select current deployment
71-
this.parent.getLocalConfig().setCurrentDeployment(currentDeploymentName);
127+
// If one or more clusters are provided use the first one to determine the context
128+
// from the mapping in the LocalConfig
129+
else if (clusters.length) {
130+
selectedContext = await this.selectContextForFirstCluster(task, clusters, localConfig, isQuiet);
131+
}
132+
133+
// If a deployment name is provided get the clusters associated with the deployment from the LocalConfig
134+
// and select the context from the mapping, corresponding to the first deployment cluster
135+
else if (deploymentName) {
136+
const deployment = localConfig.deployments[deploymentName];
137+
138+
if (deployment && deployment.clusters.length) {
139+
selectedContext = await this.selectContextForFirstCluster(task, deployment.clusters, localConfig, isQuiet);
140+
}
72141

73-
// Set clusters for active deployment
74-
const deployments = this.parent.getLocalConfig().deployments;
75-
deployments[currentDeploymentName].clusters = clusters;
76-
this.parent.getLocalConfig().setDeployments(deployments);
142+
// The provided deployment does not exist in the LocalConfig
143+
else {
144+
// Add the deployment to the LocalConfig with the currently selected cluster and context in KubeConfig
145+
if (isQuiet) {
146+
selectedContext = this.parent.getK8().getKubeConfig().getCurrentContext();
147+
const selectedCluster = this.parent.getK8().getKubeConfig().getCurrentCluster().name;
148+
localConfig.deployments[deploymentName] = {
149+
clusters: [selectedCluster],
150+
};
77151

78-
this.parent.getK8().getKubeConfig().setCurrentContext(contextName);
152+
if (!localConfig.clusterContextMapping[selectedCluster]) {
153+
localConfig.clusterContextMapping[selectedCluster] = selectedContext;
154+
}
155+
}
79156

80-
this.parent.logger.info(
81-
`Save LocalConfig file: [currentDeploymentName: ${currentDeploymentName}, contextName: ${contextName}, clusters: ${clusters.join(' ')}]`,
82-
);
83-
await this.parent.getLocalConfig().write();
157+
// Prompt user for clusters and contexts
158+
else {
159+
clusters = splitFlagInput(await flags.clusterName.prompt(task, clusters));
160+
161+
for (const cluster of clusters) {
162+
if (!localConfig.clusterContextMapping[cluster]) {
163+
localConfig.clusterContextMapping[cluster] = await this.promptForContext(task, cluster);
164+
}
165+
}
166+
167+
selectedContext = localConfig.clusterContextMapping[clusters[0]];
168+
}
169+
}
170+
}
171+
172+
this.parent.getK8().getKubeConfig().setCurrentContext(selectedContext);
84173
});
85174
}
86175

87-
initialize(argv: any) {
176+
initialize(argv: any, configInit: ConfigBuilder) {
88177
const {requiredFlags, optionalFlags} = argv;
89178

90179
argv.flags = [...requiredFlags, ...optionalFlags];
@@ -93,6 +182,8 @@ export class ContextCommandTasks {
93182
if (argv[flags.devMode.name]) {
94183
this.parent.logger.setDevMode(true);
95184
}
185+
186+
ctx.config = await configInit(argv, ctx, task);
96187
});
97188
}
98189
}

src/commands/deployment.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export class DeploymentCommand extends BaseCommand {
8080
return ListrLease.newAcquireLeaseTask(lease, task);
8181
},
8282
},
83-
this.localConfig.promptLocalConfigTask(),
83+
this.localConfig.promptLocalConfigTask(self.k8),
8484
{
8585
title: 'Validate cluster connections',
8686
task: async (ctx, task): Promise<Listr<Context, any, any>> => {

0 commit comments

Comments
 (0)