## TiSonnet

A Jsonnet library that helps you create resources. 

## TiSonnet Architecture

TiSonnet is a Jsonnet library that generates Kubernetes resources for TiDB clusters. It provides a layered architecture with clear separation of concerns.

### Core Layers

**Action System (`configures.libsonnet`)**  
The foundation. Defines a simple action-based transformation system that operates on plain objects. Actions describe *what* to change (merge, set, freeze) and *where* (path). This replaces complex lens patterns with straightforward, debuggable operations.

**Component Templates (`components.libsonnet`)**  
Mid-level builders. Each function (e.g., `pd()`, `tikv()`, `tidb()`) constructs a Kubernetes resource with defaults (image, version, replicas). They operate on `{ spec: ..., metadata: ... }` structures, apply user configurations via actions, and freeze TOML configs before output.

**User-Facing DSL (`cluster-configures.libsonnet`)**  
High-level presets. Functions like `basicCluster()`, `nextGen.use()`, or `resources.l` return configuration arrays that modify components. Users compose these to describe clusters without knowing Kubernetes internals.

**Resource Builder (`maker.libsonnet`)**  
Orchestrator. Takes a cluster name and configuration list, merges overlapping configs (by `kind` and `name`), invokes the right constructors, and outputs final Kubernetes YAML.

### Data Flow

```
User Config → Maker → Component Constructors → Apply Actions → Freeze Config → K8s Resources
```

**Step 1: User writes configuration array**
```jsonnet
local cc = import 'cluster-configures.libsonnet'; [
  cc.cluster(),                              // → { kind: 'cluster' }
  cc.pd(replicas=3),                         // → { kind: 'pd', conf: [...] }
  cc.tikv(replicas=5),                       // → { kind: 'tikv', conf: [...] }
  cc.nextGen.tidbGroup('app1', name='biz'),  // → { kind: 'tidb', name: 'biz', conf: [...] }
  cc.nextGen.tidbGroup('app2', name='ops'),  // → { kind: 'tidb', name: 'ops', conf: [...] }
  { kind: 'tidb', conf: [specs.replicas(2)] },  // Broadcast: applies to all tidb instances
  { kind: 'tidb', name: 'biz', conf: [/* special config */] },  // Targeted: only to 'biz'
  cc.resources.l,                            // → [{ kind: 'pd', conf: [...] }, { kind: 'tikv', conf: [...] }]
]
```
These functions return plain objects/arrays describing *what* to create and *how* to configure it, not the final resources.

**Step 2: Maker merges by `kind` and `name`**
```jsonnet
[
  { kind: 'cluster' },
  { kind: 'pd', conf: [/* replicas action */, /* resource action */] },
  { kind: 'tikv', conf: [/* replicas action */, /* resource action */] },
  { kind: 'tidb', name: 'biz', conf: [/* keyspace action */, /* replicas(2) action */, /* special config */] },
  { kind: 'tidb', name: 'ops', conf: [/* keyspace action */, /* replicas(2) action */] },
]
```
Items with the same `kind` (and `name`, if specified) are merged together. The `name` field is optional in Step 1; if omitted, it defaults to the cluster name. This allows `pd()` and `resources.l` (which both target `kind: 'pd'`) to merge their `conf` arrays. When `name` is explicitly set (like `tidbGroup`), it creates a separate component instance of the same kind. Configurations with only `kind` (no `name`) broadcast to all instances of that kind—note how `replicas(2)` appears in both 'biz' and 'ops' TiDB groups. Configurations with both `kind` and `name` target exactly one instance—the special config only merges into 'biz', not 'ops'.

**The `create` Field**

Each configuration item can have a `create` field that controls whether the resource is actually generated:

- **`create: true`**: The resource will be created. This is required for any component that should be instantiated.
- **`create: false`** (or omitted): The resource is NOT created—only the configuration is merged. This allows pure configuration items to modify existing components without accidentally creating incomplete resources.

**Important**: Since `create` defaults to `false`, you must explicitly set `create: true` when you want to instantiate a new component. If you accidentally set `create: false` on a component that should be created, TiSonnet will emit a warning:

```
WARNING: Skipping resource creation for kind=tikv name=my-cluster (create=false). This is usually unintended.
```

This behavior prevents configuration-only items (like `resources.l` or `componentConfig(...)`) from accidentally creating malformed resources. For example:

```jsonnet
[
  cc.tikv(replicas=3),              // create: true → TiKV resource created
  cc.resources.l,                   // create: false (default) → only modifies existing TiKV, doesn't create new one
  { kind: 'tikv', conf: [...] },    // create: false (default) → broadcasts config to all TiKV instances
]
```

**Step 3: Constructors build base template**
```jsonnet
{
  spec: {
    cluster: { name: 'my-cluster' },
    replicas: 1,
    template: { spec: { image: '...', version: '...', config: {} } }
  },
  metadata: { name: 'my-cluster' }
}
```

**Step 4: Actions transform the structure**
```jsonnet
// After applying replicas(3) and resource actions:
{
  spec: {
    replicas: 3,  // modified by action
    template: { 
      spec: { 
        overlay: { 
          pod: { spec: { containers: [{ resources: {...} }] } }  // added by action
        }
      }
    }
  }
}
```

**Step 5: Final Kubernetes resource**
```yaml
apiVersion: core.pingcap.com/v1alpha1
kind: PDGroup
metadata:
  name: my-cluster
spec:
  replicas: 3
  template:
    spec:
      config: |
        [log]
        level = "info"
      overlay:
        pod:
          spec:
            containers:
            - name: pd
              resources:
                requests:
                  cpu: "4"
                  memory: 8Gi
```

### Extension Examples

**Add a custom action (configures.libsonnet):**
```jsonnet
// Add custom annotations to pod overlay
local customAction = onFieldChain(
  ['spec', 'template', 'spec', 'overlay'],
  [action.merge({ pod+: { metadata+: { annotations+: { 'my-key': 'my-value' } } } })]
);
```

**Create a new component preset (cluster-configures.libsonnet):**
```jsonnet
// Add a custom TiFlash configuration
tiflash(replicas=2, size='200Gi'):: {
  kind: 'tiflash',
  conf+: [
    specs.replicas(replicas),
    onFieldChain(['spec', 'template', 'spec'], [specs.dataVolume(size)]),
    onFieldChain(['spec', 'template', 'spec', 'config'], [
      confs.inherit({ 'flash.proxy.config': { 'log-level': 'info' } })
    ]),
  ],
}
```

**Extend component templates (components.libsonnet):**
```jsonnet
// Add support for a new component type
pumps(name='pumps', configurations=[]):: makeComponent(name, 'pumps', configurations),
```

**Add to maker's component map (maker.libsonnet):**
```jsonnet
kindToConstructor+:: {
  pumps: components.pumps,
}
```

See `../playground/` for complete examples or run `tiop.py cluster template`.

### Using `TiSonnet` Without `tiop.py`

You can convert your configuration object to Kubernetes resources like this:

```jsonnet
# standalone-starter.jsonnet
local userConfig = import 'your/config.jsonnet';
local clusterName = std.extVar('clusterName');
local maker = import 'maker.libsonnet';

local res = maker.makeByClusterConfigurations(clusterName, userConfig);

res
```

Then evaluate the file with:

```bash
jsonnet standalone-starter.jsonnet -y --ext-str 'clusterName=<your-cluster-name>' -J/path/to/tisonnet > my-cluster.yml
```

Then you can apply them to Kubernetes directly:

```bash
kubectl apply -f my-cluster.yml
```

To destroy it:

```bash
kubectl delete -f my-cluster.yml
```
