Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
12 changes: 10 additions & 2 deletions .github/workflows/e2e-chart-netbird.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ permissions:

jobs:
e2e:
name: e2e (${{ matrix.chart }}, Kubernetes ${{ matrix.kubernetes.version }}, rootless ${{ matrix.rootless }})
name: e2e (${{ matrix.from_chart && format('{0} to ', matrix.from_chart) || '' }}${{ matrix.chart }}, Kubernetes ${{ matrix.kubernetes.version }}, rootless ${{ matrix.rootless }})
runs-on: ubuntu-latest
# Above the test step's own budget, so an overrun fails that step with its
# diagnostics rather than cancelling the job.
Expand All @@ -51,6 +51,13 @@ jobs:
kubernetes: ${{ fromJSON((github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.full_matrix)) && '[{"version":"1.34","image":"kindest/node:v1.34.11@sha256:44e222ee2132dab25ff87301682f89eb82c7880ea3a1bf543bfe9708fd08d67d"},{"version":"1.35","image":"kindest/node:v1.35.8@sha256:07b2536e30b803ed61d1677a79df6115f798ce64c80f9e22f6ed45afd09323c0"},{"version":"1.36","image":"kindest/node:v1.36.4@sha256:099e049362a1526b2db71494e1947aae99bd16290d7c895f2b7ea312e3cbfaed"},{"version":"1.37","image":"kindest/node:v1.37.0@sha256:a1ed56cfb0e7b93589bdf97c8cd566405a265939e3620fc4f5de89adff580ae5"}]' || '[{"version":"1.37","image":"kindest/node:v1.37.0@sha256:a1ed56cfb0e7b93589bdf97c8cd566405a265939e3620fc4f5de89adff580ae5"}]') }}
chart: [netbird, netbird-ee]
rootless: [false, true]
# The migration from netbird to netbird-ee (charts/netbird-ee/docs/migration.md),
# once, on the newest Kubernetes.
include:
- chart: netbird-ee
from_chart: netbird
rootless: false
kubernetes: {"version":"1.37","image":"kindest/node:v1.37.0@sha256:a1ed56cfb0e7b93589bdf97c8cd566405a265939e3620fc4f5de89adff580ae5"}
steps:
- name: Clone the code
uses: actions/checkout@v4
Expand Down Expand Up @@ -81,6 +88,7 @@ jobs:
timeout-minutes: 45
env:
NB_E2E_CHART: ${{ matrix.chart }}
NB_E2E_FROM_CHART: ${{ matrix.from_chart }}
NB_E2E_KIND_NODE_IMAGE: ${{ matrix.kubernetes.image }}
NB_E2E_ROOTLESS: ${{ matrix.rootless }}
NB_E2E_ARTIFACTS_DIR: ${{ runner.temp }}/e2e-artifacts
Expand All @@ -90,6 +98,6 @@ jobs:
if: failure()
uses: actions/upload-artifact@v4
with:
name: e2e-${{ matrix.chart }}-k8s-${{ matrix.kubernetes.version }}-rootless-${{ matrix.rootless }}
name: e2e-${{ matrix.from_chart && format('{0}-to-', matrix.from_chart) || '' }}${{ matrix.chart }}-k8s-${{ matrix.kubernetes.version }}-rootless-${{ matrix.rootless }}
path: ${{ runner.temp }}/e2e-artifacts
if-no-files-found: ignore
4 changes: 4 additions & 0 deletions charts/netbird-ee/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ netbird:
key: datastore-encryption-key
```

## Migrate from the netbird chart

An existing netbird release migrates in place with one `helm upgrade`, keeping its store and peers. See [docs/migration.md](docs/migration.md).

## Try it in Kind

[`examples/kind`](examples/kind/values.yaml) is the netbird chart's Kind example under `netbird:`, with fixed development credentials. The same tasks bring it up, on the netbird chart's Kind cluster config:
Expand Down
57 changes: 57 additions & 0 deletions charts/netbird-ee/docs/migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Migrate from the netbird chart

A release of the [netbird chart](../../netbird) becomes a netbird-ee release with one `helm upgrade` of the same release. netbird-ee renders the same resources under the same names and selectors, so Helm patches them in place: management keeps its volume or database, the credentials Secrets keep their values, and connected peers stay enrolled. The enterprise tables are created when management-cloud first starts. There is no separate schema migration.

The end-to-end suite runs this migration: `NB_E2E_CHART=netbird-ee NB_E2E_FROM_CHART=netbird task chart:e2e`.

## Before you start

- **Same backend version.** Management, signal and relay must run the version netbird-ee ships (`0.76.2`, see [`values.yaml`](../values.yaml)). If your release is older, upgrade it within the netbird chart first. If it is newer, for example the netbird chart's default `0.77.0`, wait for a netbird-ee release at that version; never move the store to an older management.
- **Single instance.** netbird-ee runs one replica of management and of signal. Set `replicaCount` to 1 for both before migrating.
- **Back up the store.** With the default SQLite store, snapshot the management volume (`<release>-management` PersistentVolumeClaim). With Postgres or MySQL, dump the database. Going back to the netbird chart after the migration is not supported; restoring this backup is the way back.

## Migrate

1. Export the release's values:

```bash
helm get values netbird --namespace netbird --output yaml > netbird-values.yaml
```

2. Delete every `image` block from `netbird-values.yaml`, under `dashboard` and under `backend.split.management`, `signal` and `relay`. Values you set win over netbird-ee's, so a leftover `image.repository` or `image.tag` keeps the community image.

3. Nest the values under `netbird:`:

```bash
{ echo 'netbird:'; sed 's/^\(.\)/ \1/' netbird-values.yaml; } > netbird-ee-values.yaml
```

4. Keep the release name, the namespace and `dataStoreEncryptionKey` (or `dataStoreEncryptionKeyRef`) unchanged. netbird-ee derives `NETBIRD_ENCRYPTION_KEY` from that key, and changing it makes the existing store unreadable.

5. Check what changes. Expect new images, the enterprise environment, and new `helm.sh/chart` labels, and no resource created or deleted:

```bash
helm dependency update charts/netbird-ee
helm template netbird charts/netbird --namespace netbird -f netbird-values.yaml > before.yaml
helm template netbird charts/netbird-ee --namespace netbird -f netbird-ee-values.yaml > after.yaml
diff before.yaml after.yaml
```

6. Upgrade the release:

```bash
helm upgrade netbird charts/netbird-ee --namespace netbird -f netbird-ee-values.yaml --wait --timeout 10m
```

Management restarts; peers reconnect once it is ready.

## Verify

```bash
kubectl --namespace netbird get deployments -o custom-columns=NAME:.metadata.name,IMAGE:.spec.template.spec.containers[0].image
kubectl --namespace netbird logs deployment/netbird-management | grep "Starting CloudServer"
```

Management, signal and the dashboard run the `-cloud` images, the relay keeps `netbirdio/relay`, and management logs `Starting CloudServer`. Your peers are still listed in the dashboard.

To turn on the paid modules, add the license afterwards; see [License](../README.md#license).
9 changes: 5 additions & 4 deletions e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,13 @@ Read [`netbird/chart_test.go`](netbird/chart_test.go). `TestChart` lists the ste

1. **Bootstrap** (`netbird.Bootstrap`):
- Create a one-node Kind cluster and install ingress-nginx on it.
- Install the chart (`NB_E2E_CHART`) from its own `examples/kind`, with its hostnames swapped for `netbird.e2e.test`, at backend version `NB_E2E_FROM_VERSION`.
- Install the first chart (`NB_E2E_FROM_CHART`, default `NB_E2E_CHART`) from its own `examples/kind`, with its hostnames swapped for `netbird.e2e.test`, at backend version `NB_E2E_FROM_VERSION`.
- Activate the account through `POST /api/setup` to get an API token.
2. **TestChart** (`netbird/chart_test.go`):
1. HTTP routes: the dashboard, the API and the IdP answer through the ingress.
2. netbird-ee only: the enterprise images run, and management starts the enterprise server.
3. Two peers connect directly.
4. The backends are upgraded to the chart's defaults, and the peers and the credential Secrets survive it.
2. Two peers connect directly.
3. The release is upgraded to the chart under test (`NB_E2E_FROM_CHART` → `NB_E2E_CHART`) and its backends to the chart's defaults, and the peers and the credential Secrets survive it.
4. netbird-ee only: the enterprise images run, and management starts the enterprise server.
5. Two peers forced onto the relay connect through it.
6. Management's pod is deleted, and every peer is still listed and reconnects.
3. **Teardown**: remove the peers and the cluster.
Expand All @@ -73,6 +73,7 @@ The peers run in Docker containers on Kind's network, outside the cluster. They
| Variable | Effect |
| --- | --- |
| `NB_E2E_CHART` | netbird: the chart under test, `netbird` (default) or `netbird-ee` |
| `NB_E2E_FROM_CHART` | netbird: the chart installed first. `netbird` with `NB_E2E_CHART=netbird-ee` tests the migration in `charts/netbird-ee/docs/migration.md` (default: `NB_E2E_CHART`) |
| `NB_E2E_KIND_NODE_IMAGE` | Kind node image, which picks the Kubernetes version |
| `NB_E2E_KEEP` | Leave the cluster running. The kubeconfig path is logged |
| `NB_E2E_ARTIFACTS_DIR` | On failure, write the cluster, chart and client logs here |
Expand Down
45 changes: 22 additions & 23 deletions e2e/framework/netbird/chart.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,39 +28,39 @@ func (e *Env) Enterprise() bool {
return e.Chart == ChartEnterprise
}

// valuesPrefix is where the netbird chart's values sit in the chart under
// test: netbird-ee wraps it as a dependency.
func (e *Env) valuesPrefix() string {
if e.Enterprise() {
// valuesPrefix is where the netbird chart's values sit in chart: netbird-ee
// wraps it as a dependency.
func valuesPrefix(chart string) string {
if chart == ChartEnterprise {
return "netbird."
}
return ""
}

// InstallChart installs or upgrades the chart from examples/kind under the
// suite's hostnames, with the backends at version.
func (e *Env) InstallChart(ctx context.Context, version string) error {
files, err := e.valuesFiles()
// InstallChart installs chart, or upgrades the release to it, from its
// examples/kind under the suite's hostnames, with the backends at version.
func (e *Env) InstallChart(ctx context.Context, chart, version string) error {
files, err := e.valuesFiles(chart)
if err != nil {
return err
}
args := []string{"upgrade", "--install", ReleaseName, framework.ChartDir(e.Chart), "--namespace", Namespace,
args := []string{"upgrade", "--install", ReleaseName, framework.ChartDir(chart), "--namespace", Namespace,
"--wait", "--timeout", "10m"}
for _, f := range files {
args = append(args, "--values", f)
}
for _, component := range []string{"management", "signal", "relay"} {
args = append(args, "--set", fmt.Sprintf("%sbackend.split.%s.image.tag=%s", e.valuesPrefix(), component, version))
args = append(args, "--set", fmt.Sprintf("%sbackend.split.%s.image.tag=%s", valuesPrefix(chart), component, version))
}
_, err = e.Cluster.Helm(ctx, args...)
return err
}

// valuesFiles returns the chart's examples/kind with the suite's hostnames,
// the netbird chart's examples/rootless when rootless, and the suite's own
// additions, the last two nested where the chart under test expects them.
func (e *Env) valuesFiles() ([]string, error) {
example, err := os.ReadFile(filepath.Join(framework.ChartDir(e.Chart), "examples", "kind", "values.yaml"))
// valuesFiles returns chart's examples/kind with the suite's hostnames, the
// netbird chart's examples/rootless when rootless, and the suite's own
// additions, the last two nested where chart expects them.
func (e *Env) valuesFiles(chart string) ([]string, error) {
example, err := os.ReadFile(filepath.Join(framework.ChartDir(chart), "examples", "kind", "values.yaml"))
if err != nil {
return nil, err
}
Expand All @@ -74,15 +74,15 @@ func (e *Env) valuesFiles() ([]string, error) {
if err != nil {
return nil, err
}
rootlessValues, err := e.writeValues("rootless-values.yaml", string(rootless))
rootlessValues, err := writeValues(chart, filepath.Join(e.workDir, "rootless-values.yaml"), string(rootless))
if err != nil {
return nil, err
}
files = append(files, rootlessValues)
}

// No owner, so POST /api/setup creates the account and returns a token.
suiteValues, err := e.writeValues("e2e-values.yaml", `backend:
suiteValues, err := writeValues(chart, filepath.Join(e.workDir, "e2e-values.yaml"), `backend:
split:
management:
env:
Expand All @@ -96,14 +96,13 @@ func (e *Env) valuesFiles() ([]string, error) {
return append(files, suiteValues), err
}

// writeValues writes netbird chart values into the work dir, under netbird:
// for netbird-ee, and returns the file's path. Indenting every line keeps block
// scalars intact.
func (e *Env) writeValues(name, values string) (string, error) {
if e.Enterprise() {
// writeValues writes netbird chart values to path, under netbird: when chart
// is netbird-ee, and returns path. Indenting every line keeps block scalars
// intact.
func writeValues(chart, path, values string) (string, error) {
if chart == ChartEnterprise {
values = "netbird:\n" + regexp.MustCompile(`(?m)^(.)`).ReplaceAllString(values, " $1")
}
path := filepath.Join(e.workDir, name)
return path, os.WriteFile(path, []byte(values), 0o644)
}

Expand Down
20 changes: 15 additions & 5 deletions e2e/framework/netbird/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const (
ChartEnterprise = "netbird-ee"

envChart = "NB_E2E_CHART"
envFromChart = "NB_E2E_FROM_CHART"
envFromVersion = "NB_E2E_FROM_VERSION"
envRootless = "NB_E2E_ROOTLESS"
)
Expand All @@ -52,7 +53,10 @@ const (
// peers the tests started outside it.
type Env struct {
// Chart is the chart under test: ChartCommunity or ChartEnterprise.
Chart string
// FromChart is the chart installed first, Chart unless the suite tests
// the migration from netbird to netbird-ee.
Chart string
FromChart string
// FromVersion is the backend version installed first, DefaultVersion the
// chart's. They are equal when there is no upgrade to test.
FromVersion string
Expand Down Expand Up @@ -84,11 +88,16 @@ func Bootstrap(ctx context.Context) (*Env, error) {
if chart != ChartCommunity && chart != ChartEnterprise {
return nil, fmt.Errorf("%s must be %s or %s, not %q", envChart, ChartCommunity, ChartEnterprise, chart)
}
fromChart := framework.EnvOr(envFromChart, chart)
if fromChart != chart && (fromChart != ChartCommunity || chart != ChartEnterprise) {
return nil, fmt.Errorf("%s=%s with %s=%s: the only migration is from %s to %s", envFromChart, fromChart, envChart, chart, ChartCommunity, ChartEnterprise)
}
defaultVersion, err := chartBackendVersion(chart)
if err != nil {
return nil, err
}
// netbird-ee has no earlier release to upgrade from yet.
// netbird-ee has no earlier release to upgrade from yet, and a migration
// keeps the backend version: the enterprise store must not be older.
fromVersion := defaultFromVersion
if chart == ChartEnterprise {
fromVersion = defaultVersion
Expand All @@ -100,11 +109,12 @@ func Bootstrap(ctx context.Context) (*Env, error) {
e := &Env{
workDir: workDir,
Chart: chart,
FromChart: fromChart,
DefaultVersion: defaultVersion,
FromVersion: framework.EnvOr(envFromVersion, fromVersion),
Rootless: framework.EnvBool(envRootless),
}
log.Printf("e2e: chart %s, backends %s, then %s; rootless %t", e.Chart, e.FromVersion, e.DefaultVersion, e.Rootless)
log.Printf("e2e: chart %s with backends %s, then %s with %s; rootless %t", e.FromChart, e.FromVersion, e.Chart, e.DefaultVersion, e.Rootless)
if e.Enterprise() {
log.Print("e2e: vendoring the netbird chart into netbird-ee")
if _, err := framework.Command(ctx, "helm", "dependency", "update", framework.ChartDir(ChartEnterprise)); err != nil {
Expand All @@ -124,8 +134,8 @@ func Bootstrap(ctx context.Context) (*Env, error) {
if err := e.prepareNamespace(ctx); err != nil {
return e, err
}
log.Printf("e2e: installing the chart with backends %s", e.FromVersion)
if err := e.InstallChart(ctx, e.FromVersion); err != nil {
log.Printf("e2e: installing %s with backends %s", e.FromChart, e.FromVersion)
if err := e.InstallChart(ctx, e.FromChart, e.FromVersion); err != nil {
return e, err
}
e.ingress, err = e.Cluster.ForwardIngress(ctx)
Expand Down
18 changes: 10 additions & 8 deletions e2e/netbird/chart_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ func TestChart(t *testing.T) {
run func(t *testing.T)
}{
{"the dashboard, the API and the IdP answer through the ingress", testHTTPRoutes},
{"peers connect directly", testDirectConnection},
{"an upgrade to the chart and its backends keeps the peers", testUpgrade},
{"netbird-ee runs the enterprise images", testEnterpriseImages},
{"netbird-ee management runs the enterprise server", testEnterpriseServer},
{"peers connect directly", testDirectConnection},
{"an upgrade to the chart's backends keeps the peers", testUpgrade},
{"peers connect through the relay", testRelayedConnection},
{"management keeps its peers across a restart", testManagementRestart},
}
Expand Down Expand Up @@ -55,7 +55,7 @@ func testEnterpriseImages(t *testing.T) {
images["netbird-dashboard"] += "-rootless"
}
for _, component := range []string{"management", "signal", "relay"} {
images["netbird-"+component] = backendImage(component, env.FromVersion)
images["netbird-"+component] = backendImage(component, env.DefaultVersion)
}
for name, image := range images {
if err := env.WaitDeploymentImage(t.Context(), name, image); err != nil {
Expand Down Expand Up @@ -95,17 +95,19 @@ func testDirectConnection(t *testing.T) {
requireConnected(t, beta, alpha, connP2P)
}

// testUpgrade upgrades the backends from FromVersion to the chart's defaults
// and checks the peers and the credential Secrets survive it.
// testUpgrade upgrades the release from FromChart to Chart and the backends
// from FromVersion to the chart's defaults, and checks the peers and the
// credential Secrets survive it. From netbird to netbird-ee it is the
// migration charts/netbird-ee/docs/migration.md describes.
func testUpgrade(t *testing.T) {
if env.FromVersion == env.DefaultVersion {
t.Skipf("installed at the chart's default %s; nothing to upgrade", env.DefaultVersion)
if env.FromChart == env.Chart && env.FromVersion == env.DefaultVersion {
t.Skipf("installed %s at its default %s; nothing to upgrade", env.Chart, env.DefaultVersion)
}
before, err := env.CredentialSecrets(t.Context())
if err != nil {
t.Fatal(err)
}
if err := env.InstallChart(t.Context(), env.DefaultVersion); err != nil {
if err := env.InstallChart(t.Context(), env.Chart, env.DefaultVersion); err != nil {
t.Fatal(err)
}
for _, component := range []string{"management", "signal", "relay"} {
Expand Down
Loading