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
6 changes: 3 additions & 3 deletions core/cmd/initialize/initialize.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,15 @@ type Options struct {
}

// NewTier1Options creates Options for Tier1 initialization.
func NewTier1Options(cfg *config.Tier1Config) Options {
func NewTier1Options(fs afero.Fs, cfg *config.Tier1Config) Options {
walDirectory := cfg.Wal.WALPath
kopiaDirectory := cfg.Base.RepositoryDirectory

return Options{
WalFS: afero.NewBasePathFs(afero.NewOsFs(), walDirectory),
WalFS: afero.NewBasePathFs(fs, walDirectory),
WalEncryptionPassword: cfg.EncryptionKey,
Kopia: &KopiaOptions{
FS: afero.NewBasePathFs(afero.NewOsFs(), kopiaDirectory),
FS: afero.NewBasePathFs(fs, kopiaDirectory),
EncryptionPassword: cfg.EncryptionKey,
InitializeRepo: func(ctx context.Context) error {
return kopiaserver.InitializeTier1(ctx, cfg)
Expand Down
124 changes: 124 additions & 0 deletions core/cmd/server/directories_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
Copyright © contributors to CloudNativePG, established as
CloudNativePG a Series of LF Projects, LLC.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

SPDX-License-Identifier: Apache-2.0
*/

package server

import (
"context"
"testing"

"github.com/spf13/afero"

"github.com/cloudnative-pg/klio/core/pkg/config"
)

func assertDirExists(t *testing.T, fs afero.Fs, dir string) {
t.Helper()

ok, err := afero.DirExists(fs, dir)
if err != nil {
t.Fatalf("while checking if directory %q exists: %v", dir, err)
}
if !ok {
t.Fatalf("expected directory %q to exist", dir)
}
}

func assertDirNotExists(t *testing.T, fs afero.Fs, dir string) {
t.Helper()

ok, err := afero.DirExists(fs, dir)
if err != nil {
t.Fatalf("while checking if directory %q exists: %v", dir, err)
}
if ok {
t.Fatalf("expected directory %q not to exist", dir)
}
}

func newTestServerConfig() *config.ServerConfig {
return &config.ServerConfig{
QueueDirectory: "/queue",
Tier1: config.Tier1Config{
Base: config.BaseServerConfig{
CacheDirectory: "/cache_tier1/kopia-cache",
RepositoryDirectory: "/data/base",
},
Wal: config.WalServerConfig{
WALPath: "/data/wal",
},
},
Tier2: config.Tier2Config{
CacheDirectory: "/cache_tier2/kopia-cache",
},
}
}

// TestInitializeTier1CreatesDirectories verifies that initializeTier1
// creates the queue, WAL, Kopia repository, and cache directories, and does
// not touch the tier2 one, even though it later fails (the kopia binary is
// absent in unit tests). This test only asserts directory creation.
func TestInitializeTier1CreatesDirectories(t *testing.T) {
cfg := newTestServerConfig()
fs := afero.NewMemMapFs()

_ = initializeTier1(context.Background(), fs, cfg)

assertDirExists(t, fs, cfg.QueueDirectory)
assertDirExists(t, fs, cfg.Tier1.Wal.WALPath)
assertDirExists(t, fs, cfg.Tier1.Base.RepositoryDirectory)
assertDirExists(t, fs, cfg.Tier1.Base.CacheDirectory)
assertDirNotExists(t, fs, cfg.Tier2.CacheDirectory)
}

// TestInitializeTier1RequiresQueueDirectory verifies that initializeTier1
// fails, without creating any directory, when the queue directory is not
// configured.
func TestInitializeTier1RequiresQueueDirectory(t *testing.T) {
cfg := newTestServerConfig()
cfg.QueueDirectory = ""
fs := afero.NewMemMapFs()

err := initializeTier1(context.Background(), fs, cfg)
if err == nil {
t.Fatal("expected an error, got nil")
}

assertDirNotExists(t, fs, cfg.Tier1.Wal.WALPath)
assertDirNotExists(t, fs, cfg.Tier1.Base.RepositoryDirectory)
assertDirNotExists(t, fs, cfg.Tier1.Base.CacheDirectory)
}

// TestInitializeTier2CreatesDirectories verifies that initializeTier2
// creates only the tier2 cache directory, and none of the tier1 ones (nor
// the queue directory, which is only needed when tier1 is enabled), even
// though it later fails (the kopia binary is absent in unit tests). This
// test only asserts directory creation.
func TestInitializeTier2CreatesDirectories(t *testing.T) {
cfg := newTestServerConfig()
fs := afero.NewMemMapFs()

_ = initializeTier2(context.Background(), fs, cfg)

assertDirExists(t, fs, cfg.Tier2.CacheDirectory)
assertDirNotExists(t, fs, cfg.QueueDirectory)
assertDirNotExists(t, fs, cfg.Tier1.Wal.WALPath)
assertDirNotExists(t, fs, cfg.Tier1.Base.RepositoryDirectory)
assertDirNotExists(t, fs, cfg.Tier1.Base.CacheDirectory)
}
49 changes: 39 additions & 10 deletions core/cmd/server/initialize.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@ package server

import (
"context"
"errors"
"fmt"

"github.com/cloudnative-pg/machinery/pkg/log"
"github.com/spf13/afero"

"github.com/cloudnative-pg/klio/core/cmd/initialize"
"github.com/cloudnative-pg/klio/core/internal/tier2"
Expand All @@ -32,34 +34,61 @@ import (

func initializeRepository(ctx context.Context, opts serverOpts) error {
if opts.tier1 {
if err := initializeTier1(ctx, opts.cfg); err != nil {
if err := initializeTier1(ctx, opts.fs, opts.cfg); err != nil {
return err
}
}

if opts.tier2 {
if err := initializeTier2(ctx, opts.cfg); err != nil {
if err := initializeTier2(ctx, opts.fs, opts.cfg); err != nil {
return err
}
}

return nil
}

func initializeTier1(ctx context.Context, cfg *config.ServerConfig) error {
walDirectory := cfg.Tier1.Wal.WALPath
kopiaDirectory := cfg.Tier1.Base.RepositoryDirectory

func initializeTier1(ctx context.Context, fs afero.Fs, cfg *config.ServerConfig) error {
log.FromContext(ctx).Info(
"Ensuring tier1 repository is initialized.",
"walDirectory", walDirectory,
"kopiaDirectory", kopiaDirectory,
"walDirectory", cfg.Tier1.Wal.WALPath,
"kopiaDirectory", cfg.Tier1.Base.RepositoryDirectory,
"cacheDirectory", cfg.Tier1.Base.CacheDirectory,
"queueDirectory", cfg.QueueDirectory,
)

return initialize.Run(ctx, initialize.NewTier1Options(&cfg.Tier1))
// The queue is always required when tier1 is enabled to support retention
// policy enforcement. Retention needs to check for pending tier2 transfers
// even when tier2 is not configured, to allow consistent behavior across
// configurations.
if cfg.QueueDirectory == "" {
return errors.New("queue is required when tier1 is enabled")
}

if err := fs.MkdirAll(cfg.QueueDirectory, 0o750); err != nil {
return fmt.Errorf("while ensuring that the queue directory exists: %w", err)
}

if err := fs.MkdirAll(cfg.Tier1.Wal.WALPath, 0o750); err != nil {
return fmt.Errorf("while ensuring that the tier1 WAL directory exists: %w", err)
}

if err := fs.MkdirAll(cfg.Tier1.Base.RepositoryDirectory, 0o750); err != nil {
return fmt.Errorf("while ensuring that the tier1 repository directory exists: %w", err)
}

if err := fs.MkdirAll(cfg.Tier1.Base.CacheDirectory, 0o750); err != nil {
return fmt.Errorf("while ensuring that the tier1 cache directory exists: %w", err)
}

return initialize.Run(ctx, initialize.NewTier1Options(fs, &cfg.Tier1))
}

func initializeTier2(ctx context.Context, cfg *config.ServerConfig) error {
func initializeTier2(ctx context.Context, fs afero.Fs, cfg *config.ServerConfig) error {
if err := fs.MkdirAll(cfg.Tier2.CacheDirectory, 0o750); err != nil {
return fmt.Errorf("while ensuring that the tier2 cache directory exists: %w", err)
}

tier2BaseFS, err := tier2.ConnectBase(ctx, &cfg.Tier2)
if err != nil {
return fmt.Errorf("error while connecting to tier2 (base): %w", err)
Expand Down
10 changes: 2 additions & 8 deletions core/cmd/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ package server

import (
"context"
"errors"
"fmt"
"os"

"github.com/cloudnative-pg/machinery/pkg/log"
"github.com/spf13/afero"
"github.com/thejerf/suture/v4"

"github.com/cloudnative-pg/klio/core/internal/kopia"
Expand Down Expand Up @@ -81,6 +81,7 @@ type serverOpts struct {
tier1 bool
tier2 bool

fs afero.Fs
cfg *config.ServerConfig
adminSocketPath string
runID string
Expand Down Expand Up @@ -193,15 +194,8 @@ func runServer(ctx context.Context, opts serverOpts) error {
})

// Configure NATS
// The queue is always required when tier1 is enabled to support retention policy
// enforcement. Retention needs to check for pending tier2 transfers even when
// tier2 is not configured, to allow consistent behavior across configurations.
var queueURL string
if opts.tier1 {
if opts.cfg.QueueDirectory == "" {
return errors.New("queue is required when tier1 is enabled")
}

nats, err := server.NewNatsService(opts.cfg.QueueDirectory)
if err != nil {
return err
Expand Down
2 changes: 2 additions & 0 deletions core/cmd/server/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (

"github.com/cloudnative-pg/machinery/pkg/log"
"github.com/google/uuid"
"github.com/spf13/afero"
"github.com/spf13/cobra"
"github.com/spf13/viper"

Expand Down Expand Up @@ -92,6 +93,7 @@ var startCmd = &cobra.Command{
opts := serverOpts{
tier1: tier1Enabled,
tier2: tier2Enabled,
fs: afero.NewOsFs(),
cfg: &configuration,
runID: runID.String(),
runSecret: runSecret.String(),
Expand Down
3 changes: 3 additions & 0 deletions documentation/.wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ github
goroutines
grpc
gzip
hardcodes
hostPath
http
https
Expand Down Expand Up @@ -322,6 +323,8 @@ sfixed
sint
snapshotted
str
subdirectories
subdirectory
subprocess
tablespaces
teardown
Expand Down
14 changes: 10 additions & 4 deletions documentation/web/docs/developer/running-e2e-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ The E2E tests are located in `operator/test/e2e/` and include:
(`RecoverClusterFromTier2`)
- **`tier2_pitr_test.go`** - Point-in-time recovery from tier2 storage
(`RecoverClusterFromTier2Pitr`)
- **`tier2_recovery_common_test.go`** - Shared tier2 recovery helpers used
by both of the above; also asserts that the read-only (tier2-only)
recovery Server gets the unified `klio` PVC/mount, same as a tier1
server
- **`tier2_retention_test.go`** - Backup and WAL retention policy
enforcement in tier2 storage (`Tier2Retention`)
- **`compression_test.go`** - Kopia compression policies: verifies the
Expand All @@ -132,12 +136,14 @@ The E2E tests are located in `operator/test/e2e/` and include:
server-side tier1 retention prunes old WALs only after they reach tier2,
driven by backup completion rather than a client command
(`WALRetentionQueueAwareness`)
- **`server_reconfig_test.go`** - Adding tier2 storage to an existing
tier1+queue server (`ServerTierReconfiguration`)
- **`server_reconfig_test.go`** - Adding tier2 to an existing tier1-only
server: verifies the single unified `klio` PVC is retained (same UID,
no new PVC created) since the StatefulSet's VolumeClaimTemplates are
unaffected by tier1/tier2 changes (`ServerTierReconfiguration`)
- **`pluginconfiguration_update_test.go`** - PluginConfiguration updates
and sidecar restart behavior (`PluginConfigurationUpdate`)
- **`pvc_resize_test.go`** - PVC resize for data, cache, and queue
volumes (`PVCResize`)
- **`pvc_resize_test.go`** - Resize of the single unified `klio` PVC
backing `/klio` (`PVCResize`)
- **`otel_test.go`** - OpenTelemetry metrics and traces export: deploys
an OTEL Collector and verifies that backup lifecycle metrics and
traces are correctly exported via OTLP. After the success-path
Expand Down
Loading
Loading