Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
13 changes: 8 additions & 5 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,16 +201,19 @@ func (p *Provider) someFunction(ctx context.Context, clients *Clients, cfg *conf
| `templates/` | Rendered Terraform root module that wraps the external module |
| `examples/azure-config.yaml` | Working config (at repo root `examples/`) |

### Local Provider (pkg/providers/cluster/local/) - **Stub**
### Local Provider (pkg/providers/cluster/local/)

**Location:** `pkg/providers/cluster/local/`

**Status:** Stub implementation for K3s local development
**Status:** NIC-managed kind (Kubernetes-in-Docker) cluster for local development

**Purpose:** Creates and tears down a local kind cluster as part of `nic deploy`/`nic destroy` (cluster named after `project_name`). Installs MetalLB so the gateway gets a LoadBalancer IP, deriving the address pool from the kind Docker network so it is routable. To deploy onto a pre-existing cluster instead, use the `existing` provider.

| File | Purpose |
|------|---------|
| `provider.go` | Stub provider that prints operations |
| `config.go` | Local-specific config types: `Config` |
| `provider.go` | Provider implementation: create/destroy the kind cluster, fetch kubeconfig, derive the MetalLB pool for `InfraSettings` |
| `kind.go` | kind cluster lifecycle via `sigs.k8s.io/kind` (create/delete/list, gitops mount, address-pool derivation) |
| `config.go` | Local-specific config types: `Config`, `KindConfig`, `KindMount`, `MetalLBConfig` |

## DNS Provider System (pkg/providers/dns/)

Expand Down Expand Up @@ -366,7 +369,7 @@ defer cleanup()
| `examples/aws-config-with-dns.yaml` | AWS config with Cloudflare DNS |
| `examples/gcp-config.yaml` | Sample GCP configuration |
| `examples/azure-config.yaml` | Sample Azure configuration |
| `examples/local-config.yaml` | Sample local K3s configuration |
| `examples/local-config.yaml` | Sample local kind configuration |

### Development Tools

Expand Down
50 changes: 1 addition & 49 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: help build test test-unit test-integration test-coverage test-race clean fmt vet lint install pre-commit release-snapshot localkind-up localkind-down
.PHONY: help build test test-unit test-integration test-coverage test-race clean fmt vet lint install pre-commit release-snapshot

# Variables
BINARY_NAME=nic
Expand Down Expand Up @@ -82,54 +82,6 @@ test-all: ## Run all tests (unit + integration)
LOCAL_CONFIG?=./examples/local-config.yaml
REGEN_APPS?=

localkind-up: build ## Create local kind cluster and deploy Nebari (mounts file:// gitops repos automatically)
@echo "Setting up local kind cluster..."
@which kind > /dev/null || (echo "Error: kind is not installed" && exit 1)
@which yq > /dev/null || (echo "Error: yq is not installed. Please install and try again" && exit 1)
@which docker > /dev/null || (echo "Error: Docker is not installed or not running" && exit 1)
-docker network create --subnet=192.168.1.0/24 --gateway=192.168.1.1 kind
@GITOPS_URL=$$(yq '.git_repository.url // ""' $(LOCAL_CONFIG)); \
PROJECT_NAME=$$(yq '.project_name // ""' $(LOCAL_CONFIG)); \
if echo "$$GITOPS_URL" | grep -q '^file:///'; then \
LOCAL_PATH=$$(echo "$$GITOPS_URL" | sed 's|^file://||'); \
echo "Mounting explicit gitops repo: $$LOCAL_PATH"; \
elif [ -z "$$GITOPS_URL" ]; then \
LOCAL_PATH="/tmp/nebari-gitops-$$PROJECT_NAME"; \
echo "No git_repository configured, mounting auto-generated dir: $$LOCAL_PATH"; \
else \
LOCAL_PATH=""; \
echo "Remote git repo detected ($$GITOPS_URL), no mount needed"; \
fi; \
if [ -n "$$LOCAL_PATH" ]; then \
mkdir -p "$$LOCAL_PATH"; \
KIND_CONFIG=$$(mktemp); \
printf '%s\n' \
'kind: Cluster' \
'apiVersion: kind.x-k8s.io/v1alpha4' \
'name: nebari-local' \
'nodes:' \
'- role: control-plane' \
' extraMounts:' \
" - hostPath: \"$$LOCAL_PATH\"" \
" containerPath: \"$$LOCAL_PATH\"" \
' readOnly: true' \
> "$$KIND_CONFIG"; \
kind create cluster --config "$$KIND_CONFIG" || true; \
rm -f "$$KIND_CONFIG"; \
else \
kind create cluster --name nebari-local || true; \
fi
@echo "Deploying Nebari to local cluster..."
time ./$(BINARY_NAME) deploy -f $(LOCAL_CONFIG) $(REGEN_APPS)
@echo "Local kind cluster is ready!"

localkind-rebuild: build localkind-down localkind-up ## Rebuild local kind cluster

localkind-down: ## Delete local kind cluster
-kind delete cluster -n nebari-local
-docker network rm kind 2>/dev/null
@echo "Local kind cluster deleted"

test-coverage: ## Run unit tests with coverage
@echo "Running unit tests with coverage..."
go test -v -short -coverprofile=coverage.out -covermode=atomic $(PKG_DIRS)
Expand Down
63 changes: 13 additions & 50 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ flowchart TD

subgraph CP["Cloud Provider"]
direction LR
aws["AWS EKS"] ~~~ gcp["GCP GKE"] ~~~ az["Azure AKS"] ~~~ hz["Hetzner K3s"] ~~~ k3s["Local K3s"]
aws["AWS EKS"] ~~~ gcp["GCP GKE"] ~~~ az["Azure AKS"] ~~~ hz["Hetzner k3s"] ~~~ knd["Local kind"]
end

SP --> NO --> FS --> K8 --> CP
Expand Down Expand Up @@ -135,7 +135,7 @@ Every NIC deployment includes a landing page where users discover and access all
| ----------------------------- | ------------------------------------------------------------------------------------------------------------ |
| **Opinionated Defaults** | Production-ready configuration out of the box — multi-AZ, autoscaling, security best practices |
| **Composable Software Packs** | Install only what you need. Each pack auto-integrates with SSO, telemetry, and routing |
| **Multi-Cloud** | AWS (EKS), GCP (GKE), Azure (AKS), Hetzner (K3s), and local (K3s) from the same config format |
| **Multi-Cloud** | AWS (EKS), GCP (GKE), Azure (AKS), Hetzner (k3s), and local (kind) from the same config format |
| **GitOps Native** | ArgoCD manages all foundational software with dependency ordering and health checks |
| **OpenTelemetry Native** | Built-in OTel Collector exports metrics, logs, and traces — plugs into whatever observability system you run |
| **SSO Everywhere** | Keycloak provides centralized auth. The Nebari Operator creates OAuth clients automatically |
Expand Down Expand Up @@ -269,7 +269,7 @@ NIC uses a YAML configuration file. See the `examples/` directory for sample con
- `examples/gcp-config.yaml` - GCP/GKE configuration
- `examples/azure-config.yaml` - Azure/AKS configuration
- `examples/hetzner-config.yaml` - Hetzner Cloud/K3s configuration
- `examples/local-config.yaml` - Local Kind/K3s configuration
- `examples/local-config.yaml` - Local kind configuration

### Environment Variables

Expand Down Expand Up @@ -299,60 +299,23 @@ OTEL_EXPORTER=otlp OTEL_ENDPOINT=localhost:4317 ./nic deploy -f config.yaml

### Local Cluster Testing with Kind

For local development, you can deploy a Kind cluster with foundational services:
For local development, you can deploy a kind cluster with foundational services via:

```bash
make localkind-up # Create Kind cluster and deploy
make localkind-down # Tear down
./nic deploy -f examples/local-config.yaml
```

When using a remote repo, a repo URL must be set in your `local-config.yaml`, and a valid private SSH key must be set as the `GIT_SSH_PRIVATE_KEY` environment variable.
The cluster is named after `project_name` (kube context `kind-<project_name>`) and requires Docker.

Ommitting the `git_repository` or explicitely setting a local git path will result in a local git directory being used for gitops.
**GitOps repository:**

### Local Cluster Testing with an Existing Cluster (k3s/k3d/minikube)
- Omit `git_repository` and NIC auto-creates and mounts a local repo at `/tmp/nebari-gitops-<project_name>` — zero configuration.
- For a remote repo, set `git_repository.url` to an SSH/HTTPS URL and supply credentials via the `GIT_SSH_PRIVATE_KEY` environment variable (or a token).
- For a custom local `file://` path, you must also declare a matching `cluster.local.kind.extra_mounts` entry so the kind node can read it — see `examples/local-config.yaml`.

The `local` provider works against any cluster already present in your kubeconfig — it does not create the cluster. To use a tool other than Kind:
### Using a pre-existing Cluster (k3d / k3s / minikube / cloud)

1. **Create the cluster** with your tool of choice.
2. **Point NIC at it** by setting `kube_context` in your `local-config.yaml` to the context name of that cluster (NIC reads the kubeconfig from `$KUBECONFIG`, falling back to `~/.kube/config`). `kube_context` is a context *name*, not a file path — list available names with `kubectl config get-contexts -o name`.
3. **Make the local GitOps directory visible to the cluster.** When `git_repository` is omitted (or set to a `file://` path), NIC uses a local GitOps directory at `/tmp/nebari-gitops-<project_name>`, where `project_name` comes from your config. ArgoCD's repo-server mounts this path via a `hostPath` volume, so it must exist *inside* the cluster node, not just on your host. Cluster nodes run in containers/VMs that don't share your host filesystem, so the directory must be bind-mounted in when the cluster is created. The `make localkind-up` target does this for you by generating a kind config with `extraMounts`; for k3d and minikube you mount it manually as shown below.

#### k3d

k3d nodes run as Docker containers and don't see your host's `/tmp` by default. Create the directory first, then mount it into the nodes at the same path:

```bash
mkdir -p /tmp/nebari-gitops-my-nebari-local

k3d cluster create \
--volume /tmp/nebari-gitops-my-nebari-local:/tmp/nebari-gitops-my-nebari-local@all

k3d kubeconfig get --all > kubeconfig
export KUBECONFIG=$(pwd)/kubeconfig

./nic deploy --file local-config.yaml
```

Set `kube_context: "k3d-<cluster-name>"` in your config (k3d prefixes the context with `k3d-`). For k3s clusters, also set `storage_class: local-path` and disable MetalLB (k3s ships ServiceLB) as noted in `examples/local-config.yaml`.

#### minikube

minikube runs the node inside a VM/container. Mount the host directory before deploying:

```bash
mkdir -p /tmp/nebari-gitops-my-nebari-local

minikube start
minikube mount /tmp/nebari-gitops-my-nebari-local:/tmp/nebari-gitops-my-nebari-local &

export KUBECONFIG=$HOME/.kube/config # minikube updates this automatically
./nic deploy --file local-config.yaml
```

`minikube mount` runs in the foreground and must stay running for the duration of the deploy (and while ArgoCD is reconciling), so launch it in a separate terminal or background it as shown. Set `kube_context: "minikube"` in your config.

> If you'd rather avoid the host-path mount entirely, set an explicit remote `git_repository` (see OPTION 3 in `examples/local-config.yaml`); ArgoCD then clones the repo over HTTPS/SSH and no local directory needs to be mounted into the node.
The `local` provider always creates its own kind cluster. To deploy onto a cluster you provisioned yourself (e.g., k3d, k3s, minikube, or a managed cloud cluster) use the `existing` provider instead. It connects to a context in your kubeconfig and installs the platform without provisioning any infrastructure. Note that it assumes the cluster already provides its own LoadBalancer. See `examples/existing-config.yaml`.

### Running Tests

Expand Down Expand Up @@ -403,7 +366,7 @@ pkg/
│ │ ├── gcp/ GCP provider
│ │ ├── azure/ Azure provider
│ │ ├── hetzner/ Hetzner Cloud provider (K3s via hetzner-k3s)
│ │ └── local/ Local Kind/K3s provider
│ │ └── local/ Local kind provider
│ └── dns/ DNS provider interface
│ └── cloudflare/ Cloudflare DNS provider
├── telemetry/ OpenTelemetry setup
Expand Down
39 changes: 26 additions & 13 deletions examples/local-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,20 @@ certificate:

cluster:
local:
kube_context: "kind-nebari-local"
# Optional kind config. Can be omitted entirely to use defaults.
# kind:
# node_image: kindest/node:v1.35.0 # default: bundled kind's default image
# extra_mounts:
# # Any additional host directory you want available inside the node
# - host_path: /absolute/host/path
# container_path: /absolute/node/path
# read_only: true
# # GitOps repo mount: only needed for a CUSTOM git_repository file://
# # path (OPTION 2 below). The default gitops repo is auto-mounted, so
# # omit this unless you set git_repository. host_path must match it exactly.
# - host_path: /home/user/my-gitops-repo
# container_path: /home/user/my-gitops-repo
# read_only: true
node_selectors:
general:
kubernetes.io/os: linux
Expand All @@ -14,33 +27,33 @@ cluster:
worker:
kubernetes.io/os: linux

# Storage class for persistent volumes (default: "standard")
# Use "local-path" for k3s clusters
# storage_class: local-path

# HTTPS port for Gateway listener (default: 443)
# Override if 443 is already in use or requires root
# https_port: 8443

# MetalLB configuration (default: enabled with 192.168.1.100-192.168.1.110 pool)
# Disable for k3s which ships with ServiceLB
# MetalLB provides the gateway's LoadBalancer IP. NIC derives the address pool
# from the kind Docker network automatically so the gateway IP is routable. override

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Tiny nit: "override" starts mid-sentence in lowercase here - reads a little oddly.

@marcelovilla marcelovilla Jun 22, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed in 33b10fe

# address_pool only to pin a specific range.
# metallb:
# enabled: false
# address_pool: 172.18.255.100-172.18.255.110

# GitOps repository configuration (optional)
# Configures the repository that ArgoCD will use to manage cluster resources
#
# OPTION 1: Auto-generated local directory (zero configuration for development)
# Simply omit the git_repository section and NIC will auto-create /tmp/nebari-gitops-{project_name}
# and mount it on the kind cluster
#
# OPTION 2: Explicit local directory (full control over location)
# OPTION 2: Explicit local directory (custom location)
# With the local (kind) provider you must also declare a matching
# cluster.local.kind.extra_mounts entry for the same path (see the kind block
# above). NIC only auto-mounts the OPTION 1 default, so a custom file:// path is
# invisible to the in-cluster ArgoCD repo-server unless you mount it yourself.
# The paths must match exactly.
# git_repository:
# # Local path to git repository (must be an absolute path)
# url: "file:///home/user/my-gitops-repo"
# url: "file:///home/user/my-gitops-repo" # must be an absolute path
# branch: main
# path: "clusters/local" # Optional subdirectory within the repo
# # No auth configuration needed for local paths
# path: "clusters/local" # optional subdirectory within the repo

# OPTION 3: Remote git repository (production setup)
# git_repository:
Expand Down
6 changes: 5 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,11 @@ require (
k8s.io/api v0.35.0
k8s.io/apimachinery v0.35.0
k8s.io/client-go v0.35.0
sigs.k8s.io/kind v0.32.0
)

require (
al.essio.dev/pkg/shellescape v1.5.1 // indirect
dario.cat/mergo v1.0.2 // indirect
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect
Expand Down Expand Up @@ -82,6 +84,7 @@ require (
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/evanphx/json-patch v5.9.11+incompatible // indirect
github.com/evanphx/json-patch/v5 v5.6.0 // indirect
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
Expand Down Expand Up @@ -125,7 +128,7 @@ require (
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
Expand All @@ -139,6 +142,7 @@ require (
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
Expand Down
Loading
Loading