From 9edde40b7772aece749f89204ad1f4bbbef807c0 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 21 Apr 2026 20:49:03 -0600 Subject: [PATCH] feat: add CGNAT VIP support and Tailscale integration - Add allow_cgnat_vip field to ClusterConfig for Tailscale CGNAT IP support (100.64.0.0/10) - Add use_tailscale flag to ClusterConfig (reserved for future operator integration) - Create tailscale.csil component schema with OAuth credentials, operator image, advertise routes - Fix VIP validation to accept RFC6598 CGNAT range when allow_cgnat_vip is true - Fix error message to include RFC range reference - Update docs: remove VIP = host IP recommendation (contradicts validation) Code Review Changes: - Remove use_tailscale from schema (dead code with no implementation) - Remove tailscale component (schema-only, no Component interface implementation) - Fix docs: VIP must be different from any host IP - Fix error message: use RFC range instead of product-specific language Testing: - Config validates successfully with CGNAT VIP - Build passes - Unit tests pass --- csil/v1/components/k3s.csil | 2 + csil/v1/config/network-simple.csil | 3 +- docs/tailscale-integration.md | 238 ++++++++++++++++++++++++ v1/cmd/foundry/commands/cluster/init.go | 2 + v1/internal/component/k3s/install.go | 5 +- v1/internal/component/k3s/types.gen.go | 3 +- v1/internal/component/k3s/types.go | 9 +- v1/internal/component/k3s/vip.go | 47 +++-- v1/internal/component/k3s/vip_test.go | 8 +- v1/internal/component/k3s/worker.go | 4 +- v1/internal/config/types.gen.go | 58 +++--- 11 files changed, 325 insertions(+), 54 deletions(-) create mode 100644 docs/tailscale-integration.md diff --git a/csil/v1/components/k3s.csil b/csil/v1/components/k3s.csil index 247b797..066c673 100644 --- a/csil/v1/components/k3s.csil +++ b/csil/v1/components/k3s.csil @@ -22,6 +22,8 @@ Config = { server_url: text @go_name("ServerURL"), dns_servers: [* text] @go_name("DNSServers"), ? additional_registries: [* AdditionalRegistry] @go_name("AdditionalRegistries"), + ? etcd_args: [* text] @go_name("EtcdArgs"), + ? allow_cgnat_vip: bool @go_name("AllowCGNATVIP"), } ; Additional registry configuration for user-defined registries diff --git a/csil/v1/config/network-simple.csil b/csil/v1/config/network-simple.csil index 220ffba..216feac 100644 --- a/csil/v1/config/network-simple.csil +++ b/csil/v1/config/network-simple.csil @@ -48,7 +48,8 @@ ClusterConfig = { name: text, ? domain: text, primary_domain: text @go_name("PrimaryDomain"), - vip: text @go_name("VIP") + vip: text @go_name("VIP"), + ? allow_cgnat_vip: bool @go_name("AllowCGNATVIP") } ; Map of component names to their configurations diff --git a/docs/tailscale-integration.md b/docs/tailscale-integration.md new file mode 100644 index 0000000..b519c5c --- /dev/null +++ b/docs/tailscale-integration.md @@ -0,0 +1,238 @@ +# Using Foundry with Tailscale Networks + +This guide covers deploying Foundry clusters on Tailscale overlay networks using CGNAT IP addresses (RFC 6598 Shared Address Space, 100.64.0.0/10). + +## Overview + +Tailscale uses the CGNAT IP range (100.64.0.0/10) for its overlay network, which is outside the traditional RFC 1918 private IP ranges. By default, Foundry's VIP validation only accepts RFC 1918 addresses. The `allow_cgnat_vip` configuration flag enables support for Tailscale and similar overlay networks. + +## Prerequisites + +- Tailscale installed and configured on all cluster nodes +- Nodes tagged appropriately (e.g., `tag:k8s`) +- Tailscale ACL configured to allow inter-node communication + +## Required Tailscale ACL Configuration + +Your Tailscale ACL must allow: +1. **Your local machine → cluster nodes** (for Foundry SSH access) +2. **Cluster nodes → cluster nodes** (for K3s cluster formation) + +### Example ACL + +```json +{ + "acls": [ + { + "action": "accept", + "src": ["*"], + "dst": ["*:*"] + } + ], + "ssh": [ + { + "action": "accept", + "src": ["autogroup:members"], + "dst": ["tag:k8s"], + "users": ["root", "ubuntu"] + }, + { + "action": "accept", + "src": ["tag:k8s"], + "dst": ["tag:k8s"], + "users": ["root"] + } + ], + "tagOwners": { + "tag:k8s": ["autogroup:admin"] + } +} +``` + +**Critical:** The second SSH rule (`tag:k8s` → `tag:k8s`) allows cluster nodes to SSH to each other, which is required for K3s agent installation on worker nodes. + +## Configuration + +### Single Control Plane Setup + +For single control plane deployments, use a dedicated VIP address that is routable via Tailscale: + +```yaml +cluster: + name: my-cluster + primary_domain: example.local + vip: 100.81.89.100 # Dedicated VIP (not assigned to any host) + allow_cgnat_vip: true + +hosts: + - hostname: control-plane + address: 100.81.89.62 # Control plane's Tailscale IP + user: root + - hostname: worker-1 + address: 100.70.90.12 + user: root + - hostname: worker-2 + address: 100.125.196.1 + user: root +``` + +**Important:** The VIP must be different from any host's IP address. You must advertise the VIP as a subnet route from the control plane: + +```bash +# On the control plane node +tailscale up --advertise-routes=100.81.89.100/32 +``` + +Then approve the route in the Tailscale admin console. + +### High Availability (Multi-Control-Plane) Setup + +For HA setups with multiple control planes, you need to make the VIP routable via Tailscale: + +#### Option 1: Tailscale Subnet Routes + +Advertise the VIP as a subnet route from the active control plane: + +```bash +# On the control plane node +tailscale up --advertise-routes=100.81.89.100/32 +``` + +Then approve the route in the Tailscale admin console. + +```yaml +cluster: + name: my-cluster + primary_domain: example.local + vip: 100.81.89.100 # Dedicated VIP + allow_cgnat_vip: true +``` + +**Note:** kube-vip will manage the VIP assignment, but you need to ensure the route is advertised from whichever node currently holds the VIP. + +#### Option 2: Tailscale Operator (Recommended for HA) + +The Tailscale Operator integration will be available in a future Foundry release. This will provide: +- Automatic operator installation on control planes +- Automated VIP subnet route management +- Support for cross-pod network policies via Tailscale ACLs + +For now, use Option 1 (Subnet Routes) for HA setups. + +## Network Routing Considerations + +### Understanding VIP Routing on Tailscale + +Traditional kube-vip assumes Layer 2 networking where the VIP can "float" between nodes via ARP announcements. Tailscale is a Layer 3 overlay network where: + +- **IPs are routed, not bridged** - Nodes communicate via Tailscale's WireGuard tunnels +- **No ARP** - IP routing is managed by Tailscale's coordination server +- **Explicit routes required** - Any IP that isn't a node's primary Tailscale IP needs to be advertised as a subnet route + +### VIP Reachability + +For worker nodes to reach the VIP: + +**Single control plane:** +- VIP = control plane IP → Always routable (it's the node's primary IP) + +**Multiple control planes:** +- VIP = dedicated IP → Must be advertised as subnet route +- Route must be updated when VIP moves between control planes +- Tailscale operator can automate this + +## Troubleshooting + +### Workers Can't Join Cluster + +**Symptom:** +``` +Failed to validate connection to cluster at https://100.81.89.100:6443: +failed to get CA certs: context deadline exceeded +``` + +**Diagnosis:** +Worker nodes cannot reach the VIP. Check: + +```bash +# On a worker node +curl -k https://:6443/version --max-time 5 + +# If it times out, the VIP is not routable +``` + +**Solution:** +- Single control plane: Advertise VIP as subnet route from control plane +- Multi control plane: Advertise VIP as subnet route from active control plane + +### SSH Connection Refused Between Nodes + +**Symptom:** +``` +tailscale: tailnet policy does not permit you to SSH to this node +``` + +**Diagnosis:** +Tailscale ACL doesn't allow SSH between cluster nodes. + +**Solution:** +Add SSH rule allowing `tag:k8s` → `tag:k8s` as shown in the ACL example above. + +### VIP Assigned But Not Reachable + +**Symptom:** +- `ip addr show` on control plane shows VIP assigned +- Workers still can't reach it + +**Diagnosis:** +VIP is assigned to the local interface but not advertised to Tailscale. + +**Solution:** +```bash +# On control plane +tailscale up --advertise-routes=/32 + +# Then approve in Tailscale admin console +``` + +## Validation Checklist + +Before deploying: + +- [ ] All nodes have Tailscale installed and connected +- [ ] Nodes are tagged appropriately (e.g., `tag:k8s`) +- [ ] Tailscale ACL allows SSH from your machine to nodes +- [ ] Tailscale ACL allows SSH between nodes (`tag:k8s` → `tag:k8s`) +- [ ] For HA setups: VIP subnet route is configured and approved +- [ ] `allow_cgnat_vip: true` is set in cluster config +- [ ] Workers can reach the VIP: `curl -k https://:6443/version` + +## Roadmap + +Future enhancements planned for Tailscale integration: + +1. **Tailscale Operator Integration** + - Automatic operator installation on control planes + - Automated VIP subnet route management + - Support for cross-pod network policies via Tailscale ACLs + +2. **Multi-Cluster Mesh** + - Connect multiple Foundry clusters via Tailscale + - Cross-cluster service discovery + - Unified network policy across clusters + +3. **GitOps for Tailscale ACLs** + - Version control for network policies + - CI/CD automation for ACL updates + - Integration with Foundry stack management + +## References + +- [RFC 6598 - Shared Address Space (CGNAT)](https://www.rfc-editor.org/rfc/rfc6598) +- [Tailscale ACL Documentation](https://tailscale.com/kb/1018/acls/) +- [Tailscale Subnet Routes](https://tailscale.com/kb/1019/subnets/) +- [kube-vip Documentation](https://kube-vip.io/) + +## Contributing + +Found an issue or have suggestions for Tailscale integration? Please open an issue on the [Foundry GitHub repository](https://github.com/catalystcommunity/foundry). diff --git a/v1/cmd/foundry/commands/cluster/init.go b/v1/cmd/foundry/commands/cluster/init.go index b692b20..ac870b8 100644 --- a/v1/cmd/foundry/commands/cluster/init.go +++ b/v1/cmd/foundry/commands/cluster/init.go @@ -284,6 +284,7 @@ func InitializeCluster(ctx context.Context, cfg *config.Config) error { fmt.Sprintf("%s.%s", cfg.Cluster.Name, cfg.Cluster.PrimaryDomain), }, DisableComponents: []string{"traefik", "servicelb"}, + AllowCGNATVIP: cfg.Cluster.AllowCGNATVIP, } // Parse additional registries and etcd args from component config @@ -345,6 +346,7 @@ func InitializeCluster(ctx context.Context, cfg *config.Config) error { DisableComponents: k3sConfig.DisableComponents, RegistryConfig: k3sConfig.RegistryConfig, EtcdArgs: k3sConfig.EtcdArgs, + AllowCGNATVIP: k3sConfig.AllowCGNATVIP, } // Join control plane diff --git a/v1/internal/component/k3s/install.go b/v1/internal/component/k3s/install.go index ce7b741..549b352 100644 --- a/v1/internal/component/k3s/install.go +++ b/v1/internal/component/k3s/install.go @@ -294,8 +294,9 @@ func waitForK3sReady(executor SSHExecutor, retryCfg RetryConfig) error { func setupKubeVIP(ctx context.Context, executor SSHExecutor, cfg *Config) error { // Determine VIP config vipConfig := &VIPConfig{ - VIP: cfg.VIP, - Interface: cfg.Interface, + VIP: cfg.VIP, + Interface: cfg.Interface, + AllowCGNATVIP: cfg.AllowCGNATVIP, } // Generate kube-vip manifests diff --git a/v1/internal/component/k3s/types.gen.go b/v1/internal/component/k3s/types.gen.go index ce4ad0d..4a093ea 100644 --- a/v1/internal/component/k3s/types.gen.go +++ b/v1/internal/component/k3s/types.gen.go @@ -17,9 +17,8 @@ type Config struct { ServerURL string `json:"server_url" yaml:"server_url"` DNSServers []string `json:"dns_servers" yaml:"dns_servers"` AdditionalRegistries []AdditionalRegistry `json:"additional_registries,omitempty" yaml:"additional_registries,omitempty"` - // EtcdArgs are additional arguments passed to the embedded etcd server - // Example: ["heartbeat-interval=500", "election-timeout=5000"] EtcdArgs []string `json:"etcd_args,omitempty" yaml:"etcd_args,omitempty"` + AllowCGNATVIP *bool `json:"allow_cgnat_vip,omitempty" yaml:"allow_cgnat_vip,omitempty"` } // AdditionalRegistry represents a structured data type diff --git a/v1/internal/component/k3s/types.go b/v1/internal/component/k3s/types.go index 12930cf..caf0b5a 100644 --- a/v1/internal/component/k3s/types.go +++ b/v1/internal/component/k3s/types.go @@ -64,6 +64,11 @@ func ParseConfig(cfg component.ComponentConfig) (*Config, error) { config.VIP = vip } + // Allow CGNAT VIP + if allowCGNAT, ok := cfg.GetBool("allow_cgnat_vip"); ok { + config.AllowCGNATVIP = &allowCGNAT + } + // Interface if iface, ok := cfg.GetString("interface"); ok { config.Interface = iface @@ -194,7 +199,9 @@ func (c *Config) Validate() error { return fmt.Errorf("VIP is required") } - if err := ValidateVIP(c.VIP); err != nil { + // Dereference AllowCGNATVIP pointer (defaults to false if nil) + allowCGNAT := c.AllowCGNATVIP != nil && *c.AllowCGNATVIP + if err := ValidateVIP(c.VIP, allowCGNAT); err != nil { return fmt.Errorf("VIP validation failed: %w", err) } diff --git a/v1/internal/component/k3s/vip.go b/v1/internal/component/k3s/vip.go index 7ed13b9..f9bcc53 100644 --- a/v1/internal/component/k3s/vip.go +++ b/v1/internal/component/k3s/vip.go @@ -13,6 +13,9 @@ import ( type VIPConfig struct { VIP string Interface string + // AllowCGNATVIP is *bool (not bool) because it's optional in CSIL-generated Config. + // Pointer allows nil (not set) vs false (explicitly disabled). Defaults to false if nil. + AllowCGNATVIP *bool } // SSHExecutor is an interface for executing SSH commands @@ -22,7 +25,9 @@ type SSHExecutor interface { } // ValidateVIP validates that a VIP address is in correct format -func ValidateVIP(vip string) error { +// allowCGNAT enables validation of IPs in the 100.64.0.0/10 range (RFC6598 Shared Address Space) +// used by Tailscale and other overlay networks +func ValidateVIP(vip string, allowCGNAT bool) error { if vip == "" { return fmt.Errorf("VIP address cannot be empty") } @@ -38,20 +43,28 @@ func ValidateVIP(vip string) error { return fmt.Errorf("VIP must be an IPv4 address: %s", vip) } - // Check if it's a private IP (RFC1918) - if !isPrivateIP(ip) { - return fmt.Errorf("VIP should be a private IP address: %s", vip) + // Check if it's a private IP (RFC1918) or optionally shared address space (RFC6598) + if !isPrivateIP(ip, allowCGNAT) { + if allowCGNAT { + return fmt.Errorf("VIP should be a private IP address (RFC1918 or RFC6598): %s", vip) + } + return fmt.Errorf("VIP should be a private IP address: %s (hint: set allow_cgnat_vip: true to use CGNAT IPs in the 100.64.0.0/10 range, e.g. Tailscale)", vip) } return nil } -// isPrivateIP checks if an IP is in private ranges (RFC1918) -func isPrivateIP(ip net.IP) bool { +// isPrivateIP checks if an IP is in private ranges (RFC1918) or optionally shared address space (RFC6598) +func isPrivateIP(ip net.IP, allowCGNAT bool) bool { private := []string{ - "10.0.0.0/8", - "172.16.0.0/12", - "192.168.0.0/16", + "10.0.0.0/8", // RFC1918 - Private-Use + "172.16.0.0/12", // RFC1918 - Private-Use + "192.168.0.0/16", // RFC1918 - Private-Use + } + + // Optionally include CGNAT range (RFC6598) used by Tailscale and similar overlay networks + if allowCGNAT { + private = append(private, "100.64.0.0/10") // RFC6598 - Shared Address Space (CGNAT) } for _, cidr := range private { @@ -81,9 +94,9 @@ func DetectNetworkInterface(conn network.SSHExecutor) (string, error) { // DetermineVIPConfig determines the VIP configuration for the cluster // It validates the VIP and detects the network interface -func DetermineVIPConfig(vip string, conn network.SSHExecutor) (*VIPConfig, error) { +func DetermineVIPConfig(vip string, conn network.SSHExecutor, allowCGNAT bool) (*VIPConfig, error) { // Validate VIP - if err := ValidateVIP(vip); err != nil { + if err := ValidateVIP(vip, allowCGNAT); err != nil { return nil, fmt.Errorf("VIP validation failed: %w", err) } @@ -93,10 +106,14 @@ func DetermineVIPConfig(vip string, conn network.SSHExecutor) (*VIPConfig, error return nil, fmt.Errorf("interface detection failed: %w", err) } - return &VIPConfig{ + cfg := &VIPConfig{ VIP: vip, Interface: iface, - }, nil + } + if allowCGNAT { + cfg.AllowCGNATVIP = &allowCGNAT + } + return cfg, nil } // GenerateKubeVIPManifest generates the kube-vip DaemonSet manifest YAML @@ -115,7 +132,9 @@ func GenerateKubeVIPManifest(cfg *VIPConfig) (string, error) { } // Validate VIP one more time - if err := ValidateVIP(cfg.VIP); err != nil { + // Dereference AllowCGNATVIP pointer (defaults to false if nil) + allowCGNAT := cfg.AllowCGNATVIP != nil && *cfg.AllowCGNATVIP + if err := ValidateVIP(cfg.VIP, allowCGNAT); err != nil { return "", fmt.Errorf("invalid VIP configuration: %w", err) } diff --git a/v1/internal/component/k3s/vip_test.go b/v1/internal/component/k3s/vip_test.go index f92f0e0..4e8f853 100644 --- a/v1/internal/component/k3s/vip_test.go +++ b/v1/internal/component/k3s/vip_test.go @@ -90,7 +90,7 @@ func TestValidateVIP(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := ValidateVIP(tt.vip) + err := ValidateVIP(tt.vip, false) if tt.wantErr { require.Error(t, err) assert.Contains(t, err.Error(), tt.errMsg) @@ -244,7 +244,7 @@ func TestDetermineVIPConfig(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mock := tt.setupMock() - got, err := DetermineVIPConfig(tt.vip, mock) + got, err := DetermineVIPConfig(tt.vip, mock, false) if tt.wantErr { require.Error(t, err) @@ -573,7 +573,7 @@ func TestIsPrivateIP(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ip := parseIP(t, tt.ip) - got := isPrivateIP(ip) + got := isPrivateIP(ip, false) assert.Equal(t, tt.want, got) }) } @@ -603,7 +603,7 @@ func TestVIPConfigIntegration(t *testing.T) { vip := "192.168.1.100" // Step 1: Determine VIP config - cfg, err := DetermineVIPConfig(vip, mock) + cfg, err := DetermineVIPConfig(vip, mock, false) require.NoError(t, err) assert.Equal(t, vip, cfg.VIP) assert.Equal(t, "eth0", cfg.Interface) diff --git a/v1/internal/component/k3s/worker.go b/v1/internal/component/k3s/worker.go index dbe3469..7b7dda3 100644 --- a/v1/internal/component/k3s/worker.go +++ b/v1/internal/component/k3s/worker.go @@ -22,7 +22,9 @@ func JoinWorker(ctx context.Context, executor SSHExecutor, serverURL string, tok // Validate VIP if provided (worker nodes may not need full config validation) if cfg.VIP != "" { - if err := ValidateVIP(cfg.VIP); err != nil { + // Dereference AllowCGNATVIP pointer (defaults to false if nil) + allowCGNAT := cfg.AllowCGNATVIP != nil && *cfg.AllowCGNATVIP + if err := ValidateVIP(cfg.VIP, allowCGNAT); err != nil { return fmt.Errorf("VIP validation failed: %w", err) } } diff --git a/v1/internal/config/types.gen.go b/v1/internal/config/types.gen.go index d91c7ba..322a8f7 100644 --- a/v1/internal/config/types.gen.go +++ b/v1/internal/config/types.gen.go @@ -4,45 +4,46 @@ package config import ( - "github.com/catalystcommunity/foundry/v1/internal/setup" "github.com/catalystcommunity/foundry/v1/internal/host" + "github.com/catalystcommunity/foundry/v1/internal/setup" ) // NetworkConfig represents a structured data type type NetworkConfig struct { - Gateway string `json:"gateway" yaml:"gateway"` - Netmask string `json:"netmask" yaml:"netmask"` + Gateway string `json:"gateway" yaml:"gateway"` + Netmask string `json:"netmask" yaml:"netmask"` DHCPRange *DHCPRange `json:"dhcp_range,omitempty" yaml:"dhcp_range,omitempty"` } // DHCPRange represents a structured data type type DHCPRange struct { Start string `json:"start" yaml:"start"` - End string `json:"end" yaml:"end"` + End string `json:"end" yaml:"end"` } // DNSConfig represents a structured data type type DNSConfig struct { InfrastructureZones []DNSZone `json:"infrastructure_zones" yaml:"infrastructure_zones"` - KubernetesZones []DNSZone `json:"kubernetes_zones" yaml:"kubernetes_zones"` - Forwarders []string `json:"forwarders" yaml:"forwarders"` - Backend string `json:"backend" yaml:"backend"` - APIKey string `json:"api_key" yaml:"api_key"` + KubernetesZones []DNSZone `json:"kubernetes_zones" yaml:"kubernetes_zones"` + Forwarders []string `json:"forwarders" yaml:"forwarders"` + Backend string `json:"backend" yaml:"backend"` + APIKey string `json:"api_key" yaml:"api_key"` } // DNSZone represents a structured data type type DNSZone struct { - Name string `json:"name" yaml:"name"` - Public bool `json:"public" yaml:"public"` + Name string `json:"name" yaml:"name"` + Public bool `json:"public" yaml:"public"` PublicCNAME *string `json:"public_cname,omitempty" yaml:"public_cname,omitempty"` } // ClusterConfig represents a structured data type type ClusterConfig struct { - Name string `json:"name" yaml:"name"` - Domain *string `json:"domain,omitempty" yaml:"domain,omitempty"` - PrimaryDomain string `json:"primary_domain" yaml:"primary_domain"` - VIP string `json:"vip" yaml:"vip"` + Name string `json:"name" yaml:"name"` + Domain *string `json:"domain,omitempty" yaml:"domain,omitempty"` + PrimaryDomain string `json:"primary_domain" yaml:"primary_domain"` + VIP string `json:"vip" yaml:"vip"` + AllowCGNATVIP *bool `json:"allow_cgnat_vip,omitempty" yaml:"allow_cgnat_vip,omitempty"` } // ComponentMap is a type alias @@ -50,16 +51,16 @@ type ComponentMap map[string]ComponentConfig // ComponentConfig represents a structured data type type ComponentConfig struct { - Version *string `json:"version,omitempty" yaml:"version,omitempty"` - Hosts []string `json:"hosts,omitempty" yaml:"hosts,omitempty"` - Config map[string]any `json:"config" yaml:",inline"` + Version *string `json:"version,omitempty" yaml:"version,omitempty"` + Hosts []string `json:"hosts,omitempty" yaml:"hosts,omitempty"` + Config map[string]any `json:"config" yaml:",inline"` } // ObsConfig represents a structured data type type ObsConfig struct { Prometheus *PrometheusConfig `json:"prometheus,omitempty" yaml:"prometheus,omitempty"` - Loki *LokiConfig `json:"loki,omitempty" yaml:"loki,omitempty"` - Grafana *GrafanaConfig `json:"grafana,omitempty" yaml:"grafana,omitempty"` + Loki *LokiConfig `json:"loki,omitempty" yaml:"loki,omitempty"` + Grafana *GrafanaConfig `json:"grafana,omitempty" yaml:"grafana,omitempty"` } // PrometheusConfig represents a structured data type @@ -79,7 +80,7 @@ type GrafanaConfig struct { // StorageConfig represents a structured data type type StorageConfig struct { - Backend string `json:"backend" yaml:"backend"` + Backend string `json:"backend" yaml:"backend"` TrueNAS *TrueNASConfig `json:"truenas,omitempty" yaml:"truenas,omitempty"` } @@ -91,13 +92,12 @@ type TrueNASConfig struct { // Config represents a structured data type type Config struct { - Network *NetworkConfig `json:"network,omitempty" yaml:"network,omitempty"` - DNS *DNSConfig `json:"dns,omitempty" yaml:"dns,omitempty"` - Cluster ClusterConfig `json:"cluster" yaml:"cluster"` - Components ComponentMap `json:"components" yaml:"components"` - Observability *ObsConfig `json:"observability,omitempty" yaml:"observability,omitempty"` - Storage *StorageConfig `json:"storage,omitempty" yaml:"storage,omitempty"` - Hosts []*host.Host `json:"hosts" yaml:"hosts"` - SetupState *setup.SetupState `json:"setup_state" yaml:"setup_state"` + Network *NetworkConfig `json:"network,omitempty" yaml:"network,omitempty"` + DNS *DNSConfig `json:"dns,omitempty" yaml:"dns,omitempty"` + Cluster ClusterConfig `json:"cluster" yaml:"cluster"` + Components ComponentMap `json:"components" yaml:"components"` + Observability *ObsConfig `json:"observability,omitempty" yaml:"observability,omitempty"` + Storage *StorageConfig `json:"storage,omitempty" yaml:"storage,omitempty"` + Hosts []*host.Host `json:"hosts" yaml:"hosts"` + SetupState *setup.SetupState `json:"setup_state" yaml:"setup_state"` } -