Add --account N flag and fix credential priority in Erlang, Elixir, OCaml, F#, Haskell SDKs
Correct 5-tier credential priority across all five implementations: 1. Explicit -p/-k flags (function arguments) 2. --account N -> accounts.csv row N (bypasses env vars) 3. UNSANDBOX_PUBLIC_KEY / UNSANDBOX_SECRET_KEY env vars 4. ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env) 5. ./accounts.csv row 0 Erlang: add load_credentials_from_csv/2, extract_account_arg/3, process-dict-based account_index, dispatch/1 helper, fix get_api_keys/0. Elixir: add load_credentials_from_csv/2, extract_account_arg/3, Process.put/get-based account_index, dispatch/1 helper, fix get_api_keys/0. OCaml: add cli_account_index ref, parse_accounts_csv, load_csv_at, strip_account_arg, fix get_credentials priority ordering. F#: add AccountIndex field to Args, loadCredentialsFromCsv, fix getApiKeys signature to accept accountIndex, wire --account N in parseArgs. Haskell: add cliAccountIndex IORef (unsafePerformIO), loadCredentialsFromCsv, stripAccountArg, fix getApiKeys priority ordering.
This commit is contained in:
parent
a839ffcd12
commit
13f15c8abc
5 changed files with 512 additions and 129 deletions
|
|
@ -51,8 +51,10 @@ defmodule Un do
|
||||||
|
|
||||||
Credentials are loaded in priority order:
|
Credentials are loaded in priority order:
|
||||||
1. Function arguments (public_key, secret_key)
|
1. Function arguments (public_key, secret_key)
|
||||||
2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
2. --account N -> accounts.csv row N (bypasses env vars)
|
||||||
3. Config file (~/.unsandbox/accounts.csv)
|
3. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
||||||
|
4. ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env)
|
||||||
|
5. ./accounts.csv row 0
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@blue "\e[34m"
|
@blue "\e[34m"
|
||||||
|
|
@ -1112,25 +1114,50 @@ defmodule Un do
|
||||||
# CLI Entry Point
|
# CLI Entry Point
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
def main([]), do: print_usage()
|
def main(raw_args) do
|
||||||
def main(["session" | rest]), do: session_command(rest)
|
{account_index, args} = extract_account_arg(raw_args, nil, [])
|
||||||
def main(["service" | rest]), do: service_command(rest)
|
if account_index != nil do
|
||||||
def main(["snapshot" | rest]), do: snapshot_command(rest)
|
Process.put(:account_index, account_index)
|
||||||
def main(["image" | rest]), do: image_command(rest)
|
end
|
||||||
def main(["key" | rest]), do: key_command(rest)
|
dispatch(args)
|
||||||
def main(["languages" | rest]), do: languages_command(rest)
|
end
|
||||||
def main(args), do: execute_command(args)
|
|
||||||
|
defp dispatch([]), do: print_usage()
|
||||||
|
defp dispatch(["session" | rest]), do: session_command(rest)
|
||||||
|
defp dispatch(["service" | rest]), do: service_command(rest)
|
||||||
|
defp dispatch(["snapshot" | rest]), do: snapshot_command(rest)
|
||||||
|
defp dispatch(["image" | rest]), do: image_command(rest)
|
||||||
|
defp dispatch(["key" | rest]), do: key_command(rest)
|
||||||
|
defp dispatch(["languages" | rest]), do: languages_command(rest)
|
||||||
|
defp dispatch(args), do: execute_command(args)
|
||||||
|
|
||||||
|
defp extract_account_arg([], acc, rest_acc), do: {acc, Enum.reverse(rest_acc)}
|
||||||
|
defp extract_account_arg(["--account", n_str | rest], _acc, rest_acc) do
|
||||||
|
n = case Integer.parse(n_str) do
|
||||||
|
{n, ""} -> n
|
||||||
|
_ ->
|
||||||
|
IO.puts(:stderr, "Error: --account requires an integer argument")
|
||||||
|
System.halt(1)
|
||||||
|
end
|
||||||
|
extract_account_arg(rest, n, rest_acc)
|
||||||
|
end
|
||||||
|
defp extract_account_arg([arg | rest], acc, rest_acc) do
|
||||||
|
extract_account_arg(rest, acc, [arg | rest_acc])
|
||||||
|
end
|
||||||
|
|
||||||
defp print_usage do
|
defp print_usage do
|
||||||
IO.puts("Usage: un.ex [options] <source_file>")
|
IO.puts("Usage: un.ex [--account N] [options] <source_file>")
|
||||||
IO.puts(" un.ex session [options]")
|
IO.puts(" un.ex [--account N] session [options]")
|
||||||
IO.puts(" un.ex service [options]")
|
IO.puts(" un.ex [--account N] service [options]")
|
||||||
IO.puts(" un.ex service env <action> <service_id>")
|
IO.puts(" un.ex [--account N] service env <action> <service_id>")
|
||||||
IO.puts(" un.ex snapshot [options]")
|
IO.puts(" un.ex [--account N] snapshot [options]")
|
||||||
IO.puts(" un.ex image [options]")
|
IO.puts(" un.ex [--account N] image [options]")
|
||||||
IO.puts(" un.ex key [--extend]")
|
IO.puts(" un.ex [--account N] key [--extend]")
|
||||||
IO.puts(" un.ex languages [--json]")
|
IO.puts(" un.ex languages [--json]")
|
||||||
IO.puts("")
|
IO.puts("")
|
||||||
|
IO.puts("Global options:")
|
||||||
|
IO.puts(" --account N Use accounts.csv row N (bypasses env vars)")
|
||||||
|
IO.puts("")
|
||||||
IO.puts("Service options: --name, --ports, --bootstrap, -e KEY=VALUE, --env-file FILE")
|
IO.puts("Service options: --name, --ports, --bootstrap, -e KEY=VALUE, --env-file FILE")
|
||||||
IO.puts(" --set-unfreeze-on-demand ID true|false")
|
IO.puts(" --set-unfreeze-on-demand ID true|false")
|
||||||
IO.puts("Service env commands: status, set, export, delete")
|
IO.puts("Service env commands: status, set, export, delete")
|
||||||
|
|
@ -1913,11 +1940,46 @@ defmodule Un do
|
||||||
end
|
end
|
||||||
|
|
||||||
# Helpers
|
# Helpers
|
||||||
|
|
||||||
|
defp load_credentials_from_csv(csv_path, account_index) do
|
||||||
|
case File.read(csv_path) do
|
||||||
|
{:ok, content} ->
|
||||||
|
accounts =
|
||||||
|
content
|
||||||
|
|> String.split("\n")
|
||||||
|
|> Enum.map(&String.trim/1)
|
||||||
|
|> Enum.filter(fn line -> line != "" and not String.starts_with?(line, "#") end)
|
||||||
|
|> Enum.flat_map(fn line ->
|
||||||
|
case String.split(line, ",") do
|
||||||
|
[pk, sk | _] ->
|
||||||
|
pk = String.trim(pk)
|
||||||
|
sk = String.trim(sk)
|
||||||
|
if String.length(pk) > 8 and String.length(sk) > 8 do
|
||||||
|
[{pk, sk}]
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
_ -> []
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
case Enum.at(accounts, account_index) do
|
||||||
|
nil -> :error
|
||||||
|
creds -> {:ok, creds}
|
||||||
|
end
|
||||||
|
_ -> :error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp get_api_keys do
|
defp get_api_keys do
|
||||||
|
home = System.get_env("HOME") || "."
|
||||||
|
home_csv = Path.join([home, ".unsandbox", "accounts.csv"])
|
||||||
|
|
||||||
|
# Priority 1: --account N (stored in process dict by main/1)
|
||||||
|
case Process.get(:account_index) do
|
||||||
|
nil ->
|
||||||
|
# Priority 2: environment variables
|
||||||
public_key = System.get_env("UNSANDBOX_PUBLIC_KEY")
|
public_key = System.get_env("UNSANDBOX_PUBLIC_KEY")
|
||||||
secret_key = System.get_env("UNSANDBOX_SECRET_KEY")
|
secret_key = System.get_env("UNSANDBOX_SECRET_KEY")
|
||||||
|
|
||||||
# Fall back to UNSANDBOX_API_KEY for backwards compatibility
|
|
||||||
api_key = System.get_env("UNSANDBOX_API_KEY")
|
api_key = System.get_env("UNSANDBOX_API_KEY")
|
||||||
|
|
||||||
cond do
|
cond do
|
||||||
|
|
@ -1926,10 +1988,39 @@ defmodule Un do
|
||||||
api_key ->
|
api_key ->
|
||||||
{api_key, nil}
|
{api_key, nil}
|
||||||
true ->
|
true ->
|
||||||
|
# Priority 3: ~/.unsandbox/accounts.csv (or UNSANDBOX_ACCOUNT index)
|
||||||
|
default_index =
|
||||||
|
case System.get_env("UNSANDBOX_ACCOUNT") do
|
||||||
|
nil -> 0
|
||||||
|
s -> case Integer.parse(s) do {n, ""} -> n; _ -> 0 end
|
||||||
|
end
|
||||||
|
case load_credentials_from_csv(home_csv, default_index) do
|
||||||
|
{:ok, {pk, sk}} -> {pk, sk}
|
||||||
|
:error ->
|
||||||
|
# Priority 4: ./accounts.csv
|
||||||
|
case load_credentials_from_csv("accounts.csv", default_index) do
|
||||||
|
{:ok, {pk, sk}} -> {pk, sk}
|
||||||
|
:error ->
|
||||||
IO.puts(:stderr, "Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)")
|
IO.puts(:stderr, "Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)")
|
||||||
System.halt(1)
|
System.halt(1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
account_index ->
|
||||||
|
# Priority 1: --account N -> accounts.csv
|
||||||
|
case load_credentials_from_csv(home_csv, account_index) do
|
||||||
|
{:ok, {pk, sk}} -> {pk, sk}
|
||||||
|
:error ->
|
||||||
|
case load_credentials_from_csv("accounts.csv", account_index) do
|
||||||
|
{:ok, {pk, sk}} -> {pk, sk}
|
||||||
|
:error ->
|
||||||
|
IO.puts(:stderr, "Error: No credentials found for account index #{account_index} in accounts.csv")
|
||||||
|
System.halt(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp get_api_key do
|
defp get_api_key do
|
||||||
{public_key, _} = get_api_keys()
|
{public_key, _} = get_api_keys()
|
||||||
|
|
|
||||||
|
|
@ -56,8 +56,10 @@
|
||||||
%%%
|
%%%
|
||||||
%%% Authentication Priority:
|
%%% Authentication Priority:
|
||||||
%%% 1. Function arguments (PublicKey, SecretKey)
|
%%% 1. Function arguments (PublicKey, SecretKey)
|
||||||
%%% 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
%%% 2. --account N -> accounts.csv row N (bypasses env vars)
|
||||||
%%% 3. Config file (~/.unsandbox/accounts.csv)
|
%%% 3. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
||||||
|
%%% 4. ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env)
|
||||||
|
%%% 5. ./accounts.csv row 0
|
||||||
|
|
||||||
-define(API_BASE, "https://api.unsandbox.com").
|
-define(API_BASE, "https://api.unsandbox.com").
|
||||||
-define(PORTAL_BASE, "https://unsandbox.com").
|
-define(PORTAL_BASE, "https://unsandbox.com").
|
||||||
|
|
@ -719,37 +721,60 @@ not_contains_error(Response) ->
|
||||||
%% CLI Entry Point
|
%% CLI Entry Point
|
||||||
%% ============================================================================
|
%% ============================================================================
|
||||||
|
|
||||||
main([]) ->
|
main(RawArgs) ->
|
||||||
io:format("Usage: un.erl [options] <source_file>~n"),
|
%% Strip --account N from args and store index in process dict before dispatch
|
||||||
io:format(" un.erl session [options]~n"),
|
{AccountIndex, Args} = extract_account_arg(RawArgs, undefined, []),
|
||||||
io:format(" un.erl service [options]~n"),
|
case AccountIndex of
|
||||||
io:format(" un.erl snapshot [options]~n"),
|
undefined -> ok;
|
||||||
io:format(" un.erl image [options]~n"),
|
N -> erlang:put(account_index, N)
|
||||||
io:format(" un.erl key [options]~n"),
|
end,
|
||||||
|
dispatch(Args).
|
||||||
|
|
||||||
|
dispatch([]) ->
|
||||||
|
io:format("Usage: un.erl [--account N] [options] <source_file>~n"),
|
||||||
|
io:format(" un.erl [--account N] session [options]~n"),
|
||||||
|
io:format(" un.erl [--account N] service [options]~n"),
|
||||||
|
io:format(" un.erl [--account N] snapshot [options]~n"),
|
||||||
|
io:format(" un.erl [--account N] image [options]~n"),
|
||||||
|
io:format(" un.erl [--account N] key [options]~n"),
|
||||||
io:format(" un.erl languages [--json]~n"),
|
io:format(" un.erl languages [--json]~n"),
|
||||||
|
io:format("~nGlobal options:~n"),
|
||||||
|
io:format(" --account N Use accounts.csv row N (bypasses env vars)~n"),
|
||||||
halt(1);
|
halt(1);
|
||||||
|
|
||||||
main(["session" | Rest]) ->
|
dispatch(["session" | Rest]) ->
|
||||||
session_command(Rest);
|
session_command(Rest);
|
||||||
|
|
||||||
main(["service" | Rest]) ->
|
dispatch(["service" | Rest]) ->
|
||||||
service_command(Rest);
|
service_command(Rest);
|
||||||
|
|
||||||
main(["snapshot" | Rest]) ->
|
dispatch(["snapshot" | Rest]) ->
|
||||||
snapshot_command(Rest);
|
snapshot_command(Rest);
|
||||||
|
|
||||||
main(["image" | Rest]) ->
|
dispatch(["image" | Rest]) ->
|
||||||
image_command(Rest);
|
image_command(Rest);
|
||||||
|
|
||||||
main(["key" | Rest]) ->
|
dispatch(["key" | Rest]) ->
|
||||||
key_command(Rest);
|
key_command(Rest);
|
||||||
|
|
||||||
main(["languages" | Rest]) ->
|
dispatch(["languages" | Rest]) ->
|
||||||
languages_command(Rest);
|
languages_command(Rest);
|
||||||
|
|
||||||
main(Args) ->
|
dispatch(Args) ->
|
||||||
execute_command(Args).
|
execute_command(Args).
|
||||||
|
|
||||||
|
%% Strip --account N from argument list, return {Index | undefined, RestArgs}
|
||||||
|
extract_account_arg([], Acc, RestAcc) ->
|
||||||
|
{Acc, lists:reverse(RestAcc)};
|
||||||
|
extract_account_arg(["--account", NStr | Rest], _Acc, RestAcc) ->
|
||||||
|
N = try list_to_integer(NStr) catch _:_ ->
|
||||||
|
io:format("Error: --account requires an integer argument~n"),
|
||||||
|
halt(1)
|
||||||
|
end,
|
||||||
|
extract_account_arg(Rest, N, RestAcc);
|
||||||
|
extract_account_arg([Arg | Rest], Acc, RestAcc) ->
|
||||||
|
extract_account_arg(Rest, Acc, [Arg | RestAcc]).
|
||||||
|
|
||||||
%% Execute command
|
%% Execute command
|
||||||
execute_command(Args) ->
|
execute_command(Args) ->
|
||||||
{File, _Opts} = parse_exec_args(Args, #{file => undefined}),
|
{File, _Opts} = parse_exec_args(Args, #{file => undefined}),
|
||||||
|
|
@ -1452,19 +1477,96 @@ open_extend_page(PublicKey) ->
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% Helpers
|
%% Helpers
|
||||||
|
|
||||||
|
%% @doc Load credentials from a CSV file at the given path.
|
||||||
|
%% Skips blank lines and comment lines (#). Returns {ok, {PK, SK}} or error.
|
||||||
|
load_credentials_from_csv(CsvPath, AccountIndex) ->
|
||||||
|
case file:read_file(CsvPath) of
|
||||||
|
{ok, Bin} ->
|
||||||
|
Lines = string:split(binary_to_list(Bin), "\n", all),
|
||||||
|
ValidAccounts = lists:filtermap(fun(Line) ->
|
||||||
|
Trimmed = string:trim(Line),
|
||||||
|
case Trimmed of
|
||||||
|
"" -> false;
|
||||||
|
[$# | _] -> false;
|
||||||
|
_ ->
|
||||||
|
Parts = string:split(Trimmed, ",", all),
|
||||||
|
case Parts of
|
||||||
|
[PK, SK | _] ->
|
||||||
|
PKt = string:trim(PK),
|
||||||
|
SKt = string:trim(SK),
|
||||||
|
if
|
||||||
|
length(PKt) > 8 andalso length(SKt) > 8 ->
|
||||||
|
{true, {PKt, SKt}};
|
||||||
|
true -> false
|
||||||
|
end;
|
||||||
|
_ -> false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end, Lines),
|
||||||
|
if
|
||||||
|
AccountIndex < length(ValidAccounts) ->
|
||||||
|
{ok, lists:nth(AccountIndex + 1, ValidAccounts)};
|
||||||
|
true ->
|
||||||
|
error
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
error
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% @doc Resolve credentials with correct priority:
|
||||||
|
%% 1. --account N process-dict override -> accounts.csv row N
|
||||||
|
%% 2. UNSANDBOX_PUBLIC_KEY / UNSANDBOX_SECRET_KEY env vars
|
||||||
|
%% 3. ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env)
|
||||||
|
%% 4. ./accounts.csv row 0
|
||||||
get_api_keys() ->
|
get_api_keys() ->
|
||||||
|
Home = case os:getenv("HOME") of false -> "."; H -> H end,
|
||||||
|
HomeCsv = filename:join([Home, ".unsandbox", "accounts.csv"]),
|
||||||
|
%% Priority 1: explicit --account N (stored in process dict by main/1)
|
||||||
|
case erlang:get(account_index) of
|
||||||
|
undefined ->
|
||||||
|
%% Priority 2: environment variables
|
||||||
PublicKey = os:getenv("UNSANDBOX_PUBLIC_KEY"),
|
PublicKey = os:getenv("UNSANDBOX_PUBLIC_KEY"),
|
||||||
SecretKey = os:getenv("UNSANDBOX_SECRET_KEY"),
|
SecretKey = os:getenv("UNSANDBOX_SECRET_KEY"),
|
||||||
ApiKey = os:getenv("UNSANDBOX_API_KEY"),
|
ApiKey = os:getenv("UNSANDBOX_API_KEY"),
|
||||||
|
|
||||||
if
|
if
|
||||||
PublicKey =/= false andalso SecretKey =/= false ->
|
PublicKey =/= false andalso SecretKey =/= false ->
|
||||||
{PublicKey, SecretKey};
|
{PublicKey, SecretKey};
|
||||||
ApiKey =/= false ->
|
ApiKey =/= false ->
|
||||||
{ApiKey, false};
|
{ApiKey, false};
|
||||||
true ->
|
true ->
|
||||||
|
%% Priority 3: ~/.unsandbox/accounts.csv (or UNSANDBOX_ACCOUNT index)
|
||||||
|
DefaultIndex = case os:getenv("UNSANDBOX_ACCOUNT") of
|
||||||
|
false -> 0;
|
||||||
|
IdxStr -> try list_to_integer(string:trim(IdxStr)) catch _:_ -> 0 end
|
||||||
|
end,
|
||||||
|
case load_credentials_from_csv(HomeCsv, DefaultIndex) of
|
||||||
|
{ok, {PK, SK}} ->
|
||||||
|
{PK, SK};
|
||||||
|
error ->
|
||||||
|
%% Priority 4: ./accounts.csv
|
||||||
|
case load_credentials_from_csv("accounts.csv", DefaultIndex) of
|
||||||
|
{ok, {PK2, SK2}} ->
|
||||||
|
{PK2, SK2};
|
||||||
|
error ->
|
||||||
io:format("Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)~n"),
|
io:format("Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)~n"),
|
||||||
halt(1)
|
halt(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end;
|
||||||
|
AccountIndex ->
|
||||||
|
case load_credentials_from_csv(HomeCsv, AccountIndex) of
|
||||||
|
{ok, {PK, SK}} ->
|
||||||
|
{PK, SK};
|
||||||
|
error ->
|
||||||
|
case load_credentials_from_csv("accounts.csv", AccountIndex) of
|
||||||
|
{ok, {PK2, SK2}} ->
|
||||||
|
{PK2, SK2};
|
||||||
|
error ->
|
||||||
|
io:format("Error: No credentials found for account index ~B in accounts.csv~n", [AccountIndex]),
|
||||||
|
halt(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
end.
|
end.
|
||||||
|
|
||||||
get_api_key() ->
|
get_api_key() ->
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,7 @@ type Args = {
|
||||||
mutable Command: string option
|
mutable Command: string option
|
||||||
mutable SourceFile: string option
|
mutable SourceFile: string option
|
||||||
mutable ApiKey: string option
|
mutable ApiKey: string option
|
||||||
|
mutable AccountIndex: int option
|
||||||
mutable Network: string option
|
mutable Network: string option
|
||||||
mutable Vcpu: int
|
mutable Vcpu: int
|
||||||
Env: ResizeArray<string>
|
Env: ResizeArray<string>
|
||||||
|
|
@ -144,19 +145,70 @@ type Args = {
|
||||||
mutable ImagePorts: string option
|
mutable ImagePorts: string option
|
||||||
}
|
}
|
||||||
|
|
||||||
let getApiKeys (argsKey: string option) =
|
let loadCredentialsFromCsv (csvPath: string) (accountIndex: int) =
|
||||||
|
if File.Exists(csvPath) then
|
||||||
|
try
|
||||||
|
let lines = File.ReadAllLines(csvPath)
|
||||||
|
let accounts =
|
||||||
|
lines
|
||||||
|
|> Array.map (fun l -> l.Trim())
|
||||||
|
|> Array.filter (fun l -> l.Length > 0 && not (l.StartsWith("#")))
|
||||||
|
|> Array.choose (fun line ->
|
||||||
|
let parts = line.Split(',')
|
||||||
|
if parts.Length >= 2 then
|
||||||
|
let pk = parts.[0].Trim()
|
||||||
|
let sk = parts.[1].Trim()
|
||||||
|
if pk.Length > 8 && sk.Length > 8 then Some (pk, sk)
|
||||||
|
else None
|
||||||
|
else None)
|
||||||
|
if accountIndex < accounts.Length then Some accounts.[accountIndex]
|
||||||
|
else None
|
||||||
|
with _ -> None
|
||||||
|
else None
|
||||||
|
|
||||||
|
let getApiKeys (argsKey: string option) (accountIndex: int option) =
|
||||||
|
let home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)
|
||||||
|
let homeCsv = Path.Combine(home, ".unsandbox", "accounts.csv")
|
||||||
|
|
||||||
|
// Priority 1: --account N -> accounts.csv row N (bypasses env vars)
|
||||||
|
match accountIndex with
|
||||||
|
| Some idx ->
|
||||||
|
let creds =
|
||||||
|
match loadCredentialsFromCsv homeCsv idx with
|
||||||
|
| Some c -> Some c
|
||||||
|
| None -> loadCredentialsFromCsv "accounts.csv" idx
|
||||||
|
match creds with
|
||||||
|
| Some (pk, sk) -> (pk, sk)
|
||||||
|
| None ->
|
||||||
|
eprintfn "%sError: No credentials found for account index %d in accounts.csv%s" red idx reset
|
||||||
|
exit 1
|
||||||
|
| None ->
|
||||||
let publicKey = Environment.GetEnvironmentVariable("UNSANDBOX_PUBLIC_KEY")
|
let publicKey = Environment.GetEnvironmentVariable("UNSANDBOX_PUBLIC_KEY")
|
||||||
let secretKey = Environment.GetEnvironmentVariable("UNSANDBOX_SECRET_KEY")
|
let secretKey = Environment.GetEnvironmentVariable("UNSANDBOX_SECRET_KEY")
|
||||||
|
|
||||||
// Fall back to UNSANDBOX_API_KEY for backwards compatibility
|
// Priority 2: environment variables
|
||||||
if String.IsNullOrEmpty(publicKey) || String.IsNullOrEmpty(secretKey) then
|
if not (String.IsNullOrEmpty(publicKey)) && not (String.IsNullOrEmpty(secretKey)) then
|
||||||
|
(publicKey, secretKey)
|
||||||
|
else
|
||||||
|
// Fall back to legacy UNSANDBOX_API_KEY
|
||||||
let legacyKey = match argsKey with | Some k -> k | None -> Environment.GetEnvironmentVariable("UNSANDBOX_API_KEY")
|
let legacyKey = match argsKey with | Some k -> k | None -> Environment.GetEnvironmentVariable("UNSANDBOX_API_KEY")
|
||||||
if String.IsNullOrEmpty(legacyKey) then
|
if not (String.IsNullOrEmpty(legacyKey)) then
|
||||||
eprintfn "%sError: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set%s" red reset
|
|
||||||
exit 1
|
|
||||||
(legacyKey, null)
|
(legacyKey, null)
|
||||||
else
|
else
|
||||||
(publicKey, secretKey)
|
// Priority 3: ~/.unsandbox/accounts.csv (or UNSANDBOX_ACCOUNT index)
|
||||||
|
let defaultIndex =
|
||||||
|
let envIdx = Environment.GetEnvironmentVariable("UNSANDBOX_ACCOUNT")
|
||||||
|
if String.IsNullOrEmpty(envIdx) then 0
|
||||||
|
else match System.Int32.TryParse(envIdx) with | (true, n) -> n | _ -> 0
|
||||||
|
let creds =
|
||||||
|
match loadCredentialsFromCsv homeCsv defaultIndex with
|
||||||
|
| Some c -> Some c
|
||||||
|
| None -> loadCredentialsFromCsv "accounts.csv" defaultIndex
|
||||||
|
match creds with
|
||||||
|
| Some (pk, sk) -> (pk, sk)
|
||||||
|
| None ->
|
||||||
|
eprintfn "%sError: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set%s" red reset
|
||||||
|
exit 1
|
||||||
|
|
||||||
let detectLanguage (filename: string) =
|
let detectLanguage (filename: string) =
|
||||||
let dotIndex = filename.LastIndexOf('.')
|
let dotIndex = filename.LastIndexOf('.')
|
||||||
|
|
@ -631,7 +683,7 @@ let cmdServiceEnv (args: Args) (publicKey: string) (secretKey: string) =
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
let cmdExecute (args: Args) =
|
let cmdExecute (args: Args) =
|
||||||
let (publicKey, secretKey) = getApiKeys args.ApiKey
|
let (publicKey, secretKey) = getApiKeys args.ApiKey args.AccountIndex
|
||||||
let code = File.ReadAllText(args.SourceFile.Value)
|
let code = File.ReadAllText(args.SourceFile.Value)
|
||||||
let language = detectLanguage args.SourceFile.Value
|
let language = detectLanguage args.SourceFile.Value
|
||||||
|
|
||||||
|
|
@ -680,7 +732,7 @@ let cmdExecute (args: Args) =
|
||||||
exit exitCode
|
exit exitCode
|
||||||
|
|
||||||
let cmdSession (args: Args) =
|
let cmdSession (args: Args) =
|
||||||
let (publicKey, secretKey) = getApiKeys args.ApiKey
|
let (publicKey, secretKey) = getApiKeys args.ApiKey args.AccountIndex
|
||||||
|
|
||||||
if args.SessionSnapshot.IsSome then
|
if args.SessionSnapshot.IsSome then
|
||||||
let mutable payload = []
|
let mutable payload = []
|
||||||
|
|
@ -740,7 +792,7 @@ let openBrowser (url: string) =
|
||||||
eprintfn "%sError opening browser: %s%s" red ex.Message reset
|
eprintfn "%sError opening browser: %s%s" red ex.Message reset
|
||||||
|
|
||||||
let cmdKey (args: Args) =
|
let cmdKey (args: Args) =
|
||||||
let (publicKey, secretKey) = getApiKeys args.ApiKey
|
let (publicKey, secretKey) = getApiKeys args.ApiKey args.AccountIndex
|
||||||
|
|
||||||
ServicePointManager.SecurityProtocol <- SecurityProtocolType.Tls12 ||| SecurityProtocolType.Tls11 ||| SecurityProtocolType.Tls
|
ServicePointManager.SecurityProtocol <- SecurityProtocolType.Tls12 ||| SecurityProtocolType.Tls11 ||| SecurityProtocolType.Tls
|
||||||
|
|
||||||
|
|
@ -817,7 +869,7 @@ let cmdKey (args: Args) =
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
let cmdLanguages (args: Args) =
|
let cmdLanguages (args: Args) =
|
||||||
let (publicKey, secretKey) = getApiKeys args.ApiKey
|
let (publicKey, secretKey) = getApiKeys args.ApiKey args.AccountIndex
|
||||||
|
|
||||||
// Try to load from cache first
|
// Try to load from cache first
|
||||||
let cachedResponse = loadLanguagesCache ()
|
let cachedResponse = loadLanguagesCache ()
|
||||||
|
|
@ -872,7 +924,7 @@ let cmdLanguages (args: Args) =
|
||||||
printfn "%s" lang
|
printfn "%s" lang
|
||||||
|
|
||||||
let cmdImage (args: Args) =
|
let cmdImage (args: Args) =
|
||||||
let (publicKey, secretKey) = getApiKeys args.ApiKey
|
let (publicKey, secretKey) = getApiKeys args.ApiKey args.AccountIndex
|
||||||
|
|
||||||
if args.ImageList then
|
if args.ImageList then
|
||||||
let result = apiRequest "/images" "GET" None publicKey secretKey
|
let result = apiRequest "/images" "GET" None publicKey secretKey
|
||||||
|
|
@ -928,7 +980,7 @@ let cmdImage (args: Args) =
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
let cmdSnapshot (args: Args) =
|
let cmdSnapshot (args: Args) =
|
||||||
let (publicKey, secretKey) = getApiKeys args.ApiKey
|
let (publicKey, secretKey) = getApiKeys args.ApiKey args.AccountIndex
|
||||||
|
|
||||||
if args.SnapshotList then
|
if args.SnapshotList then
|
||||||
let result = apiRequest "/snapshots" "GET" None publicKey secretKey
|
let result = apiRequest "/snapshots" "GET" None publicKey secretKey
|
||||||
|
|
@ -959,7 +1011,7 @@ let cmdSnapshot (args: Args) =
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
let cmdService (args: Args) =
|
let cmdService (args: Args) =
|
||||||
let (publicKey, secretKey) = getApiKeys args.ApiKey
|
let (publicKey, secretKey) = getApiKeys args.ApiKey args.AccountIndex
|
||||||
|
|
||||||
// Handle env subcommand
|
// Handle env subcommand
|
||||||
if args.EnvAction.IsSome then
|
if args.EnvAction.IsSome then
|
||||||
|
|
@ -1107,6 +1159,7 @@ let parseArgs (argv: string[]) =
|
||||||
Command = None
|
Command = None
|
||||||
SourceFile = None
|
SourceFile = None
|
||||||
ApiKey = None
|
ApiKey = None
|
||||||
|
AccountIndex = None
|
||||||
Network = None
|
Network = None
|
||||||
Vcpu = 0
|
Vcpu = 0
|
||||||
Env = ResizeArray<string>()
|
Env = ResizeArray<string>()
|
||||||
|
|
@ -1192,6 +1245,13 @@ let parseArgs (argv: string[]) =
|
||||||
i <- i + 1
|
i <- i + 1
|
||||||
args.EnvTarget <- Some argv.[i]
|
args.EnvTarget <- Some argv.[i]
|
||||||
| "-k" | "--api-key" -> i <- i + 1; args.ApiKey <- Some argv.[i]
|
| "-k" | "--api-key" -> i <- i + 1; args.ApiKey <- Some argv.[i]
|
||||||
|
| "--account" ->
|
||||||
|
i <- i + 1
|
||||||
|
match System.Int32.TryParse(argv.[i]) with
|
||||||
|
| (true, n) -> args.AccountIndex <- Some n
|
||||||
|
| _ ->
|
||||||
|
eprintfn "Error: --account requires an integer argument"
|
||||||
|
Environment.Exit(1)
|
||||||
| "-n" | "--network" -> i <- i + 1; args.Network <- Some argv.[i]
|
| "-n" | "--network" -> i <- i + 1; args.Network <- Some argv.[i]
|
||||||
| "-v" | "--vcpu" -> i <- i + 1; args.Vcpu <- int argv.[i]
|
| "-v" | "--vcpu" -> i <- i + 1; args.Vcpu <- int argv.[i]
|
||||||
| "-e" | "--env" -> i <- i + 1; args.Env.Add(argv.[i])
|
| "-e" | "--env" -> i <- i + 1; args.Env.Add(argv.[i])
|
||||||
|
|
@ -1358,6 +1418,7 @@ let printHelp () =
|
||||||
printfn " -n MODE Network mode (zerotrust/semitrusted)"
|
printfn " -n MODE Network mode (zerotrust/semitrusted)"
|
||||||
printfn " -v N vCPU count (1-8)"
|
printfn " -v N vCPU count (1-8)"
|
||||||
printfn " -k KEY API key"
|
printfn " -k KEY API key"
|
||||||
|
printfn " --account N Use accounts.csv row N (bypasses env vars)"
|
||||||
printfn ""
|
printfn ""
|
||||||
printfn "Session options:"
|
printfn "Session options:"
|
||||||
printfn " --list List active sessions"
|
printfn " --list List active sessions"
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,8 @@ import Data.Char (isDigit, ord)
|
||||||
import Text.Printf (printf)
|
import Text.Printf (printf)
|
||||||
import Control.Monad (when, unless, forM_)
|
import Control.Monad (when, unless, forM_)
|
||||||
import Control.Exception (try, catch, IOError)
|
import Control.Exception (try, catch, IOError)
|
||||||
|
import Data.IORef (IORef, newIORef, readIORef, writeIORef)
|
||||||
|
import System.IO.Unsafe (unsafePerformIO)
|
||||||
import qualified Data.ByteString as BS
|
import qualified Data.ByteString as BS
|
||||||
import qualified Data.ByteString.Char8 as BSC
|
import qualified Data.ByteString.Char8 as BSC
|
||||||
import qualified Data.ByteString.Base64 as B64
|
import qualified Data.ByteString.Base64 as B64
|
||||||
|
|
@ -86,6 +88,11 @@ portalBase = "https://unsandbox.com"
|
||||||
languagesCacheTtl :: Int
|
languagesCacheTtl :: Int
|
||||||
languagesCacheTtl = 3600 -- 1 hour in seconds
|
languagesCacheTtl = 3600 -- 1 hour in seconds
|
||||||
|
|
||||||
|
-- Global account index set by --account N flag (Nothing = not set)
|
||||||
|
{-# NOINLINE cliAccountIndex #-}
|
||||||
|
cliAccountIndex :: IORef (Maybe Int)
|
||||||
|
cliAccountIndex = unsafePerformIO (newIORef Nothing)
|
||||||
|
|
||||||
-- ANSI colors
|
-- ANSI colors
|
||||||
blue, red, green, yellow, reset :: String
|
blue, red, green, yellow, reset :: String
|
||||||
blue = "\x1b[34m"
|
blue = "\x1b[34m"
|
||||||
|
|
@ -838,10 +845,26 @@ threadDelay us = do
|
||||||
_ <- readProcessWithExitCode "sleep" [show (fromIntegral ms / 1000.0 :: Double)] ""
|
_ <- readProcessWithExitCode "sleep" [show (fromIntegral ms / 1000.0 :: Double)] ""
|
||||||
return ()
|
return ()
|
||||||
|
|
||||||
|
-- Strip --account N from argument list, set cliAccountIndex IORef
|
||||||
|
stripAccountArg :: [String] -> IO [String]
|
||||||
|
stripAccountArg [] = return []
|
||||||
|
stripAccountArg ("--account":n_str:rest) = do
|
||||||
|
case reads n_str of
|
||||||
|
[(n, "")] -> do
|
||||||
|
writeIORef cliAccountIndex (Just n)
|
||||||
|
stripAccountArg rest
|
||||||
|
_ -> do
|
||||||
|
hPutStrLn stderr "Error: --account requires an integer argument"
|
||||||
|
exitFailure
|
||||||
|
stripAccountArg (arg:rest) = do
|
||||||
|
rest' <- stripAccountArg rest
|
||||||
|
return (arg : rest')
|
||||||
|
|
||||||
-- Main
|
-- Main
|
||||||
main :: IO ()
|
main :: IO ()
|
||||||
main = do
|
main = do
|
||||||
args <- getArgs
|
rawArgs <- getArgs
|
||||||
|
args <- stripAccountArg rawArgs
|
||||||
cmd <- parseArgs args
|
cmd <- parseArgs args
|
||||||
case cmd of
|
case cmd of
|
||||||
Execute opts -> executeCommand opts
|
Execute opts -> executeCommand opts
|
||||||
|
|
@ -865,6 +888,9 @@ printHelp = do
|
||||||
putStrLn " un.hs languages [--json] List available languages"
|
putStrLn " un.hs languages [--json] List available languages"
|
||||||
putStrLn " un.hs key [options] Validate/extend API key"
|
putStrLn " un.hs key [options] Validate/extend API key"
|
||||||
putStrLn ""
|
putStrLn ""
|
||||||
|
putStrLn "Global options:"
|
||||||
|
putStrLn " --account N Use accounts.csv row N (bypasses env vars)"
|
||||||
|
putStrLn ""
|
||||||
putStrLn "Execute options:"
|
putStrLn "Execute options:"
|
||||||
putStrLn " -e KEY=VALUE Environment variable"
|
putStrLn " -e KEY=VALUE Environment variable"
|
||||||
putStrLn " -f FILE Input file"
|
putStrLn " -f FILE Input file"
|
||||||
|
|
@ -1433,9 +1459,56 @@ serviceEnvDelete serviceId = do
|
||||||
(exitCode, _, _) <- curlDelete apiKey (apiBase ++ "/services/" ++ serviceId ++ "/env")
|
(exitCode, _, _) <- curlDelete apiKey (apiBase ++ "/services/" ++ serviceId ++ "/env")
|
||||||
return (exitCode == ExitSuccess)
|
return (exitCode == ExitSuccess)
|
||||||
|
|
||||||
-- Get API keys from environment
|
-- Load credentials from a CSV file at a given account index
|
||||||
|
loadCredentialsFromCsv :: FilePath -> Int -> IO (Maybe (String, String))
|
||||||
|
loadCredentialsFromCsv csvPath accountIndex = do
|
||||||
|
result <- (try (readFile csvPath) :: IO (Either IOError String))
|
||||||
|
case result of
|
||||||
|
Left _ -> return Nothing
|
||||||
|
Right content -> do
|
||||||
|
let ls = filter (\l -> not (null l) && head l /= '#') $
|
||||||
|
map trim $
|
||||||
|
lines content
|
||||||
|
accounts = [ (pk', sk')
|
||||||
|
| l <- ls
|
||||||
|
, let (pk, rest) = break (== ',') l
|
||||||
|
, not (null rest)
|
||||||
|
, let pk' = trim pk
|
||||||
|
sk' = trim (drop 1 rest)
|
||||||
|
, length pk' > 8 && length sk' > 8
|
||||||
|
]
|
||||||
|
if accountIndex < length accounts then
|
||||||
|
return $ Just (accounts !! accountIndex)
|
||||||
|
else
|
||||||
|
return Nothing
|
||||||
|
where
|
||||||
|
trim = reverse . dropWhile (== ' ') . reverse . dropWhile (== ' ')
|
||||||
|
|
||||||
|
-- Get API keys with correct priority:
|
||||||
|
-- 1. --account N (cliAccountIndex IORef) -> accounts.csv row N
|
||||||
|
-- 2. UNSANDBOX_PUBLIC_KEY / UNSANDBOX_SECRET_KEY env vars
|
||||||
|
-- 3. ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env)
|
||||||
|
-- 4. ./accounts.csv row 0
|
||||||
getApiKeys :: IO (String, Maybe String)
|
getApiKeys :: IO (String, Maybe String)
|
||||||
getApiKeys = do
|
getApiKeys = do
|
||||||
|
home <- maybe "." id <$> lookupEnv "HOME"
|
||||||
|
let homeCsv = home ++ "/.unsandbox/accounts.csv"
|
||||||
|
-- Priority 1: --account N
|
||||||
|
mIdx <- readIORef cliAccountIndex
|
||||||
|
case mIdx of
|
||||||
|
Just idx -> do
|
||||||
|
creds <- loadCredentialsFromCsv homeCsv idx
|
||||||
|
case creds of
|
||||||
|
Just (pk, sk) -> return (pk, Just sk)
|
||||||
|
Nothing -> do
|
||||||
|
creds2 <- loadCredentialsFromCsv "accounts.csv" idx
|
||||||
|
case creds2 of
|
||||||
|
Just (pk, sk) -> return (pk, Just sk)
|
||||||
|
Nothing -> do
|
||||||
|
hPutStrLn stderr $ "Error: No credentials found for account index " ++ show idx ++ " in accounts.csv"
|
||||||
|
exitFailure
|
||||||
|
Nothing -> do
|
||||||
|
-- Priority 2: environment variables
|
||||||
publicKey <- lookupEnv "UNSANDBOX_PUBLIC_KEY"
|
publicKey <- lookupEnv "UNSANDBOX_PUBLIC_KEY"
|
||||||
secretKey <- lookupEnv "UNSANDBOX_SECRET_KEY"
|
secretKey <- lookupEnv "UNSANDBOX_SECRET_KEY"
|
||||||
apiKey <- lookupEnv "UNSANDBOX_API_KEY"
|
apiKey <- lookupEnv "UNSANDBOX_API_KEY"
|
||||||
|
|
@ -1443,6 +1516,18 @@ getApiKeys = do
|
||||||
(Just pk, Just sk, _) -> return (pk, Just sk)
|
(Just pk, Just sk, _) -> return (pk, Just sk)
|
||||||
(_, _, Just ak) -> return (ak, Nothing)
|
(_, _, Just ak) -> return (ak, Nothing)
|
||||||
_ -> do
|
_ -> do
|
||||||
|
-- Priority 3: ~/.unsandbox/accounts.csv (or UNSANDBOX_ACCOUNT index)
|
||||||
|
defaultIndexStr <- lookupEnv "UNSANDBOX_ACCOUNT"
|
||||||
|
let defaultIndex = maybe 0 (\s -> case reads s of [(n,"")] -> n; _ -> 0) defaultIndexStr
|
||||||
|
creds <- loadCredentialsFromCsv homeCsv defaultIndex
|
||||||
|
case creds of
|
||||||
|
Just (pk, sk) -> return (pk, Just sk)
|
||||||
|
Nothing -> do
|
||||||
|
-- Priority 4: ./accounts.csv
|
||||||
|
creds2 <- loadCredentialsFromCsv "accounts.csv" defaultIndex
|
||||||
|
case creds2 of
|
||||||
|
Just (pk, sk) -> return (pk, Just sk)
|
||||||
|
Nothing -> do
|
||||||
hPutStrLn stderr "Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)"
|
hPutStrLn stderr "Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)"
|
||||||
exitFailure
|
exitFailure
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -281,39 +281,53 @@ let extract_json_int json_str key =
|
||||||
Credentials Management
|
Credentials Management
|
||||||
============================================================================ *)
|
============================================================================ *)
|
||||||
|
|
||||||
(** Get credentials from config file ~/.unsandbox/accounts.csv *)
|
(** Global account index set by --account N CLI flag; -1 means not set *)
|
||||||
let get_credentials_from_file ?(account_index=0) () =
|
let cli_account_index = ref (-1)
|
||||||
let home = try Sys.getenv "HOME" with Not_found -> "." in
|
|
||||||
let accounts_path = Filename.concat home ".unsandbox/accounts.csv" in
|
(** Parse accounts from CSV content, return list of (pk, sk) pairs *)
|
||||||
if Sys.file_exists accounts_path then
|
let parse_accounts_csv content =
|
||||||
try
|
|
||||||
let content = read_file accounts_path in
|
|
||||||
let lines = String.split_on_char '\n' content in
|
let lines = String.split_on_char '\n' content in
|
||||||
let valid_accounts = List.filter_map (fun line ->
|
List.filter_map (fun line ->
|
||||||
let line = String.trim line in
|
let line = String.trim line in
|
||||||
if String.length line = 0 || line.[0] = '#' then None
|
if String.length line = 0 || line.[0] = '#' then None
|
||||||
else
|
else
|
||||||
try
|
try
|
||||||
let comma_pos = String.index line ',' in
|
let comma_pos = String.index line ',' in
|
||||||
let pk = String.sub line 0 comma_pos in
|
let pk = String.trim (String.sub line 0 comma_pos) in
|
||||||
let sk = String.sub line (comma_pos + 1) (String.length line - comma_pos - 1) in
|
let sk = String.trim (String.sub line (comma_pos + 1) (String.length line - comma_pos - 1)) in
|
||||||
if String.length pk > 8 && String.sub pk 0 8 = "unsb-pk-" &&
|
if String.length pk > 8 && String.length sk > 8 then
|
||||||
String.length sk > 8 && String.sub sk 0 8 = "unsb-sk-" then
|
|
||||||
Some (pk, sk)
|
Some (pk, sk)
|
||||||
else None
|
else None
|
||||||
with Not_found -> None
|
with Not_found -> None
|
||||||
) lines in
|
) lines
|
||||||
if account_index < List.length valid_accounts then
|
|
||||||
Some (List.nth valid_accounts account_index)
|
(** Load credentials from a specific CSV path at the given account index *)
|
||||||
|
let load_csv_at path account_index =
|
||||||
|
if Sys.file_exists path then
|
||||||
|
try
|
||||||
|
let content = read_file path in
|
||||||
|
let accounts = parse_accounts_csv content in
|
||||||
|
if account_index < List.length accounts then
|
||||||
|
Some (List.nth accounts account_index)
|
||||||
else None
|
else None
|
||||||
with _ -> None
|
with _ -> None
|
||||||
else None
|
else None
|
||||||
|
|
||||||
|
(** Get credentials from config file ~/.unsandbox/accounts.csv *)
|
||||||
|
let get_credentials_from_file ?(account_index=0) () =
|
||||||
|
let home = try Sys.getenv "HOME" with Not_found -> "." in
|
||||||
|
let home_csv = Filename.concat home ".unsandbox/accounts.csv" in
|
||||||
|
match load_csv_at home_csv account_index with
|
||||||
|
| Some _ as r -> r
|
||||||
|
| None -> load_csv_at "accounts.csv" account_index
|
||||||
|
|
||||||
(**
|
(**
|
||||||
Get API credentials in priority order:
|
Get API credentials in priority order:
|
||||||
1. Function arguments
|
1. Function arguments (public_key, secret_key)
|
||||||
2. Environment variables
|
2. --account N (cli_account_index ref) -> accounts.csv row N
|
||||||
3. ~/.unsandbox/accounts.csv
|
3. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
||||||
|
4. ~/.unsandbox/accounts.csv row 0 (or UNSANDBOX_ACCOUNT env)
|
||||||
|
5. ./accounts.csv row 0
|
||||||
|
|
||||||
@param public_key Optional public key override
|
@param public_key Optional public key override
|
||||||
@param secret_key Optional secret key override
|
@param secret_key Optional secret key override
|
||||||
|
|
@ -326,18 +340,32 @@ let get_credentials ?public_key ?secret_key ?(account_index=0) () =
|
||||||
match (public_key, secret_key) with
|
match (public_key, secret_key) with
|
||||||
| (Some pk, Some sk) -> (pk, sk)
|
| (Some pk, Some sk) -> (pk, sk)
|
||||||
| _ ->
|
| _ ->
|
||||||
(* Priority 2: Environment variables *)
|
(* Priority 2: --account N CLI flag overrides env vars *)
|
||||||
|
let effective_index = if !cli_account_index >= 0 then !cli_account_index else account_index in
|
||||||
|
if !cli_account_index >= 0 then begin
|
||||||
|
match get_credentials_from_file ~account_index:effective_index () with
|
||||||
|
| Some (pk, sk) -> (pk, sk)
|
||||||
|
| None ->
|
||||||
|
Printf.fprintf stderr "Error: No credentials found for account index %d in accounts.csv\n" !cli_account_index;
|
||||||
|
exit 1
|
||||||
|
end else begin
|
||||||
|
(* Priority 3: Environment variables *)
|
||||||
let env_pk = try Some (Sys.getenv "UNSANDBOX_PUBLIC_KEY") with Not_found -> None in
|
let env_pk = try Some (Sys.getenv "UNSANDBOX_PUBLIC_KEY") with Not_found -> None in
|
||||||
let env_sk = try Some (Sys.getenv "UNSANDBOX_SECRET_KEY") with Not_found -> None in
|
let env_sk = try Some (Sys.getenv "UNSANDBOX_SECRET_KEY") with Not_found -> None in
|
||||||
match (env_pk, env_sk) with
|
match (env_pk, env_sk) with
|
||||||
| (Some pk, Some sk) -> (pk, sk)
|
| (Some pk, Some sk) -> (pk, sk)
|
||||||
| _ ->
|
| _ ->
|
||||||
(* Priority 3: Config file *)
|
(* Priority 4: ~/.unsandbox/accounts.csv (or UNSANDBOX_ACCOUNT index) *)
|
||||||
match get_credentials_from_file ~account_index () with
|
let default_index =
|
||||||
|
try int_of_string (String.trim (Sys.getenv "UNSANDBOX_ACCOUNT"))
|
||||||
|
with Not_found | Failure _ -> 0
|
||||||
|
in
|
||||||
|
match get_credentials_from_file ~account_index:default_index () with
|
||||||
| Some (pk, sk) -> (pk, sk)
|
| Some (pk, sk) -> (pk, sk)
|
||||||
| None ->
|
| None ->
|
||||||
failwith "No credentials found. Set UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY, \
|
failwith "No credentials found. Set UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY, \
|
||||||
or create ~/.unsandbox/accounts.csv, or pass credentials to function."
|
or create ~/.unsandbox/accounts.csv, or pass credentials to function."
|
||||||
|
end
|
||||||
|
|
||||||
(* Legacy function for backward compatibility *)
|
(* Legacy function for backward compatibility *)
|
||||||
let get_api_keys () =
|
let get_api_keys () =
|
||||||
|
|
@ -2038,18 +2066,34 @@ let image_command args =
|
||||||
in
|
in
|
||||||
parse_args false "" "" "" "" "" "" "" "" "" "" "" "" args
|
parse_args false "" "" "" "" "" "" "" "" "" "" "" "" args
|
||||||
|
|
||||||
|
let strip_account_arg args =
|
||||||
|
let rec aux = function
|
||||||
|
| [] -> []
|
||||||
|
| "--account" :: n_str :: rest ->
|
||||||
|
(try cli_account_index := int_of_string (String.trim n_str)
|
||||||
|
with Failure _ ->
|
||||||
|
Printf.fprintf stderr "Error: --account requires an integer argument\n";
|
||||||
|
exit 1);
|
||||||
|
aux rest
|
||||||
|
| arg :: rest -> arg :: aux rest
|
||||||
|
in
|
||||||
|
aux args
|
||||||
|
|
||||||
let () =
|
let () =
|
||||||
Random.self_init ();
|
Random.self_init ();
|
||||||
let args = Array.to_list Sys.argv in
|
let raw_args = Array.to_list Sys.argv in
|
||||||
match List.tl args with
|
let args = strip_account_arg (List.tl raw_args) in
|
||||||
|
match args with
|
||||||
| [] ->
|
| [] ->
|
||||||
Printf.printf "Usage: un.ml [options] <source_file>\n";
|
Printf.printf "Usage: un.ml [--account N] [options] <source_file>\n";
|
||||||
Printf.printf " un.ml session [options]\n";
|
Printf.printf " un.ml [--account N] session [options]\n";
|
||||||
Printf.printf " un.ml service [options]\n";
|
Printf.printf " un.ml [--account N] service [options]\n";
|
||||||
Printf.printf " un.ml image [options]\n";
|
Printf.printf " un.ml [--account N] image [options]\n";
|
||||||
Printf.printf " un.ml service env <action> <service_id>\n";
|
Printf.printf " un.ml [--account N] service env <action> <service_id>\n";
|
||||||
Printf.printf " un.ml languages [--json]\n";
|
Printf.printf " un.ml languages [--json]\n";
|
||||||
Printf.printf " un.ml key [--extend]\n\n";
|
Printf.printf " un.ml key [--extend]\n\n";
|
||||||
|
Printf.printf "Global options:\n";
|
||||||
|
Printf.printf " --account N Use accounts.csv row N (bypasses env vars)\n\n";
|
||||||
Printf.printf "Service options: --name, --ports, --bootstrap, --bootstrap-file, -e KEY=VALUE, --env-file FILE\n";
|
Printf.printf "Service options: --name, --ports, --bootstrap, --bootstrap-file, -e KEY=VALUE, --env-file FILE\n";
|
||||||
Printf.printf "Service env commands: status, set, export, delete\n";
|
Printf.printf "Service env commands: status, set, export, delete\n";
|
||||||
Printf.printf "Image options: --list, --info ID, --delete ID, --lock ID, --unlock ID,\n";
|
Printf.printf "Image options: --list, --info ID, --delete ID, --lock ID, --unlock ID,\n";
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue