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: update solo context connect to connect to single remote cluster #993

Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ An opinionated CLI tool to deploy and manage standalone test networks.

### Hardware Requirements

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

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

Expand Down
7 changes: 0 additions & 7 deletions docs/content/User/Env.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,3 @@ User can configure the following environment variables to customize the behavior
| `RELAY_PODS_READY_MAX_ATTEMPTS` | The maximum number of attempts to check if relay pods are ready. | `100` |
| `RELAY_PODS_READY_DELAY` | The interval between attempts to check if relay pods are ready, in the unit of milliseconds. | `120` |
| `NETWORK_DESTROY_WAIT_TIMEOUT` | The period of time to wait for network to be destroyed, in the unit of milliseconds. | `60000` |







3 changes: 2 additions & 1 deletion docs/content/User/FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ You can run `solo account init` anytime after `solo node start`

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

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

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

Use the following command to get account balance and private key of the account `0.0.1007`:

```bash
# get account info of 0.0.1007 and also show the private key
solo account get --account-id 0.0.1007 -n solo-e2e --private-key
Expand Down
1 change: 1 addition & 0 deletions docs/content/User/GetStarted.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ For Hedera extended users:
* [Using Environment Variables](Env.md)

FAQ:

* [Frequently Asked Questions](FAQ.md)

For curious mind:
Expand Down
8 changes: 8 additions & 0 deletions src/commands/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ export abstract class BaseCommand extends ShellRunner {
return valuesArg;
}

getConfigManager(): ConfigManager {
return this.configManager;
}

/**
* Dynamically builds a class with properties from the provided list of flags
* and extra properties, will keep track of which properties are used. Call
Expand Down Expand Up @@ -185,6 +189,10 @@ export abstract class BaseCommand extends ShellRunner {
return this.localConfig;
}

getRemoteConfigManager() {
return this.remoteConfigManager;
}

abstract close(): Promise<void>;

commandActionBuilder(actionTasks: any, options: any, errorString: string, lease: Lease | null) {
Expand Down
41 changes: 41 additions & 0 deletions src/commands/context/configs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* Copyright (C) 2024 Hedera Hashgraph, LLC
*
* Licensed under the Apache License, Version 2.0 (the ""License"");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an ""AS IS"" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

import {type NodeAlias} from '../../types/aliases.js';

export const CONNECT_CONFIGS_NAME = 'connectConfig';

export const connectConfigBuilder = async function (argv, ctx, task) {
const config = this.getConfig(CONNECT_CONFIGS_NAME, argv.flags, [
'currentDeploymentName',
]) as ContextConnectConfigClass;

// set config in the context for later tasks to use
ctx.config = config;

return ctx.config;
};

export interface ContextConnectConfigClass {
app: string;
cacheDir: string;
devMode: boolean;
namespace: string;
nodeAlias: NodeAlias;
context: string;
clusterName: string;
}
9 changes: 8 additions & 1 deletion src/commands/context/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,12 @@ import {Flags as flags} from '../flags.js';
export const USE_FLAGS = {
requiredFlags: [],
requiredFlagsWithDisabledPrompt: [],
optionalFlags: [flags.devMode, flags.quiet, flags.clusterName, flags.context, flags.force, flags.namespace],
optionalFlags: [
flags.devMode,
flags.quiet,
flags.clusterName,
flags.context,
flags.namespace,
flags.userEmailAddress,
],
};
16 changes: 13 additions & 3 deletions src/commands/context/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,33 @@ import {type ContextCommandTasks} from './tasks.js';
import * as helpers from '../../core/helpers.js';
import * as constants from '../../core/constants.js';
import * as ContextFlags from './flags.js';
import {RemoteConfigTasks} from '../../core/config/remote/remote_config_tasks.js';
import type {RemoteConfigManager} from '../../core/config/remote/remote_config_manager.js';
import {connectConfigBuilder} from './configs.js';

export class ContextCommandHandlers implements CommandHandlers {
readonly parent: BaseCommand;
readonly tasks: ContextCommandTasks;
public readonly remoteConfigManager: RemoteConfigManager;
private getConfig: any;

constructor(parent: BaseCommand, tasks: ContextCommandTasks) {
constructor(parent: BaseCommand, tasks: ContextCommandTasks, remoteConfigManager: RemoteConfigManager) {
this.parent = parent;
this.tasks = tasks;
this.remoteConfigManager = remoteConfigManager;
this.getConfig = parent.getConfig.bind(parent);
}

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

const action = this.parent.commandActionBuilder(
[
this.tasks.initialize(argv),
this.parent.getLocalConfig().promptLocalConfigTask(),
this.tasks.initialize(argv, connectConfigBuilder.bind(this)),
this.parent.getLocalConfig().promptLocalConfigTask(this.parent.getK8()),
this.tasks.selectContext(argv),
RemoteConfigTasks.loadRemoteConfig.bind(this)(argv),
// todo validate remoteConfig
this.tasks.updateLocalConfig(argv),
],
{
Expand Down
2 changes: 1 addition & 1 deletion src/commands/context/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export class ContextCommand extends BaseCommand {
constructor(opts: Opts) {
super(opts);

this.handlers = new ContextCommandHandlers(this, new ContextCommandTasks(this));
this.handlers = new ContextCommandHandlers(this, new ContextCommandTasks(this), this.remoteConfigManager);
}

getCommandDefinition() {
Expand Down
163 changes: 123 additions & 40 deletions src/commands/context/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {Task} from '../../core/task.js';
import {Templates} from '../../core/templates.js';
import {Flags as flags} from '../flags.js';
import type {ListrTaskWrapper} from 'listr2';
import type {ConfigBuilder} from '../../types/aliases.js';
import {type BaseCommand} from '../base.js';

export class ContextCommandTasks {
Expand All @@ -29,62 +30,142 @@ export class ContextCommandTasks {

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

const isQuiet = !!argv[flags.quiet.name];
await this.parent.getRemoteConfigManager().modify(async remoteConfig => {
const localConfig = this.parent.getLocalConfig();
const localDeployments = localConfig.deployments;
ctx.config.clusters = Object.keys(remoteConfig.clusters);
localDeployments[localConfig.currentDeploymentName].clusters = ctx.config.clusters;
localConfig.setDeployments(localDeployments);

let currentDeploymentName = argv[flags.namespace.name];
let clusters = Templates.parseClusterAliases(argv[flags.clusterName.name]);
let contextName = argv[flags.context.name];
const contexts = Templates.parseCommaSeparatedList(configManager.getFlag(flags.context));

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

if (isQuiet) {
const currentCluster = await this.parent.getK8().getKubeConfig().getCurrentCluster();
if (!clusters.length) clusters = [currentCluster.name];
if (!contextName) contextName = await this.parent.getK8().getKubeConfig().getCurrentContext();
// If a context is provided use it to update the mapping
if (context) {
localConfig.clusterContextMapping[cluster] = context;
}

if (!currentDeploymentName) {
const selectedContext = kubeContexts.find(e => e.name === contextName);
currentDeploymentName = selectedContext && selectedContext.namespace ? selectedContext.namespace : 'default';
}
} else {
if (!clusters.length) {
const prompt = flags.clusterName.prompt;
const unparsedClusterAliases = await prompt(task, clusters);
clusters = Templates.parseClusterAliases(unparsedClusterAliases);
// In quiet mode use the currently selected context to update the mapping
else if (isQuiet) {
//default
localConfig.clusterContextMapping[cluster] = this.parent.getK8().getKubeConfig().getCurrentContext();
}

// Prompt the user to select a context if mapping value is missing
else if (!localConfig.clusterContextMapping[cluster]) {
const kubeContexts = this.parent.getK8().getContexts();
localConfig.clusterContextMapping[cluster] = await flags.context.prompt(
task,
kubeContexts.map(c => c.name),
cluster,
);
}
}
if (!contextName) {
const prompt = flags.context.prompt;
contextName = await prompt(
task,
kubeContexts.map(c => c.name),
);
this.parent.logger.info('Update local configuration...');
await localConfig.write();
});
});
}

selectContext(argv) {
jeromy-cannon marked this conversation as resolved.
Show resolved Hide resolved
return new Task('Read local configuration settings', async (ctx: any, task: ListrTaskWrapper<any, any, any>) => {
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 = Templates.parseCommaSeparatedList(configManager.getFlag(flags.clusterName));
const contexts = Templates.parseCommaSeparatedList(configManager.getFlag(flags.context));
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) {
const selectedCluster = clusters[0];
const localConfig = this.parent.getLocalConfig();

if (localConfig.clusterContextMapping[selectedCluster]) {
selectedContext = localConfig.clusterContextMapping[selectedCluster];
}
if (!currentDeploymentName) {
const prompt = flags.namespace.prompt;
currentDeploymentName = await prompt(task, currentDeploymentName);

// If cluster does not exist in LocalConfig mapping prompt the user to select a context or use the current one
else {
if (isQuiet) {
selectedContext = this.parent.getK8().getKubeConfig().getCurrentContext();
} else {
const kubeContexts = this.parent.getK8().getContexts();
selectedContext = await flags.context.prompt(
task,
kubeContexts.map(c => c.name),
selectedCluster,
);
localConfig.clusterContextMapping[selectedCluster] = selectedContext;
}
}
}

// Select current deployment
this.parent.getLocalConfig().setCurrentDeployment(currentDeploymentName);
// 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 localConfig = this.parent.getLocalConfig();
const deployment = localConfig.deployments[deploymentName];

if (deployment && deployment.clusters.length) {
const selectedCluster = deployment.clusters[0];
selectedContext = localConfig.clusterContextMapping[selectedCluster];
}

// 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;
}
}

// Set clusters for active deployment
const deployments = this.parent.getLocalConfig().deployments;
deployments[currentDeploymentName].clusters = clusters;
this.parent.getLocalConfig().setDeployments(deployments);
// Prompt user for clusters and contexts
else {
clusters = Templates.parseCommaSeparatedList(await flags.clusterName.prompt(task, clusters));

this.parent.getK8().getKubeConfig().setCurrentContext(contextName);
const kubeContexts = this.parent.getK8().getContexts();
for (const cluster of clusters) {
if (!localConfig.clusterContextMapping[cluster]) {
localConfig.clusterContextMapping[cluster] = await flags.context.prompt(
task,
kubeContexts.map(c => c.name),
cluster,
);
}
}

this.parent.logger.info(
`Save LocalConfig file: [currentDeploymentName: ${currentDeploymentName}, contextName: ${contextName}, clusters: ${clusters.join(' ')}]`,
);
await this.parent.getLocalConfig().write();
selectedContext = localConfig.clusterContextMapping[clusters[0]];
}
}
}

this.parent.getK8().getKubeConfig().setCurrentContext(selectedContext);
});
}

initialize(argv: any) {
initialize(argv: any, configInit: ConfigBuilder) {
const {requiredFlags, optionalFlags} = argv;

argv.flags = [...requiredFlags, ...optionalFlags];
Expand All @@ -93,6 +174,8 @@ export class ContextCommandTasks {
if (argv[flags.devMode.name]) {
this.parent.logger.setDevMode(true);
}

ctx.config = await configInit(argv, ctx, task);
});
}
}
2 changes: 1 addition & 1 deletion src/commands/deployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export class DeploymentCommand extends BaseCommand {
return ListrLease.newAcquireLeaseTask(lease, task);
},
},
this.localConfig.promptLocalConfigTask(),
this.localConfig.promptLocalConfigTask(self.k8),
{
title: 'Validate cluster connections',
task: async (ctx, task): Promise<Listr<Context, any, any>> => {
Expand Down
6 changes: 3 additions & 3 deletions src/commands/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1413,15 +1413,15 @@ export class Flags {
constName: 'contextName',
name: 'context',
definition: {
describe: 'The Kubernetes context name to be used',
describe: 'The Kubernetes context name to be used. Multiple contexts can be separated by a comma',
defaultValue: '',
type: 'string',
},
prompt: async function promptContext(task: ListrTaskWrapper<any, any, any>, input: string[]) {
prompt: async function promptContext(task: ListrTaskWrapper<any, any, any>, input: string[], cluster?: string) {
return await task.prompt(ListrEnquirerPromptAdapter).run({
type: 'select',
name: 'context',
message: 'Select kubectl context',
message: 'Select kubectl context' + (cluster ? ` to be associated with cluster: ${cluster}` : ''),
choices: input,
});
},
Expand Down
Loading
Loading