Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
8 changes: 8 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"permissions": {
"allow": [
"Bash(git add:*)",
"Bash(git commit -m \"$\\(cat <<''EOF''\nUpdate System.CommandLine to 2.0.2 stable release\n\nThe System.CommandLine package was upgraded from the beta version to the\nstable 2.0.2 release, which introduced breaking API changes that required\nupdates across all CLI command files.\n\nAPI changes addressed:\n- AddCommand\\(\\) -> Subcommands.Add\\(\\) for adding subcommands\n- AddOption\\(\\) -> Options.Add\\(\\) for adding options\n- SetHandler\\(\\) extension -> SetAction\\(\\) method for command handlers\n- Option constructor signature changed: description is now set via\n Description property instead of constructor parameter\n- InvokeAsync\\(\\) on Command -> Parse\\(args\\).InvokeAsync\\(\\) in Program.cs\n\nFiles modified:\n- All command files under cli/src/Vdk/Commands/\n- cli/src/Vdk/Program.cs\n\nAlso normalizes line endings to LF per .gitattributes.\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")"
]
}
}
27 changes: 22 additions & 5 deletions ReadMe.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,26 @@ For detailed installation instructions for your operating system, please refer t

## Quick Start

1. **Create a default cluster:**
1. **Login (device code flow):**
```bash
vdk create cluster
vega login
```
Follow the printed link/code to authenticate. Tokens are stored under `~/.vega/tokens`.

2. **Create a default cluster:**
```bash
vega create cluster
```
*(This may take a few minutes)*

2. **Verify cluster access:**
3. **Verify cluster access:**
```bash
kubectl cluster-info --context kind-kind
```

3. **Delete the cluster:**
4. **Delete the cluster:**
```bash
vdk delete cluster
vega delete cluster
```

## Usage
Expand All @@ -55,6 +61,17 @@ For comprehensive usage details, examples, and command references, please see th
* **[Managing Clusters](./docs/usage/managing-clusters.md)**
* **[Command Reference](./docs/usage/command-reference.md)**

## Authentication

VDK enforces that you are logged in before executing most `vega` commands. Authentication uses an OAuth2 Device Code flow (Ory Hydra):

* __Login__: `vega login [--profile <name>]`
* __Logout__: `vega logout [--profile <name>]`
* __Multi-profile__: Use `--profile` to login or logout different affiliations. The current profile pointer is stored in `~/.vega/tokens/.current_profile`.
* __Token storage__: Access/refresh tokens are stored per-profile in `~/.vega/tokens/<profile>.json`. Refresh is automatic when the access token expires.

During cluster creation, VDK extracts `TenantId` from your access token and writes a ConfigMap named `vega-tenant` in the `vega-system` namespace so downstream tooling can correlate ownership.

## Contributing

We welcome contributions! Please read our **[Contribution Guidelines](./docs/contribution/guidelines.md)** and **[Development Setup](./docs/contribution/development-setup.md)** guides to get started.
Expand Down
4 changes: 3 additions & 1 deletion cli/src/Vdk/Commands/AppCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ namespace Vdk.Commands;

public class AppCommand : RootCommand
{
public AppCommand(CreateCommand create, RemoveCommand remove, ListCommand list, InitializeCommand init, UpdateCommand update, IHubClient client) : base("Vega CLI - Manage Vega development environment")
public AppCommand(CreateCommand create, RemoveCommand remove, ListCommand list, InitializeCommand init, UpdateCommand update, LoginCommand login, LogoutCommand logout, IHubClient client) : base("Vega CLI - Manage Vega development environment")
{
Add(create);
Add(remove);
Add(list);
Add(init);
Add(update);
Add(login);
Add(logout);
}
}
83 changes: 75 additions & 8 deletions cli/src/Vdk/Commands/CreateClusterCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public class CreateClusterCommand : Command
{
private readonly Func<string, IKubernetesClient> _clientFunc;
private readonly GlobalConfiguration _configs;
private readonly IAuthService _auth;
private readonly IConsole _console;
private readonly IFileSystem _fileSystem;
private readonly IFluxClient _flux;
Expand All @@ -32,7 +33,8 @@ public CreateClusterCommand(
IFluxClient flux,
IReverseProxyClient reverseProxy,
Func<string, IKubernetesClient> clientFunc,
GlobalConfiguration configs)
GlobalConfiguration configs,
IAuthService auth)
: base("cluster", "Create a Vega development cluster")
{
_console = console;
Expand All @@ -45,34 +47,58 @@ public CreateClusterCommand(
_reverseProxy = reverseProxy;
_clientFunc = clientFunc;
_configs = configs;
_auth = auth;

var nameOption = new Option<string>("--Name") { DefaultValueFactory = _ => Defaults.ClusterName, Description = "The name of the kind cluster to create." };
nameOption.Aliases.Add("-n");
var controlNodes = new Option<int>("--ControlPlaneNodes") { DefaultValueFactory = _ => Defaults.ControlPlaneNodes, Description = "The number of control plane nodes in the cluster." };
controlNodes.Aliases.Add("-c");
var workers = new Option<int>("--Workers") { DefaultValueFactory = _ => Defaults.WorkerNodes, Description = "The number of worker nodes in the cluster." };
workers.Aliases.Add("-w");
var kubeVersion = new Option<string>("--KubeVersion") { DefaultValueFactory = _ => "1.29", Description = "The kubernetes api version." };
var kubeVersion = new Option<string>("--KubeVersion") { DefaultValueFactory = _ => "", Description = "The kubernetes api version." };
kubeVersion.Aliases.Add("-k");
var labels = new Option<string>("--Labels") { DefaultValueFactory = _ => "", Description = "The labels to apply to the cluster to use in the configuration of Sectors. Each label pair should be separated by commas and the format should be KEY=VALUE. eg. KEY1=VAL1,KEY2=VAL2" };
labels.Aliases.Add("-l");

Options.Add(nameOption);
Options.Add(controlNodes);
Options.Add(workers);
Options.Add(kubeVersion);
Options.Add(labels);
SetAction(parseResult => InvokeAsync(
parseResult.GetValue(nameOption) ?? Defaults.ClusterName,
parseResult.GetValue(controlNodes),
parseResult.GetValue(workers),
parseResult.GetValue(kubeVersion)));
parseResult.GetValue(kubeVersion),
parseResult.GetValue(labels)));
}

public async Task InvokeAsync(string name = Defaults.ClusterName, int controlPlaneNodes = 1, int workerNodes = 2, string? kubeVersionRequested = null)
public async Task InvokeAsync(string name = Defaults.ClusterName, int controlPlaneNodes = 1, int workerNodes = 2, string? kubeVersionRequested = null, string? labels = null)
{
// check if the hub and proxy are there
if (!_reverseProxy.Exists())
_reverseProxy.Create();
if (!_hub.ExistRegistry())
_hub.CreateRegistry();

// validate the labels if they were passed in
var pairs = (labels??"").Split(',').Select(x=>x.Split('='));
if (pairs.Any())
{
//validate the labels and clean them up if needed
foreach (var pair in pairs)
{
pair[0] = pair[0].Trim();
if (pair.Length > 1)
pair[1] = pair[1].Trim();
if (pair.Length != 2 || string.IsNullOrWhiteSpace(pair[0]) || string.IsNullOrWhiteSpace(pair[1]))
{
_console.WriteError($"The provided label '{string.Join('=', pair)}' is not valid. Labels must be in the format KEY=VALUE and multiple labels must be separated by commas.");
return;
}
}
}

var map = await _kindVersionInfo.GetVersionInfoAsync();
string? kindVersion = null;
try
Expand All @@ -89,7 +115,7 @@ public async Task InvokeAsync(string name = Defaults.ClusterName, int controlPla
_console.WriteWarning($"Kind version {kindVersion} is not supported by the current VDK.");
return;
}
var kubeVersion = kubeVersionRequested ?? await _kindVersionInfo.GetDefaultKubernetesVersionAsync(kindVersion);
var kubeVersion = string.IsNullOrWhiteSpace(kubeVersionRequested) ? await _kindVersionInfo.GetDefaultKubernetesVersionAsync(kindVersion) : kubeVersionRequested.Trim();
var image = map.FindImage(kindVersion, kubeVersion);
if (image is null)
{
Expand Down Expand Up @@ -198,15 +224,56 @@ public async Task InvokeAsync(string name = Defaults.ClusterName, int controlPla
{
_reverseProxy.UpsertCluster(name.ToLower(), masterNode.ExtraPortMappings.First().HostPort,
masterNode.ExtraPortMappings.Last().HostPort);
var ns = _clientFunc(name.ToLower()).Get<V1Namespace>("vega-system");
var client = _clientFunc(name.ToLower());
var ns = client.Get<V1Namespace>("vega-system");
ns.EnsureMetadata().EnsureAnnotations()[_configs.MasterNodeAnnotation] = _yaml.Serialize(masterNode);
_clientFunc(name.ToLower()).Update(ns);
client.Update(ns);

// Write TenantId ConfigMap in vega-system
var tenantId = await _auth.GetTenantIdAsync();
if (!string.IsNullOrWhiteSpace(tenantId))
{
V1ConfigMap? cfg = null;
try
{
cfg = client.Get<V1ConfigMap>("vega-tenant", "vega-system");
}
catch { /* not found, will create */ }
Comment on lines +239 to +241
Copy link

Copilot AI Aug 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The empty catch block should specify the expected exception type (e.g., catch (HttpOperationException) or catch (KubernetesException)) to avoid catching unexpected exceptions that should be handled differently.

Suggested change
cfg = client.Get<V1ConfigMap>("vega-tenant", "vega-system");
}
catch { /* not found, will create */ }
catch (k8s.KubernetesException) { /* not found, will create */ }

Copilot uses AI. Check for mistakes.

if (cfg is null)
{
cfg = new V1ConfigMap
{
Metadata = new V1ObjectMeta { Name = "vega-tenant", NamespaceProperty = "vega-system" },
Data = new Dictionary<string, string> { ["TenantId"] = tenantId }
};
client.Create(cfg);
}
else
{
cfg.Data ??= new Dictionary<string, string>();
cfg.Data["TenantId"] = tenantId;
// add the label pairs here
foreach (var pair in pairs)
{
if (pair.Length == 2 && !string.IsNullOrWhiteSpace(pair[0]) && !string.IsNullOrWhiteSpace(pair[1]))
{
cfg.Data[$"{pair[0]}"] = pair[1];
}
}
client.Update(cfg);
}
}
else
{
_console.WriteWarning("No TenantId found in token; skipping tenant config map.");
}
}
catch (Exception e)
{
// print the stack trace
_console.WriteLine(e.StackTrace);
_console.WriteError("Failed to update reverse proxy: " + e.Message);
_console.WriteError("Failed to update reverse proxy or tenant config: " + e.Message);
throw e;
}
}
Expand Down
25 changes: 25 additions & 0 deletions cli/src/Vdk/Commands/LoginCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System.CommandLine;
using Vdk.Services;

namespace Vdk.Commands;

public class LoginCommand : Command
{
private readonly IAuthService _auth;

public LoginCommand(IAuthService auth) : base("login", "Authenticate with the Vega identity provider using device code flow")
{
_auth = auth;
var profile = new Option<string?>("--profile") { Description = "Optional profile name for this login (supports multiple accounts)" };
Options.Add(profile);
SetAction(async parseResult =>
{
var p = parseResult.GetValue(profile);
if (!string.IsNullOrWhiteSpace(p))
{
_auth.SetCurrentProfile(p!);
}
await _auth.LoginAsync(p);
});
}
}
25 changes: 25 additions & 0 deletions cli/src/Vdk/Commands/LogoutCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System.CommandLine;
using Vdk.Services;

namespace Vdk.Commands;

public class LogoutCommand : Command
{
private readonly IAuthService _auth;

public LogoutCommand(IAuthService auth) : base("logout", "Remove local credentials for the current or specified profile")
{
_auth = auth;
var profile = new Option<string?>("--profile") { Description = "Optional profile name to logout (defaults to current)" };
Options.Add(profile);
SetAction(async parseResult =>
{
var p = parseResult.GetValue(profile);
if (!string.IsNullOrWhiteSpace(p))
{
_auth.SetCurrentProfile(p!);
}
await _auth.LogoutAsync(p);
});
}
}
11 changes: 10 additions & 1 deletion cli/src/Vdk/GlobalConfiguration.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Vdk.Constants;
using Vdk.Constants;

namespace Vdk;

Expand All @@ -21,4 +21,13 @@ public string VegaDirectory
public string KindVersionInfoFilePath => Path.Combine(ConfigDirectoryPath, Defaults.KindVersionInfoFileName);

public string MasterNodeAnnotation = "vdk.vega.io/cluster";

// OAuth / Hydra configuration (defaults can be overridden later)
public string HydraDeviceAuthorizationEndpoint { get; set; } = "https://idp.dev-k8s.cloud/oidc/oauth2/device/auth";
public string HydraTokenEndpoint { get; set; } = "https://idp.dev-k8s.cloud/oidc/oauth2/token";
public string OAuthClientId { get; set; } = "vega-cli";
public string[] OAuthScopes { get; set; } = new[] { "openid", "offline", "profile" };

// JWT claim names
public string TenantIdClaim { get; set; } = "tenant_id";
}
8 changes: 8 additions & 0 deletions cli/src/Vdk/Models/AuthTokens.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Vdk.Models;

public class AuthTokens
{
public string AccessToken { get; set; } = string.Empty;
public string RefreshToken { get; set; } = string.Empty;
public DateTimeOffset ExpiresAt { get; set; }
}
13 changes: 13 additions & 0 deletions cli/src/Vdk/Program.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System.CommandLine;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Vdk.Commands;
using Vdk.Services;

namespace Vdk;

Expand All @@ -10,6 +12,17 @@ class Program

static async Task<int> Main(string[] args)
{
var auth = Services.GetRequiredService<IAuthService>();
// Skip auth for explicit non-exec commands
var skipAuth = args.Length == 0 ||
args.Contains("--help") || args.Contains("-h") ||
args.Contains("--version") ||
(args.Length > 0 && (string.Equals(args[0], "login", StringComparison.OrdinalIgnoreCase) ||
string.Equals(args[0], "logout", StringComparison.OrdinalIgnoreCase)));
if (!skipAuth)
{
await auth.EnsureAuthenticatedAsync();
}
return await Services.GetRequiredService<AppCommand>().Parse(args).InvokeAsync();
}
}
7 changes: 7 additions & 0 deletions cli/src/Vdk/ServiceProviderBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Octokit;
using Vdk.Constants;
using k8s.Exceptions;
using System.Net.Http;

namespace Vdk;

Expand All @@ -37,6 +38,8 @@ public static IServiceProvider Build()
.AddSingleton<CreateCommand>()
.AddSingleton<RemoveCommand>()
.AddSingleton<ListCommand>()
.AddSingleton<LoginCommand>()
.AddSingleton<LogoutCommand>()
.AddSingleton<CreateClusterCommand>()
.AddSingleton<RemoveClusterCommand>()
.AddSingleton<ListClustersCommand>()
Expand All @@ -57,6 +60,10 @@ public static IServiceProvider Build()
.AddSingleton<IFluxClient, FluxClient>()
.AddSingleton<IReverseProxyClient, ReverseProxyClient>()
.AddSingleton<IEmbeddedDataReader, EmbeddedDataReader>()
.AddSingleton<ITokenStore, TokenStoreFile>()
.AddSingleton<IAuthService, AuthService>()
.AddSingleton<HttpClient>(_ => new HttpClient())
.AddSingleton<HydraDeviceFlowClient>()
.AddSingleton<IDockerEngine>(provider =>
{
// Intelligent fallback logic
Expand Down
Loading
Loading