Skip to content
Merged
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
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ local YACD environment from a checked-in config file.
- Local-mode `CardanoNetwork` reconciliation for one primary node with Ogmios
as the default chain API, Kupo as the default chain index API, and an opt-in
token-protected faucet for local top-ups.
- Developer CLI under `cli/` with `up`, `down`, `list`, `info`, and `topup`
lifecycle commands, plus `run`, `connect`, and `exec` host-access verbs that
- Developer CLI under `cli/` with `init` (print a commented `yacd.yaml`
template), `up`, `down`, `list`, `info`, and `topup` lifecycle commands, plus
`run`, `connect`, and `exec` host-access verbs that
bridge the network's chain APIs to your tests through the `YACD_*` environment
contract (see [docs/host-access.md](docs/host-access.md)).
- Helm chart packaging for the manager deployment.
Expand Down Expand Up @@ -53,9 +54,11 @@ moon run root:test
git diff --check
```

Render the example local environment without changing the cluster:
Scaffold a commented `yacd.yaml` to start from, or render the example local
environment without changing the cluster:

```sh
go run ./cli/cmd/yacd init > yacd.yaml
go run ./cli/cmd/yacd up phase4-smoke -f examples/local/yacd.yaml --dry-run
```

Expand All @@ -80,9 +83,9 @@ go run ./cli/cmd/yacd run phase4-smoke -- go test ./e2e/...
# Or hold the forwards open in one terminal and work in another:
go run ./cli/cmd/yacd connect phase4-smoke

# Fund a checked-in address and wait for on-chain confirmation:
go run ./cli/cmd/yacd run phase4-smoke -- sh -c \
'yacd topup phase4-smoke --address addr_test... --lovelace 1000000 --faucet-url "$YACD_FAUCET_URL" --await'
# Fund an address and wait for on-chain confirmation. topup forwards the faucet
# (and Kupo, for --await) itself, so it needs no `yacd run` wrapper:
go run ./cli/cmd/yacd topup phase4-smoke 1000000 --address addr_test... --await

# cardano-cli reaches the node over its local socket, so use exec (in-pod):
go run ./cli/cmd/yacd exec phase4-smoke -- cardano-cli query tip --testnet-magic 42
Expand Down
8 changes: 8 additions & 0 deletions cli/internal/cli/embed.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,11 @@ import _ "embed"
//
//go:embed devnet.yaml
var defaultDevnetEnvYAML []byte

// defaultInitEnvYAML is the fully-commented developer environment template
// `yacd init` prints to stdout. Its active (uncommented) portion is a valid
// batteries-included local network; commented blocks document the rest of the
// API. init_test.go guards the active config against drift from the real schema.
//
//go:embed init.yaml
var defaultInitEnvYAML []byte
41 changes: 28 additions & 13 deletions cli/internal/cli/forward.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,7 @@ func connectNetwork(ctx context.Context, kubeClient kube.Client, namespace strin
return nil, err
}

specs := forwardSpecs(network)
if len(specs) == 0 {
return nil, fmt.Errorf("cardanonetwork %s/%s publishes no chain-API endpoints to forward", namespace, name)
}

podName, err := kubeClient.PrimaryPodName(ctx, namespace, name)
if err != nil {
return nil, err
}

session, err := kubeClient.Forward(ctx, namespace, podName, specs)
session, endpoints, err := forwardEndpoints(ctx, kubeClient, network, namespace, name)
if err != nil {
return nil, err
}
Expand All @@ -71,13 +61,38 @@ func connectNetwork(ctx context.Context, kubeClient kube.Client, namespace strin
_ = session.Close()
return nil, err
}

return &connectedSession{session: session, env: env, endpoints: endpoints}, nil
}

// forwardEndpoints forwards a ready network's published chain-API endpoints and
// returns the live session plus the token-free loopback endpoints document. It
// reads no Secret, so callers (notably topup) can run their trust gate before
// fetching any token. The caller owns the returned session and must Close it;
// forwardEndpoints closes it itself only when a later step here fails.
func forwardEndpoints(ctx context.Context, kubeClient kube.Client, network *yacdv1alpha1.CardanoNetwork, namespace string, name string) (kube.ForwardSession, endpointsDocument, error) {
specs := forwardSpecs(network)
if len(specs) == 0 {
return nil, endpointsDocument{}, fmt.Errorf("cardanonetwork %s/%s publishes no chain-API endpoints to forward", namespace, name)
}

podName, err := kubeClient.PrimaryPodName(ctx, namespace, name)
if err != nil {
return nil, endpointsDocument{}, err
}

session, err := kubeClient.Forward(ctx, namespace, podName, specs)
if err != nil {
return nil, endpointsDocument{}, err
}

endpoints, err := newEndpointsDocument(network, session.LocalPort)
if err != nil {
_ = session.Close()
return nil, err
return nil, endpointsDocument{}, err
}

return &connectedSession{session: session, env: env, endpoints: endpoints}, nil
return session, endpoints, nil
}

// forwardSpecs returns the port-forward specs for a network's published
Expand Down
34 changes: 34 additions & 0 deletions cli/internal/cli/init.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package cli

import (
"fmt"

"github.com/spf13/cobra"
)

// newInitCommand wires the `yacd init` subcommand. It prints a fully-commented
// developer Environment template to stdout; the active portion is a ready-to-run
// local network, and commented blocks document the rest of the API. Output goes
// to stdout so it composes with a redirect: `yacd init > yacd.yaml`.
func newInitCommand(commandContext *commandContext) *cobra.Command {
return &cobra.Command{
Use: "init",
Short: "Print a commented yacd.yaml environment template",
Long: `Print a fully-commented developer environment template to stdout.

The active configuration is a ready-to-run local devnet (faucet + funded
wallet); commented blocks document the rest of the API. Redirect it to a file
and apply it:

yacd init > yacd.yaml
yacd up dev -f yacd.yaml`,
Args: cobra.NoArgs,
RunE: func(_ *cobra.Command, _ []string) error {
if _, err := commandContext.out.Write(defaultInitEnvYAML); err != nil {
return fmt.Errorf("write environment template: %w", err)
}

return nil
},
}
}
82 changes: 82 additions & 0 deletions cli/internal/cli/init.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# yacd environment — generated by `yacd init`.
#
# An "Environment" describes one Cardano network. Apply it with:
# yacd up NAME -f yacd.yaml # NAME is also the namespace by default
# (or let `yacd devnet` manage a local cluster + a default network for you).
#
# The active config below is a ready-to-run LOCAL devnet with a faucet and a
# pre-funded wallet. Commented blocks show the rest of the API — uncomment a
# WHOLE block at a time (every field shown in a block is required together).
# More: https://github.com/meigma/yacd and docs/host-access.md.

apiVersion: yacd.meigma.io/devconfig/v1alpha1
kind: Environment
spec:
network:
# local generates a private devnet; public joins preview/preprod/mainnet.
mode: local

# The primary cardano-node, shared by both modes.
node:
version: "11.0.1" # cardano-node release (drives the node image).
port: 3001 # node-to-node TCP port (1-65535).
storage:
size: 2Gi # node DB volume (default 10Gi; raise for public networks).
# storageClassName: standard # pin a Kubernetes StorageClass (default: cluster default).
# image: ghcr.io/my/cardano-node:tag # override the full node image (else derived from version).
# resources: # container requests/limits (omit to use cluster defaults).
# requests: {cpu: "1", memory: 2Gi}
# limits: {cpu: "2", memory: 4Gi}

# Network-facing chain APIs deployed next to the node.
chainAPI:
# Ogmios (WebSocket bridge) and Kupo (chain indexer) are ENABLED by default.
# Uncomment to pin image/port (keep all three fields together). Kupo needs Ogmios.
# ogmios:
# enabled: true
# image: cardanosolutions/ogmios:v6.14.0
# port: 1337
# kupo:
# enabled: true
# image: cardanosolutions/kupo:v2.11.0
# port: 1442

# Faucet: funds addresses on the local network (local mode only; needs Ogmios + Kupo).
faucet:
enabled: true
port: 8080
defaultSource: utxo1 # generated UTxO source used when a request omits one.
minTopUpLovelace: 1000000 # 1 ADA = 1_000_000 lovelace.
maxTopUpLovelace: 100000000000 # must be >= wallet.fundingLovelace below.
# image: ghcr.io/my/faucet:tag # override the faucet image (else controller default).

# Pre-funded developer wallet (local mode only; needs faucet + Kupo).
# The controller generates a key once and funds it through the faucet.
wallet:
enabled: true
fundingLovelace: 100000000000 # 100,000 ADA.

# ---- LOCAL mode (required when mode: local; remove when mode: public) ----
local:
networkMagic: 42 # testnet magic for the node and client commands.
era: conway # newest ledger era (conway only; babbage is rejected).
timing:
slotLength: 100ms # fast slots for quick local blocks.
epochLength: 500 # slots per epoch.
topology:
pools:
count: 1 # generated stake pools (1 only, currently).
# defaults: {...} # reserved: shared pool economics — not yet supported by the CLI.
# genesis: {...} # reserved: custom genesis preset/params — not yet supported by the CLI.

# ---- PUBLIC mode (alternative to LOCAL) ----------------------------------
# To join a public network: set `mode: public`, delete the `local:` block
# above, and uncomment this. Public mode rejects explicit kupo/faucet
# `enabled: true` and the wallet. Mainnet also needs `bootstrap.mithril` and
# `node.storage.size` >= 300Gi.
# public:
# profile: preview # one of: preview | preprod | mainnet.
# bootstrap: # required for mainnet only.
# mithril:
# image: ghcr.io/input-output-hk/mithril-client:main-2478748
# snapshot: latest # snapshot digest, or "latest".
49 changes: 49 additions & 0 deletions cli/internal/cli/init_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package cli

import (
"bytes"
"context"
"testing"

yacdv1alpha1 "github.com/meigma/yacd/api/v1alpha1"
"github.com/meigma/yacd/cli/internal/devconfig"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// TestInitTemplateLoadsAndValidates guards the embedded init template against
// drift from the real schema: its active (uncommented) portion must parse and
// validate through the same devconfig.Load `yacd up` uses, and must be the
// batteries-included local network `init` promises (faucet + funded wallet).
func TestInitTemplateLoadsAndValidates(t *testing.T) {
t.Parallel()

env, err := devconfig.Load(bytes.NewReader(defaultInitEnvYAML))
require.NoError(t, err)

assert.Equal(t, yacdv1alpha1.CardanoNetworkModeLocal, env.Spec.Network.Mode)
require.NotNil(t, env.Spec.Network.Local)
require.NotNil(t, env.Spec.Network.ChainAPI)
require.NotNil(t, env.Spec.Network.ChainAPI.Faucet)
assert.True(t, env.Spec.Network.ChainAPI.Faucet.Enabled)
require.NotNil(t, env.Spec.Network.ChainAPI.Wallet)
assert.True(t, env.Spec.Network.ChainAPI.Wallet.Enabled)
}

// TestInitCommandPrintsTemplate proves `yacd init` writes the embedded template
// verbatim to stdout (the redirect target for `yacd init > yacd.yaml`).
func TestInitCommandPrintsTemplate(t *testing.T) {
t.Parallel()

var stdout bytes.Buffer
root := NewRootCommand(Options{
Out: &stdout,
Viper: viper.New(),
})
root.SetArgs([]string{"init"})

require.NoError(t, root.ExecuteContext(context.Background()))
require.NotEmpty(t, stdout.Bytes())
assert.Equal(t, defaultInitEnvYAML, stdout.Bytes())
}
28 changes: 11 additions & 17 deletions cli/internal/cli/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,34 +14,28 @@ import (
)

// newListCommand wires the `yacd list` subcommand. It lists CardanoNetworks
// in the active namespace (or across all namespaces with -A) and projects
// each into name/namespace/mode/ready/endpoints, rendered as a table or, with
// --json, as machine-readable JSON.
// across all namespaces by default, or a single namespace when one is given
// with -n, and projects each into name/namespace/mode/ready/endpoints,
// rendered as a table or, with --json, as machine-readable JSON.
func newListCommand(commandContext *commandContext) *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "List YACD environments in the cluster",
Short: "List YACD environments across all namespaces (or one with -n)",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
runtimeConfig, err := loadRuntimeConfig(commandContext.viper)
if err != nil {
return err
}
allNamespaces := commandContext.viper.GetBool("all-namespaces")
jsonOutput := commandContext.viper.GetBool("json")

kubeClient, _, err := commandContext.resolveKubeClient(runtimeConfig)
if err != nil {
return err
}

namespace := ""
if !allNamespaces {
namespace = strings.TrimSpace(runtimeConfig.Namespace)
if namespace == "" {
namespace = kubeClient.DefaultNamespace()
}
}
// An empty namespace lists across all namespaces; -n scopes to one.
namespace := strings.TrimSpace(runtimeConfig.Namespace)

networks, err := kubeClient.ListCardanoNetworks(cmd.Context(), namespace)
if err != nil {
Expand All @@ -64,11 +58,10 @@ func newListCommand(commandContext *commandContext) *cobra.Command {
return nil
}

return printList(commandContext.out, items, namespace, allNamespaces)
return printList(commandContext.out, items, namespace)
},
}

cmd.Flags().BoolP("all-namespaces", "A", false, "List CardanoNetworks across all namespaces")
cmd.Flags().Bool("json", false, "Print machine-readable JSON")

return cmd
Expand Down Expand Up @@ -167,11 +160,12 @@ func endpointURL(endpoint *yacdv1alpha1.ServiceEndpointStatus) string {

// printList renders the projected items as an aligned table. An empty result
// is reported explicitly, with the search scope, so the user can tell "none"
// from a filtering error.
func printList(out io.Writer, items []listItem, namespace string, allNamespaces bool) error {
// from a filtering error. A non-empty namespace means the result was scoped to
// that namespace; an empty namespace means all namespaces were searched.
func printList(out io.Writer, items []listItem, namespace string) error {
if len(items) == 0 {
message := "No CardanoNetworks found."
if !allNamespaces {
if namespace != "" {
message = fmt.Sprintf("No CardanoNetworks found in namespace %q.", namespace)
}
if _, err := fmt.Fprintln(out, message); err != nil {
Expand Down
Loading