From 61fc15e3ab2b0d5ebd078015f84370a7710d68f0 Mon Sep 17 00:00:00 2001 From: Igor Shishkin Date: Wed, 26 Jun 2024 09:04:49 +0300 Subject: [PATCH 1/6] Describe image change procedure (#49) Signed-off-by: Igor Shishkin --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 450a0b8..7b19443 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,22 @@ drop-in replacement for official ceph image to use for `cephadm shell` command. Container image is available at [GitHub Packages](https://github.com/runityru/cephctl/pkgs/container/cephctl%2Fceph) +To replace official Ceph image with the one containing cephctl in cephadm +clusters just do: + +```shell +# Set container image as a global parameter to all components +ceph config set global container_image ghcr.io/runityru/cephctl/ceph:v18.2.2 + +# Run upgrade procedure +ceph orch upgrade start --ceph_version=18.2.2 +``` + +Please note Ceph orch will automatically replace `container_image` parameter +for each component with specific sha256 image ID instead of tag we defined +manually. It's OK and it's a guarantee the image won't be changed in your +cluster. + ### Build from source It's possible to build cephctl from source by simply running the following From e5eb4c733f566a0465a505ed8af261ac18b880bf Mon Sep 17 00:00:00 2001 From: Igor Shishkin Date: Wed, 26 Jun 2024 09:09:57 +0300 Subject: [PATCH 2/6] Add osd config parameters to cluster report (#50) Signed-off-by: Igor Shishkin --- ceph/ceph_test.go | 5 +++++ ceph/models/report.go | 10 +++++++--- ceph/models/report_test.go | 21 ++++++++++++++++++--- models/clusterreport.go | 4 ++++ 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/ceph/ceph_test.go b/ceph/ceph_test.go index a77c2cd..38a590a 100644 --- a/ceph/ceph_test.go +++ b/ceph/ceph_test.go @@ -56,6 +56,11 @@ func TestClusterReport(t *testing.T) { "active": 234, "clean": 234, }, + AllowCrimson: false, + NearfullRatio: 0.85, + BackfillfullRatio: 0.9, + FullRatio: 0.95, + RequireMinCompatClient: "luminous", OSDDaemons: []models.OSDDaemon{ { ID: 0, diff --git a/ceph/models/report.go b/ceph/models/report.go index d320af4..9a12c36 100644 --- a/ceph/models/report.go +++ b/ceph/models/report.go @@ -261,9 +261,9 @@ type ReportOSDMap struct { FlagsNum int `json:"flags_num"` FlagsSet []string `json:"flags_set"` CrushVersion int `json:"crush_version"` - FullRatio float64 `json:"full_ratio"` - BackfillfullRatio float64 `json:"backfillfull_ratio"` - NearfullRatio float64 `json:"nearfull_ratio"` + FullRatio float32 `json:"full_ratio"` + BackfillfullRatio float32 `json:"backfillfull_ratio"` + NearfullRatio float32 `json:"nearfull_ratio"` ClusterSnapshot string `json:"cluster_snapshot"` PoolMax int `json:"pool_max"` MaxOsd int `json:"max_osd"` @@ -1087,6 +1087,10 @@ func (r *Report) ToSvc() (models.ClusterReport, error) { NumPools: uint16(numPools), NumPGs: numPGs, NumPGsByState: numPGsByState, + BackfillfullRatio: r.OSDMap.BackfillfullRatio, + FullRatio: r.OSDMap.FullRatio, + NearfullRatio: r.OSDMap.NearfullRatio, + RequireMinCompatClient: r.OSDMap.MinCompatClient, }, nil } diff --git a/ceph/models/report_test.go b/ceph/models/report_test.go index 2b0cce2..f0280b5 100644 --- a/ceph/models/report_test.go +++ b/ceph/models/report_test.go @@ -128,7 +128,12 @@ func TestReportToSvc(t *testing.T) { "clean": 278, "remapped": 52, }, - OSDDaemons: osdDaemons, + OSDDaemons: osdDaemons, + AllowCrimson: false, + NearfullRatio: 0.85, + BackfillfullRatio: 0.9, + FullRatio: 0.95, + RequireMinCompatClient: "luminous", }, }, { @@ -177,7 +182,12 @@ func TestReportToSvc(t *testing.T) { "clean": 250, "remapped": 153, }, - OSDDaemons: osdDaemons, + OSDDaemons: osdDaemons, + AllowCrimson: false, + NearfullRatio: 0.85, + BackfillfullRatio: 0.9, + FullRatio: 0.95, + RequireMinCompatClient: "luminous", }, }, { @@ -256,7 +266,12 @@ func TestReportToSvc(t *testing.T) { "peered": 20, "undersized": 111, }, - OSDDaemons: osdDaemons, + OSDDaemons: osdDaemons, + AllowCrimson: false, + NearfullRatio: 0.85, + BackfillfullRatio: 0.9, + FullRatio: 0.95, + RequireMinCompatClient: "luminous", }, }, } diff --git a/models/clusterreport.go b/models/clusterreport.go index d485a12..1f9a793 100644 --- a/models/clusterreport.go +++ b/models/clusterreport.go @@ -14,10 +14,13 @@ type OSDDaemon struct { type ClusterReport struct { AllowCrimson bool + BackfillfullRatio float32 Checks []ClusterStatusCheck Devices []Device + FullRatio float32 HealthStatus ClusterStatusHealth MutedChecks []ClusterStatusMutedCheck + NearfullRatio float32 NumMons uint8 NumMonsInQuorum uint8 NumOSDs uint16 @@ -31,6 +34,7 @@ type ClusterReport struct { NumPGsByState map[string]uint32 NumPools uint16 OSDDaemons []OSDDaemon + RequireMinCompatClient string StretchMode bool TotalOSDCapacityKB uint64 TotalOSDUsedDataKB uint64 From ec23ad09612dd192310a10218119ecad478ab693 Mon Sep 17 00:00:00 2001 From: Igor Shishkin Date: Wed, 26 Jun 2024 09:14:21 +0300 Subject: [PATCH 3/6] Use logger to obtain commands stderr instead of directly passing STDERR (#52) Signed-off-by: Igor Shishkin --- ceph/ceph.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/ceph/ceph.go b/ceph/ceph.go index 283ce3b..b7ea8a8 100644 --- a/ceph/ceph.go +++ b/ceph/ceph.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "encoding/json" - "os" "os/exec" "github.com/pkg/errors" @@ -37,7 +36,7 @@ func (c *ceph) ApplyCephConfigOption(ctx context.Context, section, key, value st bin, args := mkCommand(c.binaryPath, []string{"config", "set", section, key, value}) cmd := exec.CommandContext(ctx, bin, args...) - cmd.Stderr = os.Stderr + cmd.Stderr = log.StandardLogger().WriterLevel(log.DebugLevel) if err := cmd.Run(); err != nil { return errors.Wrap(err, "error applying configuration") } @@ -50,7 +49,7 @@ func (c *ceph) ClusterReport(ctx context.Context) (models.ClusterReport, error) cmd := exec.CommandContext(ctx, bin, args...) cmd.Stdout = buf - cmd.Stderr = os.Stderr + cmd.Stderr = log.StandardLogger().WriterLevel(log.DebugLevel) if err := cmd.Run(); err != nil { return models.ClusterReport{}, errors.Wrap(err, "error retrieving report") } @@ -71,7 +70,7 @@ func (c ceph) ClusterStatus(ctx context.Context) (models.ClusterStatus, error) { cmd := exec.CommandContext(ctx, bin, args...) cmd.Stdout = buf - cmd.Stderr = os.Stderr + cmd.Stderr = log.StandardLogger().WriterLevel(log.DebugLevel) if err := cmd.Run(); err != nil { return models.ClusterStatus{}, errors.Wrap(err, "error retrieving cluster status") } @@ -93,7 +92,7 @@ func (c *ceph) DumpConfig(ctx context.Context) (models.CephConfig, error) { cmd := exec.CommandContext(ctx, bin, args...) cmd.Stdout = buf - cmd.Stderr = os.Stderr + cmd.Stderr = log.StandardLogger().WriterLevel(log.DebugLevel) if err := cmd.Run(); err != nil { return nil, errors.Wrap(err, "error running command") } @@ -122,7 +121,7 @@ func (c *ceph) ListDevices(ctx context.Context) ([]models.Device, error) { cmd := exec.CommandContext(ctx, bin, args...) cmd.Stdout = buf - cmd.Stderr = os.Stderr + cmd.Stderr = log.StandardLogger().WriterLevel(log.DebugLevel) if err := cmd.Run(); err != nil { return nil, errors.Wrap(err, "error listing devices") } @@ -147,7 +146,7 @@ func (c *ceph) RemoveCephConfigOption(ctx context.Context, section, key string) bin, args := mkCommand(c.binaryPath, []string{"config", "rm", section, key}) cmd := exec.CommandContext(ctx, bin, args...) - cmd.Stderr = os.Stderr + cmd.Stderr = log.StandardLogger().WriterLevel(log.DebugLevel) if err := cmd.Run(); err != nil { return errors.Wrap(err, "error applying configuration") } From 4b6195dd3294215028e9dedda2f9190172db55ce Mon Sep 17 00:00:00 2001 From: Igor Shishkin Date: Wed, 26 Jun 2024 09:18:44 +0300 Subject: [PATCH 4/6] Add CephOSDConfig management (#51) Please note, this PR contains parts of #50 and #52 so they must be merged first. --------- Signed-off-by: Igor Shishkin --- README.md | 2 +- ceph/ceph.go | 29 ++++ ceph/ceph_test.go | 17 +++ ceph/config/spec/cephosdconfig/spec.go | 22 +++ ceph/config/spec/cephosdconfig/spec_test.go | 44 ++++++ .../spec/cephosdconfig/testdata/empty.json | 1 + .../spec/cephosdconfig/testdata/full.json | 7 + ceph/mock.go | 5 + .../ceph_mock_ApplyCephOSDConfigOption | 5 + cmd/cephctl/main.go | 17 ++- commands/apply/apply.go | 11 ++ commands/apply/apply_test.go | 23 ++- commands/apply/testdata/cephosdconfig.yaml | 8 ++ commands/diff/diff.go | 20 +++ commands/diff/diff_test.go | 35 ++++- commands/diff/testdata/cephosdconfig.yaml | 8 ++ commands/dump/cephosdconfig/cephosdconfig.go | 41 ++++++ .../dump/cephosdconfig/cephosdconfig_test.go | 40 ++++++ differ/differ.go | 54 +++++++ differ/differ_test.go | 75 ++++++++++ differ/mock.go | 5 + go.mod | 1 + go.sum | 2 + models/cephosdconfig.go | 17 +++ service/mock.go | 15 ++ service/service.go | 45 ++++++ service/service_test.go | 134 ++++++++++++++++++ 27 files changed, 677 insertions(+), 6 deletions(-) create mode 100644 ceph/config/spec/cephosdconfig/spec.go create mode 100644 ceph/config/spec/cephosdconfig/spec_test.go create mode 100644 ceph/config/spec/cephosdconfig/testdata/empty.json create mode 100644 ceph/config/spec/cephosdconfig/testdata/full.json create mode 100755 ceph/testdata/ceph_mock_ApplyCephOSDConfigOption create mode 100644 commands/apply/testdata/cephosdconfig.yaml create mode 100644 commands/diff/testdata/cephosdconfig.yaml create mode 100644 commands/dump/cephosdconfig/cephosdconfig.go create mode 100644 commands/dump/cephosdconfig/cephosdconfig_test.go create mode 100644 models/cephosdconfig.go diff --git a/README.md b/README.md index 7b19443..5b96a9b 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ to adjust `ceph` binary path to access ceph in container and/or remote machine. * [X] FreeBSD support in builds * [X] Remote Ceph cluster access via SSH * [ ] v0.2.0 - * [ ] Apply/Dump declarative configuration for `ceph osd set-*` stuff + * [X] Apply/Dump declarative configuration for `ceph osd set-*` stuff * [ ] v0.3.0 * [ ] Apply/Dump declarative configuration for Ceph Object Gateway (rgw) * [ ] v0.4.0 diff --git a/ceph/ceph.go b/ceph/ceph.go index b7ea8a8..d96c7d3 100644 --- a/ceph/ceph.go +++ b/ceph/ceph.go @@ -15,6 +15,7 @@ import ( type Ceph interface { ApplyCephConfigOption(ctx context.Context, section, key, value string) error + ApplyCephOSDConfigOption(ctx context.Context, key, value string) error ClusterReport(ctx context.Context) (models.ClusterReport, error) ClusterStatus(ctx context.Context) (models.ClusterStatus, error) DumpConfig(ctx context.Context) (models.CephConfig, error) @@ -43,6 +44,34 @@ func (c *ceph) ApplyCephConfigOption(ctx context.Context, section, key, value st return nil } +func (c *ceph) ApplyCephOSDConfigOption(ctx context.Context, key, value string) error { + keyArgs := []string{} + switch key { + case "AllowCrimson": + keyArgs = []string{"osd", "set-allow-crimson", "--yes-i-really-mean-it"} + case "BackfillfullRatio": + keyArgs = []string{"osd", "set-backfillfull-ratio", value} + case "FullRatio": + keyArgs = []string{"osd", "set-full-ratio", value} + case "NearfullRatio": + keyArgs = []string{"osd", "set-nearfull-ratio", value} + case "RequireMinCompatClient": + keyArgs = []string{"osd", "set-require-min-compat-client", value} + default: + return errors.Errorf("unexpected key: `%s`", key) + } + + bin, args := mkCommand(c.binaryPath, keyArgs) + + cmd := exec.CommandContext(ctx, bin, args...) + cmd.Stderr = log.StandardLogger().WriterLevel(log.DebugLevel) + if err := cmd.Run(); err != nil { + return errors.Wrap(err, "error applying OSD configuration") + } + + return nil +} + func (c *ceph) ClusterReport(ctx context.Context) (models.ClusterReport, error) { buf := &bytes.Buffer{} bin, args := mkCommand(c.binaryPath, []string{"report", "--format=json"}) diff --git a/ceph/ceph_test.go b/ceph/ceph_test.go index 38a590a..8b9d48f 100644 --- a/ceph/ceph_test.go +++ b/ceph/ceph_test.go @@ -22,6 +22,23 @@ func TestApplyCephConfigOption(t *testing.T) { r.NoError(err) } +func TestApplyCephOSDConfigOption(t *testing.T) { + r := require.New(t) + + c := New("testdata/ceph_mock_ApplyCephOSDConfigOption") + err := c.ApplyCephOSDConfigOption(context.Background(), "AllowCrimson", "true") + r.NoError(err) +} + +func TestApplyCephOSDConfigOptionInvalidKey(t *testing.T) { + r := require.New(t) + + c := New("testdata/ceph_mock_ApplyCephOSDConfigOption") + err := c.ApplyCephOSDConfigOption(context.Background(), "key", "value") + r.Error(err) + r.Equal("unexpected key: `key`", err.Error()) +} + func TestClusterReport(t *testing.T) { r := require.New(t) diff --git a/ceph/config/spec/cephosdconfig/spec.go b/ceph/config/spec/cephosdconfig/spec.go new file mode 100644 index 0000000..eec8c56 --- /dev/null +++ b/ceph/config/spec/cephosdconfig/spec.go @@ -0,0 +1,22 @@ +package cephosdconfig + +import ( + "github.com/creasty/defaults" + "github.com/pkg/errors" + "gopkg.in/yaml.v3" + + "github.com/runityru/cephctl/models" +) + +func New(in []byte) (models.CephOSDConfig, error) { + spec := models.CephOSDConfig{} + if err := defaults.Set(&spec); err != nil { + return models.CephOSDConfig{}, errors.Wrap(err, "error setting default values") + } + + if err := yaml.Unmarshal(in, &spec); err != nil { + return models.CephOSDConfig{}, errors.Wrap(err, "error decoding spec file") + } + + return spec, nil +} diff --git a/ceph/config/spec/cephosdconfig/spec_test.go b/ceph/config/spec/cephosdconfig/spec_test.go new file mode 100644 index 0000000..4162b9f --- /dev/null +++ b/ceph/config/spec/cephosdconfig/spec_test.go @@ -0,0 +1,44 @@ +package cephosdconfig + +import ( + "os" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/runityru/cephctl/models" +) + +func TestNewValidConfig(t *testing.T) { + r := require.New(t) + + data, err := os.ReadFile("testdata/full.json") + r.NoError(err) + + cfg, err := New(data) + r.NoError(err) + r.Equal(models.CephOSDConfig{ + AllowCrimson: true, + NearfullRatio: 0.75, + BackfillfullRatio: 0.8, + FullRatio: 0.85, + RequireMinCompatClient: "squid", + }, cfg) +} + +func TestNewEmptyConfig(t *testing.T) { + r := require.New(t) + + data, err := os.ReadFile("testdata/empty.json") + r.NoError(err) + + cfg, err := New(data) + r.NoError(err) + r.Equal(models.CephOSDConfig{ + AllowCrimson: false, + NearfullRatio: 0.85, + BackfillfullRatio: 0.9, + FullRatio: 0.95, + RequireMinCompatClient: "reef", + }, cfg) +} diff --git a/ceph/config/spec/cephosdconfig/testdata/empty.json b/ceph/config/spec/cephosdconfig/testdata/empty.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/ceph/config/spec/cephosdconfig/testdata/empty.json @@ -0,0 +1 @@ +{} diff --git a/ceph/config/spec/cephosdconfig/testdata/full.json b/ceph/config/spec/cephosdconfig/testdata/full.json new file mode 100644 index 0000000..34fe84f --- /dev/null +++ b/ceph/config/spec/cephosdconfig/testdata/full.json @@ -0,0 +1,7 @@ +{ + "allow_crimson": true, + "nearfull_ratio": 0.75, + "backfillfull_ratio": 0.8, + "full_ratio": 0.85, + "require_min_compat_client": "squid" +} diff --git a/ceph/mock.go b/ceph/mock.go index d8782b0..739fe3a 100644 --- a/ceph/mock.go +++ b/ceph/mock.go @@ -23,6 +23,11 @@ func (m *Mock) ApplyCephConfigOption(ctx context.Context, section, key, value st return args.Error(0) } +func (m *Mock) ApplyCephOSDConfigOption(_ context.Context, key, value string) error { + args := m.Called(key, value) + return args.Error(0) +} + func (m *Mock) ClusterReport(ctx context.Context) (models.ClusterReport, error) { args := m.Called() return args.Get(0).(models.ClusterReport), args.Error(1) diff --git a/ceph/testdata/ceph_mock_ApplyCephOSDConfigOption b/ceph/testdata/ceph_mock_ApplyCephOSDConfigOption new file mode 100755 index 0000000..8cd6532 --- /dev/null +++ b/ceph/testdata/ceph_mock_ApplyCephOSDConfigOption @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +set -euo pipefail + +[[ "${@}" == "osd set-allow-crimson --yes-i-really-mean-it" ]] || exit 1 diff --git a/cmd/cephctl/main.go b/cmd/cephctl/main.go index 85260bd..29db8a6 100644 --- a/cmd/cephctl/main.go +++ b/cmd/cephctl/main.go @@ -12,6 +12,7 @@ import ( applyCmd "github.com/runityru/cephctl/commands/apply" diffCmd "github.com/runityru/cephctl/commands/diff" dumpCephConfigCmd "github.com/runityru/cephctl/commands/dump/cephconfig" + dumpCephOSDConfigCmd "github.com/runityru/cephctl/commands/dump/cephosdconfig" healthcheckCmd "github.com/runityru/cephctl/commands/healthcheck" "github.com/runityru/cephctl/differ" "github.com/runityru/cephctl/printer" @@ -57,8 +58,9 @@ var ( diffSpecFile = diff.Arg("filename", "Filename with configuration specification").Required().String() - dump = app.Command("dump", "Dump runtime configuration") - dumpCephConfig = dump.Command("cephconfig", "dump Ceph runtime configuration") + dump = app.Command("dump", "Dump runtime configuration") + dumpCephConfig = dump.Command("cephconfig", "dump Ceph runtime configuration") + dumpCephOSDConfig = dump.Command("cephosdconfig", "dump Ceph OSD configuration") healthcheck = app.Command("healthcheck", "Perform a cluster healthcheck and print report") @@ -107,7 +109,7 @@ func main() { } case dumpCephConfig.FullCommand(): - log.Debug("running dump command") + log.Debug("running dump cephconfig command") if err := dumpCephConfigCmd.DumpCephConfig(ctx, dumpCephConfigCmd.DumpCephConfigConfig{ Printer: prntr, Service: svc, @@ -115,6 +117,15 @@ func main() { panic(err) } + case dumpCephOSDConfig.FullCommand(): + log.Debug("running dump cephosdconfig command") + if err := dumpCephOSDConfigCmd.DumpCephOSDConfig(ctx, dumpCephOSDConfigCmd.DumpCephOSDConfigConfig{ + Printer: prntr, + Service: svc, + }); err != nil { + panic(err) + } + case healthcheck.FullCommand(): if err := healthcheckCmd.Healthcheck(ctx, healthcheckCmd.HealthcheckConfig{ Printer: prntr, diff --git a/commands/apply/apply.go b/commands/apply/apply.go index 68437cc..29ac6b1 100644 --- a/commands/apply/apply.go +++ b/commands/apply/apply.go @@ -8,6 +8,7 @@ import ( "github.com/runityru/cephctl/ceph/config/spec" "github.com/runityru/cephctl/ceph/config/spec/cephconfig" + "github.com/runityru/cephctl/ceph/config/spec/cephosdconfig" "github.com/runityru/cephctl/service" ) @@ -33,6 +34,16 @@ func Apply(ctx context.Context, ac ApplyConfig) error { return err } + case "cephosdconfig": + cfg, err := cephosdconfig.New(specData) + if err != nil { + return err + } + + if err := ac.Service.ApplyCephOSDConfig(ctx, cfg); err != nil { + return err + } + default: return errors.Errorf("unexpected specification kind: `%s`", kind) } diff --git a/commands/apply/apply_test.go b/commands/apply/apply_test.go index db9484b..3cd5432 100644 --- a/commands/apply/apply_test.go +++ b/commands/apply/apply_test.go @@ -10,7 +10,7 @@ import ( "github.com/runityru/cephctl/service" ) -func TestApply(t *testing.T) { +func TestApplyCephConfig(t *testing.T) { r := require.New(t) m := service.NewMock() @@ -28,3 +28,24 @@ func TestApply(t *testing.T) { }) r.NoError(err) } + +func TestApplyCephOSDConfig(t *testing.T) { + r := require.New(t) + + m := service.NewMock() + defer m.AssertExpectations(t) + + m.On("ApplyCephOSDConfig", models.CephOSDConfig{ + AllowCrimson: true, + BackfillfullRatio: 0.9, + FullRatio: 0.95, + NearfullRatio: 0.85, + RequireMinCompatClient: "luminous", + }).Return(nil).Once() + + err := Apply(context.Background(), ApplyConfig{ + Service: m, + SpecFile: "testdata/cephosdconfig.yaml", + }) + r.NoError(err) +} diff --git a/commands/apply/testdata/cephosdconfig.yaml b/commands/apply/testdata/cephosdconfig.yaml new file mode 100644 index 0000000..8bf87d3 --- /dev/null +++ b/commands/apply/testdata/cephosdconfig.yaml @@ -0,0 +1,8 @@ +--- +kind: CephOSDConfig +spec: + allow_crimson: true + backfillfull_ratio: 0.9 + full_ratio: 0.95 + nearfull_ratio: 0.85 + require_min_compat_client: luminous diff --git a/commands/diff/diff.go b/commands/diff/diff.go index 5be069c..91443c1 100644 --- a/commands/diff/diff.go +++ b/commands/diff/diff.go @@ -9,6 +9,7 @@ import ( "github.com/runityru/cephctl/ceph/config/spec" "github.com/runityru/cephctl/ceph/config/spec/cephconfig" + "github.com/runityru/cephctl/ceph/config/spec/cephosdconfig" "github.com/runityru/cephctl/models" "github.com/runityru/cephctl/printer" "github.com/runityru/cephctl/service" @@ -53,6 +54,25 @@ func Diff(ctx context.Context, ac DiffConfig) error { } } + case "cephosdconfig": + cfg, err := cephosdconfig.New(specData) + if err != nil { + return err + } + + changes, err := ac.Service.DiffCephOSDConfig(ctx, cfg) + if err != nil { + return err + } + + for _, change := range changes { + log.WithFields(log.Fields{ + "component": "command", + }).Tracef("change: %#v", change) + + ac.Printer.Yellow("~ %s %s -> %s", change.Key, change.OldValue, change.Value) + } + default: return errors.Errorf("unexpected specification kind: `%s`", kind) } diff --git a/commands/diff/diff_test.go b/commands/diff/diff_test.go index f2da02e..8d83ac9 100644 --- a/commands/diff/diff_test.go +++ b/commands/diff/diff_test.go @@ -12,7 +12,7 @@ import ( "github.com/runityru/cephctl/service" ) -func TestDiff(t *testing.T) { +func TestDiffCephConfig(t *testing.T) { r := require.New(t) m := service.NewMock() @@ -57,3 +57,36 @@ func TestDiff(t *testing.T) { }) r.NoError(err) } + +func TestDiffCephOSDConfig(t *testing.T) { + r := require.New(t) + + m := service.NewMock() + defer m.AssertExpectations(t) + + p := printer.NewMock() + defer p.AssertExpectations(t) + + m.On("DiffCephOSDConfig", models.CephOSDConfig{ + AllowCrimson: true, + BackfillfullRatio: 0.9, + FullRatio: 0.95, + NearfullRatio: 0.85, + RequireMinCompatClient: "luminous", + }).Return([]models.CephOSDConfigDifference{ + { + Key: "allow_crimson", + OldValue: "false", + Value: "true", + }, + }, nil).Once() + + p.On("Yellow", "~ %s %s -> %s", []any{"allow_crimson", "false", "true"}).Return().Once() + + err := Diff(context.Background(), DiffConfig{ + Printer: p, + Service: m, + SpecFile: "testdata/cephosdconfig.yaml", + }) + r.NoError(err) +} diff --git a/commands/diff/testdata/cephosdconfig.yaml b/commands/diff/testdata/cephosdconfig.yaml new file mode 100644 index 0000000..8bf87d3 --- /dev/null +++ b/commands/diff/testdata/cephosdconfig.yaml @@ -0,0 +1,8 @@ +--- +kind: CephOSDConfig +spec: + allow_crimson: true + backfillfull_ratio: 0.9 + full_ratio: 0.95 + nearfull_ratio: 0.85 + require_min_compat_client: luminous diff --git a/commands/dump/cephosdconfig/cephosdconfig.go b/commands/dump/cephosdconfig/cephosdconfig.go new file mode 100644 index 0000000..a38a57d --- /dev/null +++ b/commands/dump/cephosdconfig/cephosdconfig.go @@ -0,0 +1,41 @@ +package cephosdconfig + +import ( + "context" + + "gopkg.in/yaml.v3" + + "github.com/runityru/cephctl/models" + "github.com/runityru/cephctl/printer" + "github.com/runityru/cephctl/service" +) + +type DumpCephOSDConfigConfig struct { + Printer printer.Printer + Service service.Service +} + +func DumpCephOSDConfig(ctx context.Context, doc DumpCephOSDConfigConfig) error { + type outputSpec struct { + Kind string `yaml:"kind"` + Spec models.CephOSDConfig `yaml:"spec"` + } + + cfg, err := doc.Service.DumpOSDConfig(ctx) + if err != nil { + return err + } + + spec := outputSpec{ + Kind: "CephOSDConfig", + Spec: cfg, + } + + data, err := yaml.Marshal(spec) + if err != nil { + return err + } + + doc.Printer.Println(string(data)) + return nil +} diff --git a/commands/dump/cephosdconfig/cephosdconfig_test.go b/commands/dump/cephosdconfig/cephosdconfig_test.go new file mode 100644 index 0000000..0707451 --- /dev/null +++ b/commands/dump/cephosdconfig/cephosdconfig_test.go @@ -0,0 +1,40 @@ +package cephosdconfig + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/runityru/cephctl/models" + "github.com/runityru/cephctl/printer" + "github.com/runityru/cephctl/service" +) + +func TestDumpCephOSDConfig(t *testing.T) { + r := require.New(t) + + m := service.NewMock() + defer m.AssertExpectations(t) + + p := printer.NewMock() + defer p.AssertExpectations(t) + + m.On("DumpOSDConfig").Return(models.CephOSDConfig{ + AllowCrimson: true, + BackfillfullRatio: 0.9, + FullRatio: 0.95, + NearfullRatio: 0.85, + RequireMinCompatClient: "reef", + }, nil).Once() + + p.On("Println", []any{ + "kind: CephOSDConfig\nspec:\n allow_crimson: true\n backfillfull_ratio: 0.9\n full_ratio: 0.95\n nearfull_ratio: 0.85\n require_min_compat_client: reef\n", + }).Return().Once() + + err := DumpCephOSDConfig(context.Background(), DumpCephOSDConfigConfig{ + Printer: p, + Service: m, + }) + r.NoError(err) +} diff --git a/differ/differ.go b/differ/differ.go index 887e6b6..b045929 100644 --- a/differ/differ.go +++ b/differ/differ.go @@ -2,6 +2,7 @@ package differ import ( "context" + "strconv" "strings" "github.com/pkg/errors" @@ -16,6 +17,7 @@ const flattenMapSeparator = ":::" type Differ interface { DiffCephConfig(ctx context.Context, from, to models.CephConfig) ([]models.CephConfigDifference, error) + DiffCephOSDConfig(ctx context.Context, from, to models.CephOSDConfig) ([]models.CephOSDConfigDifference, error) } type differ struct{} @@ -106,3 +108,55 @@ func (d *differ) DiffCephConfig(ctx context.Context, from, to models.CephConfig) return changes, nil } + +func (d *differ) DiffCephOSDConfig(ctx context.Context, from, to models.CephOSDConfig) ([]models.CephOSDConfigDifference, error) { + changelog, err := diff.Diff(from, to) + if err != nil { + return nil, errors.Wrap(err, "error comparing current and desired configuration") + } + + changes := []models.CephOSDConfigDifference{} + for _, change := range changelog { + var ( + oldValue string + newValue string + ) + + switch change.Type { + case diff.UPDATE: + switch v := change.From.(type) { + case bool: + oldValue = strconv.FormatBool(v) + case float32: + oldValue = strconv.FormatFloat(float64(v), 'f', 2, 32) + case string: + oldValue = v + default: + log.Warnf("unexpected old value type: got %T", v) + } + + switch v := change.To.(type) { + case bool: + newValue = strconv.FormatBool(v) + case float32: + newValue = strconv.FormatFloat(float64(v), 'f', 2, 32) + case string: + newValue = v + default: + log.Warnf("unexpected new value type: got %T", v) + } + + changes = append(changes, models.CephOSDConfigDifference{ + Key: change.Path[0], + OldValue: oldValue, + Value: newValue, + }) + + default: + log.Warnf("unexpected change type: `%s`", change.Type) + break + } + } + + return changes, nil +} diff --git a/differ/differ_test.go b/differ/differ_test.go index 3c5a4eb..6ccc7f9 100644 --- a/differ/differ_test.go +++ b/differ/differ_test.go @@ -106,6 +106,81 @@ func (s *differTestSuite) TestDiffCephConfig() { } } +func (s *differTestSuite) TestDiffCephOSDConfig() { + type testCase struct { + name string + from models.CephOSDConfig + to models.CephOSDConfig + expOut []models.CephOSDConfigDifference + expError error + } + + tcs := []testCase{ + { + name: "ordinary config", + from: models.CephOSDConfig{ + AllowCrimson: false, + NearfullRatio: 0.85, + BackfillfullRatio: 0.9, + FullRatio: 0.95, + RequireMinCompatClient: "luminous", + }, + to: models.CephOSDConfig{ + AllowCrimson: true, + NearfullRatio: 0.9, + BackfillfullRatio: 0.95, + FullRatio: 0.98, + RequireMinCompatClient: "reef", + }, + expOut: []models.CephOSDConfigDifference{ + { + Key: "allow_crimson", + OldValue: "false", + Value: "true", + }, + { + Key: "nearfull_ratio", + OldValue: "0.85", + Value: "0.90", + }, + { + Key: "backfillfull_ratio", + OldValue: "0.90", + Value: "0.95", + }, + { + Key: "full_ratio", + OldValue: "0.95", + Value: "0.98", + }, + { + Key: "require_min_compat_client", + OldValue: "luminous", + Value: "reef", + }, + }, + }, + } + + for _, tc := range tcs { + s.T().Run(tc.name, func(t *testing.T) { + r := require.New(t) + + diff, err := s.differ.DiffCephOSDConfig(s.ctx, tc.from, tc.to) + if tc.expError != nil { + r.Error(err) + r.Equal(tc.expError.Error(), err.Error()) + } else { + r.NoError(err) + r.NotNil(diff) + r.ElementsMatch(tc.expOut, diff) + } + }) + } +} + +// Definitions ... + type differTestSuite struct { suite.Suite diff --git a/differ/mock.go b/differ/mock.go index 5bd9887..a042887 100644 --- a/differ/mock.go +++ b/differ/mock.go @@ -22,3 +22,8 @@ func (m *Mock) DiffCephConfig(ctx context.Context, from, to models.CephConfig) ( args := m.Called(from, to) return args.Get(0).([]models.CephConfigDifference), args.Error(1) } + +func (m *Mock) DiffCephOSDConfig(ctx context.Context, from, to models.CephOSDConfig) ([]models.CephOSDConfigDifference, error) { + args := m.Called(from, to) + return args.Get(0).([]models.CephOSDConfigDifference), args.Error(1) +} diff --git a/go.mod b/go.mod index 0af871e..90ae2a2 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.22.2 require ( github.com/alecthomas/kingpin/v2 v2.4.0 + github.com/creasty/defaults v1.7.0 github.com/fatih/color v1.17.0 github.com/pkg/errors v0.9.1 github.com/r3labs/diff/v3 v3.0.1 diff --git a/go.sum b/go.sum index ba8cf67..1484641 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjH github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= +github.com/creasty/defaults v1.7.0 h1:eNdqZvc5B509z18lD8yc212CAqJNvfT1Jq6L8WowdBA= +github.com/creasty/defaults v1.7.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/models/cephosdconfig.go b/models/cephosdconfig.go new file mode 100644 index 0000000..ae8ee50 --- /dev/null +++ b/models/cephosdconfig.go @@ -0,0 +1,17 @@ +package models + +type CephOSDConfig struct { + AllowCrimson bool `yaml:"allow_crimson" diff:"allow_crimson" default:"false"` + BackfillfullRatio float32 `yaml:"backfillfull_ratio" diff:"backfillfull_ratio" default:"0.9"` + FullRatio float32 `yaml:"full_ratio" diff:"full_ratio" default:"0.95"` + NearfullRatio float32 `yaml:"nearfull_ratio" diff:"nearfull_ratio" default:"0.85"` + RequireMinCompatClient string `yaml:"require_min_compat_client" diff:"require_min_compat_client" default:"reef"` +} + +type CephOSDConfigDifferenceKind string + +type CephOSDConfigDifference struct { + Key string + OldValue string + Value string +} diff --git a/service/mock.go b/service/mock.go index f24c6ee..7f8b52c 100644 --- a/service/mock.go +++ b/service/mock.go @@ -24,11 +24,21 @@ func (m *Mock) ApplyCephConfig(_ context.Context, cfg models.CephConfig) error { return args.Error(0) } +func (m *Mock) ApplyCephOSDConfig(ctx context.Context, cfg models.CephOSDConfig) error { + args := m.Called(cfg) + return args.Error(0) +} + func (m *Mock) DiffCephConfig(_ context.Context, cfg models.CephConfig) ([]models.CephConfigDifference, error) { args := m.Called(cfg) return args.Get(0).([]models.CephConfigDifference), args.Error(1) } +func (m *Mock) DiffCephOSDConfig(ctx context.Context, cfg models.CephOSDConfig) ([]models.CephOSDConfigDifference, error) { + args := m.Called(cfg) + return args.Get(0).([]models.CephOSDConfigDifference), args.Error(1) +} + func (m *Mock) CheckClusterHealth(context.Context, []clusterHealth.ClusterHealthCheck) ([]models.ClusterHealthIndicator, error) { args := m.Called() return args.Get(0).([]models.ClusterHealthIndicator), args.Error(1) @@ -38,3 +48,8 @@ func (m *Mock) DumpConfig(context.Context) (models.CephConfig, error) { args := m.Called() return args.Get(0).(models.CephConfig), args.Error(1) } + +func (m *Mock) DumpOSDConfig(context.Context) (models.CephOSDConfig, error) { + args := m.Called() + return args.Get(0).(models.CephOSDConfig), args.Error(1) +} diff --git a/service/service.go b/service/service.go index a2941a2..7ca0e53 100644 --- a/service/service.go +++ b/service/service.go @@ -14,9 +14,12 @@ import ( type Service interface { ApplyCephConfig(ctx context.Context, cfg models.CephConfig) error + ApplyCephOSDConfig(ctx context.Context, cfg models.CephOSDConfig) error DiffCephConfig(ctx context.Context, cfg models.CephConfig) ([]models.CephConfigDifference, error) + DiffCephOSDConfig(ctx context.Context, cfg models.CephOSDConfig) ([]models.CephOSDConfigDifference, error) CheckClusterHealth(ctx context.Context, checks []clusterHealth.ClusterHealthCheck) ([]models.ClusterHealthIndicator, error) DumpConfig(ctx context.Context) (models.CephConfig, error) + DumpOSDConfig(ctx context.Context) (models.CephOSDConfig, error) } type service struct { @@ -58,6 +61,24 @@ func (s *service) ApplyCephConfig(ctx context.Context, cfg models.CephConfig) er return nil } +func (s *service) ApplyCephOSDConfig(ctx context.Context, cfg models.CephOSDConfig) error { + changes, err := s.DiffCephOSDConfig(ctx, cfg) + if err != nil { + return errors.Wrap(err, "error comparing current and desired configuration") + } + + log.WithFields(log.Fields{ + "component": "service", + }).Tracef("changelog: %#v", changes) + + for _, change := range changes { + if err := s.c.ApplyCephOSDConfigOption(ctx, change.Key, change.Value); err != nil { + return err + } + } + return nil +} + func (s *service) CheckClusterHealth(ctx context.Context, checks []clusterHealth.ClusterHealthCheck) ([]models.ClusterHealthIndicator, error) { cr, err := s.c.ClusterReport(ctx) if err != nil { @@ -93,6 +114,30 @@ func (s *service) DiffCephConfig(ctx context.Context, cfg models.CephConfig) ([] return s.d.DiffCephConfig(ctx, src, cfg) } +func (s *service) DiffCephOSDConfig(ctx context.Context, cfg models.CephOSDConfig) ([]models.CephOSDConfigDifference, error) { + src, err := s.DumpOSDConfig(ctx) + if err != nil { + return nil, errors.Wrap(err, "error retrieving current configuration") + } + + return s.d.DiffCephOSDConfig(ctx, src, cfg) +} + func (s *service) DumpConfig(ctx context.Context) (models.CephConfig, error) { return s.c.DumpConfig(ctx) } + +func (s *service) DumpOSDConfig(ctx context.Context) (models.CephOSDConfig, error) { + rep, err := s.c.ClusterReport(ctx) + if err != nil { + return models.CephOSDConfig{}, errors.Wrap(err, "error collecting cluster report") + } + + return models.CephOSDConfig{ + AllowCrimson: rep.AllowCrimson, + BackfillfullRatio: rep.BackfillfullRatio, + FullRatio: rep.FullRatio, + NearfullRatio: rep.NearfullRatio, + RequireMinCompatClient: rep.RequireMinCompatClient, + }, nil +} diff --git a/service/service_test.go b/service/service_test.go index 22269e7..bec93d9 100644 --- a/service/service_test.go +++ b/service/service_test.go @@ -2,6 +2,7 @@ package service import ( "context" + "strconv" "testing" "time" @@ -69,6 +70,35 @@ func (s *serviceTestSuite) TestApplyCephConfig() { s.Require().NoError(err) } +func (s *serviceTestSuite) TestApplyCephOSDConfig() { + newCfg := models.CephOSDConfig{ + AllowCrimson: true, + BackfillfullRatio: 0.95, + FullRatio: 0.98, + NearfullRatio: 0.9, + RequireMinCompatClient: "reef", + } + + call1 := s.cephMock.On("ClusterReport").Return(models.ClusterReport{}, nil).Once() + call2 := s.differMock.On("DiffCephOSDConfig", models.CephOSDConfig{}, newCfg).Return([]models.CephOSDConfigDifference{ + { + Key: "blah", + OldValue: "932", + Value: "123", + }, + { + Key: "some_key", + OldValue: "123", + Value: "456", + }, + }, nil).NotBefore(call1).Once() + call3 := s.cephMock.On("ApplyCephOSDConfigOption", "blah", "123").Return(nil).NotBefore(call2).Once() + s.cephMock.On("ApplyCephOSDConfigOption", "some_key", "456").Return(nil).NotBefore(call3).Once() + + err := s.svc.ApplyCephOSDConfig(s.ctx, newCfg) + s.Require().NoError(err) +} + func (s *serviceTestSuite) TestCheckClusterHealth() { s.cephMock.On("ClusterReport").Return(models.ClusterReport{ HealthStatus: models.ClusterStatusHealthOK, @@ -183,6 +213,90 @@ func (s *serviceTestSuite) TestDiffCephConfig() { s.Require().ElementsMatch(result, diff) } +func (s *serviceTestSuite) TestDiffCephOSDConfig() { + src := models.CephOSDConfig{ + AllowCrimson: false, + NearfullRatio: 0.85, + BackfillfullRatio: 0.90, + FullRatio: 0.95, + RequireMinCompatClient: "luminous", + } + + dst := models.CephOSDConfig{ + AllowCrimson: true, + NearfullRatio: 0.89, + BackfillfullRatio: 0.92, + FullRatio: 0.97, + RequireMinCompatClient: "reef", + } + + s.cephMock.On("ClusterReport").Return(models.ClusterReport{ + AllowCrimson: false, + NearfullRatio: 0.85, + BackfillfullRatio: 0.90, + FullRatio: 0.95, + RequireMinCompatClient: "luminous", + }, nil).Once() + + s.differMock.On("DiffCephOSDConfig", src, dst).Return([]models.CephOSDConfigDifference{ + { + Key: "AllowCrimson", + OldValue: "false", + Value: "true", + }, + { + Key: "NearfullRatio", + OldValue: strconv.FormatFloat(0.85, 'f', 2, 32), + Value: strconv.FormatFloat(0.89, 'f', 2, 32), + }, + { + Key: "BackfillfullRatio", + OldValue: strconv.FormatFloat(0.90, 'f', 2, 32), + Value: strconv.FormatFloat(0.92, 'f', 2, 32), + }, + { + Key: "FullRatio", + OldValue: strconv.FormatFloat(0.95, 'f', 2, 32), + Value: strconv.FormatFloat(0.97, 'f', 2, 32), + }, + { + Key: "RequireMinCompatClient", + OldValue: "luminous", + Value: "reef", + }, + }, nil).Once() + + diff, err := s.svc.DiffCephOSDConfig(s.ctx, dst) + s.Require().NoError(err) + s.Require().ElementsMatch([]models.CephOSDConfigDifference{ + { + Key: "AllowCrimson", + OldValue: "false", + Value: "true", + }, + { + Key: "NearfullRatio", + OldValue: strconv.FormatFloat(0.85, 'f', 2, 32), + Value: strconv.FormatFloat(0.89, 'f', 2, 32), + }, + { + Key: "BackfillfullRatio", + OldValue: strconv.FormatFloat(0.90, 'f', 2, 32), + Value: strconv.FormatFloat(0.92, 'f', 2, 32), + }, + { + Key: "FullRatio", + OldValue: strconv.FormatFloat(0.95, 'f', 2, 32), + Value: strconv.FormatFloat(0.97, 'f', 2, 32), + }, + { + Key: "RequireMinCompatClient", + OldValue: "luminous", + Value: "reef", + }, + }, diff) +} + func (s *serviceTestSuite) TestDumpConfig() { s.cephMock.On("DumpConfig").Return(models.CephConfig{ "osd": { @@ -199,6 +313,26 @@ func (s *serviceTestSuite) TestDumpConfig() { }, cfg) } +func (s *serviceTestSuite) TestDumpOSDConfig() { + s.cephMock.On("ClusterReport").Return(models.ClusterReport{ + AllowCrimson: true, + NearfullRatio: 0.85, + BackfillfullRatio: 0.9, + FullRatio: 0.95, + RequireMinCompatClient: "reef", + }, nil).Once() + + cfg, err := s.svc.DumpOSDConfig(s.ctx) + s.Require().NoError(err) + s.Require().Equal(models.CephOSDConfig{ + AllowCrimson: true, + NearfullRatio: 0.85, + BackfillfullRatio: 0.9, + FullRatio: 0.95, + RequireMinCompatClient: "reef", + }, cfg) +} + // Definitions ... type serviceTestSuite struct { From 77c097cf0716b8302f00e1d7b235b0b583b4ed8a Mon Sep 17 00:00:00 2001 From: Igor Shishkin Date: Wed, 26 Jun 2024 09:25:07 +0300 Subject: [PATCH 5/6] Add multi documents support (#53) Allows to describe Ceph cluster configuration in single file just like: ```yaml --- kind: CephConfig spec: // ... --- kind: CephOSDConfig spec: // ..... ``` Depends on #51 since uses kinds defined in that PR --------- Signed-off-by: Igor Shishkin --- ceph/config/spec/spec.go | 53 ++++++------ ceph/config/spec/spec_test.go | 22 +++-- .../sample_NewFromDescriptionJSON.json | 11 --- .../sample_NewFromDescriptionMulti.yaml | 11 +++ ...l => sample_NewFromDescriptionSingle.yaml} | 0 commands/apply/apply.go | 48 +++++------ commands/diff/diff.go | 82 ++++++++++--------- differ/differ.go | 1 - 8 files changed, 119 insertions(+), 109 deletions(-) delete mode 100644 ceph/config/spec/testdata/sample_NewFromDescriptionJSON.json create mode 100644 ceph/config/spec/testdata/sample_NewFromDescriptionMulti.yaml rename ceph/config/spec/testdata/{sample_NewFromDescriptionYAML.yaml => sample_NewFromDescriptionSingle.yaml} (100%) diff --git a/ceph/config/spec/spec.go b/ceph/config/spec/spec.go index 4b1200b..143ed41 100644 --- a/ceph/config/spec/spec.go +++ b/ceph/config/spec/spec.go @@ -2,50 +2,51 @@ package spec import ( "encoding/json" + "io" "os" - "path/filepath" - "strings" "github.com/pkg/errors" yaml "gopkg.in/yaml.v3" ) -type description struct { +type Description struct { Kind string `json:"kind"` Spec json.RawMessage `json:"spec"` } -func NewFromDescription(filename string) (string, json.RawMessage, error) { - desc := description{} +type yamlIntermediate struct { + Kind string `yaml:"kind"` + Spec any `yaml:"spec"` +} - data, err := os.ReadFile(filename) +func NewFromDescription(filename string) ([]Description, error) { + fp, err := os.Open(filename) if err != nil { - return "", nil, errors.Wrap(err, "error reading configuration file") + return nil, errors.Wrap(err, "error opening spec file") } + defer fp.Close() - switch strings.ToLower(filepath.Ext(filename)) { - case ".yml", ".yaml": - type intermediate struct { - Kind string `yaml:"kind"` - Spec any `yaml:"spec"` - } - - d := intermediate{} - err := yaml.Unmarshal(data, &d) + docs := []Description{} + dec := yaml.NewDecoder(fp) + for { + v := yamlIntermediate{} + err := dec.Decode(&v) if err != nil { - return "", nil, errors.Wrap(err, "error unmarshaling intermediate configuration") + if errors.Is(err, io.EOF) { + break + } + return nil, errors.Wrap(err, "error unmarshaling document") } - - spec, err := json.Marshal(d.Spec) + spec, err := json.Marshal(v.Spec) if err != nil { - return "", nil, errors.Wrap(err, "error marshaling intermediate configuration") + return nil, errors.Wrap(err, "error marshaling intermediate data structure") } - return d.Kind, json.RawMessage(spec), nil - case ".json": - // skip since supported natively - default: - return "", nil, errors.Errorf("unexpected file format: `%s`", filepath.Ext(filename)) + + docs = append(docs, Description{ + Kind: v.Kind, + Spec: json.RawMessage(spec), + }) } - return desc.Kind, desc.Spec, json.Unmarshal(data, &desc) + return docs, nil } diff --git a/ceph/config/spec/spec_test.go b/ceph/config/spec/spec_test.go index 733eb40..11db4e6 100644 --- a/ceph/config/spec/spec_test.go +++ b/ceph/config/spec/spec_test.go @@ -6,20 +6,26 @@ import ( "github.com/stretchr/testify/require" ) -func TestNewFromDescriptionYAML(t *testing.T) { +func TestNewFromDescriptionSingle(t *testing.T) { r := require.New(t) - kind, spec, err := NewFromDescription("testdata/sample_NewFromDescriptionYAML.yaml") + descs, err := NewFromDescription("testdata/sample_NewFromDescriptionSingle.yaml") r.NoError(err) - r.Equal("CephConfig", kind) - r.JSONEq(`{"global":{"rbd_cache":"true"},"osd":{"rocksdb_perf":"true"}}`, string(spec)) + r.Len(descs, 1) + r.Equal("CephConfig", descs[0].Kind) + r.JSONEq(`{"global":{"rbd_cache":"true"},"osd":{"rocksdb_perf":"true"}}`, string(descs[0].Spec)) } -func TestNewFromDescriptionJSON(t *testing.T) { +func TestNewFromDescriptionMulti(t *testing.T) { r := require.New(t) - kind, spec, err := NewFromDescription("testdata/sample_NewFromDescriptionJSON.json") + descs, err := NewFromDescription("testdata/sample_NewFromDescriptionMulti.yaml") r.NoError(err) - r.Equal("CephConfig", kind) - r.JSONEq(`{"global":{"rbd_cache":"true"},"osd":{"rocksdb_perf":"true"}}`, string(spec)) + r.Len(descs, 2) + + r.Equal("CephConfig", descs[0].Kind) + r.JSONEq(`{"global":{"rbd_cache":"true"},"osd":{"rocksdb_perf":"true"}}`, string(descs[0].Spec)) + + r.Equal("CephOSDConfig", descs[1].Kind) + r.JSONEq(`{"allow_crimson":true}`, string(descs[1].Spec)) } diff --git a/ceph/config/spec/testdata/sample_NewFromDescriptionJSON.json b/ceph/config/spec/testdata/sample_NewFromDescriptionJSON.json deleted file mode 100644 index 0d29e83..0000000 --- a/ceph/config/spec/testdata/sample_NewFromDescriptionJSON.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "kind": "CephConfig", - "spec": { - "global": { - "rbd_cache": "true" - }, - "osd": { - "rocksdb_perf": "true" - } - } -} diff --git a/ceph/config/spec/testdata/sample_NewFromDescriptionMulti.yaml b/ceph/config/spec/testdata/sample_NewFromDescriptionMulti.yaml new file mode 100644 index 0000000..ae8f5a0 --- /dev/null +++ b/ceph/config/spec/testdata/sample_NewFromDescriptionMulti.yaml @@ -0,0 +1,11 @@ +--- +kind: CephConfig +spec: + global: + rbd_cache: "true" + osd: + rocksdb_perf: "true" +--- +kind: CephOSDConfig +spec: + allow_crimson: true diff --git a/ceph/config/spec/testdata/sample_NewFromDescriptionYAML.yaml b/ceph/config/spec/testdata/sample_NewFromDescriptionSingle.yaml similarity index 100% rename from ceph/config/spec/testdata/sample_NewFromDescriptionYAML.yaml rename to ceph/config/spec/testdata/sample_NewFromDescriptionSingle.yaml diff --git a/commands/apply/apply.go b/commands/apply/apply.go index 29ac6b1..1d68dc6 100644 --- a/commands/apply/apply.go +++ b/commands/apply/apply.go @@ -18,34 +18,36 @@ type ApplyConfig struct { } func Apply(ctx context.Context, ac ApplyConfig) error { - kind, specData, err := spec.NewFromDescription(ac.SpecFile) + descs, err := spec.NewFromDescription(ac.SpecFile) if err != nil { return err } - switch strings.ToLower(kind) { - case "cephconfig": - cfg, err := cephconfig.New(specData) - if err != nil { - return err + for _, desc := range descs { + switch strings.ToLower(desc.Kind) { + case "cephconfig": + cfg, err := cephconfig.New(desc.Spec) + if err != nil { + return err + } + + if err := ac.Service.ApplyCephConfig(ctx, cfg); err != nil { + return err + } + + case "cephosdconfig": + cfg, err := cephosdconfig.New(desc.Spec) + if err != nil { + return err + } + + if err := ac.Service.ApplyCephOSDConfig(ctx, cfg); err != nil { + return err + } + + default: + return errors.Errorf("unexpected specification kind: `%s`", desc.Kind) } - - if err := ac.Service.ApplyCephConfig(ctx, cfg); err != nil { - return err - } - - case "cephosdconfig": - cfg, err := cephosdconfig.New(specData) - if err != nil { - return err - } - - if err := ac.Service.ApplyCephOSDConfig(ctx, cfg); err != nil { - return err - } - - default: - return errors.Errorf("unexpected specification kind: `%s`", kind) } return nil diff --git a/commands/diff/diff.go b/commands/diff/diff.go index 91443c1..f20d712 100644 --- a/commands/diff/diff.go +++ b/commands/diff/diff.go @@ -22,59 +22,61 @@ type DiffConfig struct { } func Diff(ctx context.Context, ac DiffConfig) error { - kind, specData, err := spec.NewFromDescription(ac.SpecFile) + descs, err := spec.NewFromDescription(ac.SpecFile) if err != nil { return err } - switch strings.ToLower(kind) { - case "cephconfig": - cfg, err := cephconfig.New(specData) - if err != nil { - return err - } + for _, desc := range descs { + switch strings.ToLower(desc.Kind) { + case "cephconfig": + cfg, err := cephconfig.New(desc.Spec) + if err != nil { + return err + } - changes, err := ac.Service.DiffCephConfig(ctx, cfg) - if err != nil { - return err - } + changes, err := ac.Service.DiffCephConfig(ctx, cfg) + if err != nil { + return err + } - for _, change := range changes { - log.WithFields(log.Fields{ - "component": "command", - }).Tracef("change: %#v", change) + for _, change := range changes { + log.WithFields(log.Fields{ + "component": "command", + }).Tracef("change: %#v", change) - switch change.Kind { - case models.CephConfigDifferenceKindAdd: - ac.Printer.Green("+ %s %s %s", change.Section, change.Key, *change.Value) - case models.CephConfigDifferenceKindChange: - ac.Printer.Yellow("~ %s %s %s -> %s", change.Section, change.Key, *change.OldValue, *change.Value) - case models.CephConfigDifferenceKindRemove: - ac.Printer.Red("- %s %s", change.Section, change.Key) + switch change.Kind { + case models.CephConfigDifferenceKindAdd: + ac.Printer.Green("+ %s %s %s", change.Section, change.Key, *change.Value) + case models.CephConfigDifferenceKindChange: + ac.Printer.Yellow("~ %s %s %s -> %s", change.Section, change.Key, *change.OldValue, *change.Value) + case models.CephConfigDifferenceKindRemove: + ac.Printer.Red("- %s %s", change.Section, change.Key) + } } - } - case "cephosdconfig": - cfg, err := cephosdconfig.New(specData) - if err != nil { - return err - } + case "cephosdconfig": + cfg, err := cephosdconfig.New(desc.Spec) + if err != nil { + return err + } - changes, err := ac.Service.DiffCephOSDConfig(ctx, cfg) - if err != nil { - return err - } + changes, err := ac.Service.DiffCephOSDConfig(ctx, cfg) + if err != nil { + return err + } - for _, change := range changes { - log.WithFields(log.Fields{ - "component": "command", - }).Tracef("change: %#v", change) + for _, change := range changes { + log.WithFields(log.Fields{ + "component": "command", + }).Tracef("change: %#v", change) - ac.Printer.Yellow("~ %s %s -> %s", change.Key, change.OldValue, change.Value) - } + ac.Printer.Yellow("~ %s %s -> %s", change.Key, change.OldValue, change.Value) + } - default: - return errors.Errorf("unexpected specification kind: `%s`", kind) + default: + return errors.Errorf("unexpected specification kind: `%s`", desc.Kind) + } } return nil diff --git a/differ/differ.go b/differ/differ.go index b045929..f059ac4 100644 --- a/differ/differ.go +++ b/differ/differ.go @@ -121,7 +121,6 @@ func (d *differ) DiffCephOSDConfig(ctx context.Context, from, to models.CephOSDC oldValue string newValue string ) - switch change.Type { case diff.UPDATE: switch v := change.From.(type) { From a5bc3938a1d2c0412b59c9d6372a3083b9960d84 Mon Sep 17 00:00:00 2001 From: Igor Shishkin Date: Wed, 26 Jun 2024 09:32:11 +0300 Subject: [PATCH 6/6] Add clean diff indication for multidoc specs (#54) Just like this: ``` CephConfig: ~ global cephx_sign_messages true -> false CephOSDConfig: ~ require_min_compat_client luminous -> reef ``` Depends on #53 Signed-off-by: Igor Shishkin --- commands/diff/diff.go | 8 ++++ commands/diff/diff_test.go | 63 ++++++++++++++++++++++++++++ commands/diff/testdata/multidoc.yaml | 13 ++++++ printer/mock.go | 4 ++ printer/printer.go | 5 +++ 5 files changed, 93 insertions(+) create mode 100644 commands/diff/testdata/multidoc.yaml diff --git a/commands/diff/diff.go b/commands/diff/diff.go index f20d712..b5d704b 100644 --- a/commands/diff/diff.go +++ b/commands/diff/diff.go @@ -40,6 +40,10 @@ func Diff(ctx context.Context, ac DiffConfig) error { return err } + if len(descs) > 1 && len(changes) > 0 { + ac.Printer.Printf("%s:\n", desc.Kind) + } + for _, change := range changes { log.WithFields(log.Fields{ "component": "command", @@ -66,6 +70,10 @@ func Diff(ctx context.Context, ac DiffConfig) error { return err } + if len(descs) > 1 && len(changes) > 0 { + ac.Printer.Printf("%s:\n", desc.Kind) + } + for _, change := range changes { log.WithFields(log.Fields{ "component": "command", diff --git a/commands/diff/diff_test.go b/commands/diff/diff_test.go index 8d83ac9..3c59093 100644 --- a/commands/diff/diff_test.go +++ b/commands/diff/diff_test.go @@ -90,3 +90,66 @@ func TestDiffCephOSDConfig(t *testing.T) { }) r.NoError(err) } + +func TestDiffMultidoc(t *testing.T) { + r := require.New(t) + + m := service.NewMock() + defer m.AssertExpectations(t) + + p := printer.NewMock() + defer p.AssertExpectations(t) + + m.On("DiffCephConfig", models.CephConfig{ + "global": { + "test": "value", + }, + }).Return([]models.CephConfigDifference{ + { + Kind: models.CephConfigDifferenceKindAdd, + Section: "mon", + Key: "test_key", + Value: ptr.String("value"), + }, + { + Kind: models.CephConfigDifferenceKindChange, + Section: "osd.3", + Key: "test_key", + OldValue: ptr.String("old_value"), + Value: ptr.String("value"), + }, + { + Kind: models.CephConfigDifferenceKindRemove, + Section: "osd", + Key: "test_key", + }, + }, nil).Once() + + m.On("DiffCephOSDConfig", models.CephOSDConfig{ + AllowCrimson: true, + BackfillfullRatio: 0.9, + FullRatio: 0.95, + NearfullRatio: 0.85, + RequireMinCompatClient: "luminous", + }).Return([]models.CephOSDConfigDifference{ + { + Key: "allow_crimson", + OldValue: "false", + Value: "true", + }, + }, nil).Once() + + call1 := p.On("Printf", "%s:\n", []any{"CephConfig"}).Return().Once() + call2 := p.On("Green", "+ %s %s %s", []any{"mon", "test_key", "value"}).Return().NotBefore(call1).Once() + call3 := p.On("Yellow", "~ %s %s %s -> %s", []any{"osd.3", "test_key", "old_value", "value"}).Return().NotBefore(call2).Once() + call4 := p.On("Red", "- %s %s", []any{"osd", "test_key"}).Return().NotBefore(call3).Once() + call5 := p.On("Printf", "%s:\n", []any{"CephOSDConfig"}).Return().NotBefore(call4).Once() + p.On("Yellow", "~ %s %s -> %s", []any{"allow_crimson", "false", "true"}).Return().NotBefore(call5).Once() + + err := Diff(context.Background(), DiffConfig{ + Printer: p, + Service: m, + SpecFile: "testdata/multidoc.yaml", + }) + r.NoError(err) +} diff --git a/commands/diff/testdata/multidoc.yaml b/commands/diff/testdata/multidoc.yaml new file mode 100644 index 0000000..5172e26 --- /dev/null +++ b/commands/diff/testdata/multidoc.yaml @@ -0,0 +1,13 @@ +--- +kind: CephConfig +spec: + global: + test: value +--- +kind: CephOSDConfig +spec: + allow_crimson: true + backfillfull_ratio: 0.9 + full_ratio: 0.95 + nearfull_ratio: 0.85 + require_min_compat_client: luminous diff --git a/printer/mock.go b/printer/mock.go index d5520cc..7f8039c 100644 --- a/printer/mock.go +++ b/printer/mock.go @@ -20,6 +20,10 @@ func (m *Mock) HiRed(format string, a ...any) { m.Called(format, a) } +func (m *Mock) Printf(format string, a ...any) { + m.Called(format, a) +} + func (m *Mock) Println(a ...any) { m.Called(a) } diff --git a/printer/printer.go b/printer/printer.go index d3fee4f..50abfc2 100644 --- a/printer/printer.go +++ b/printer/printer.go @@ -9,6 +9,7 @@ import ( type Printer interface { Green(format string, a ...any) HiRed(format string, a ...any) + Printf(format string, a ...any) Println(a ...any) Red(format string, a ...any) Yellow(format string, a ...any) @@ -30,6 +31,10 @@ func (p *printer) HiRed(format string, a ...any) { color.HiRed(format, a...) } +func (p *printer) Printf(format string, a ...any) { + fmt.Printf(format, a...) +} + func (p *printer) Println(a ...any) { fmt.Println(a...) }