From e2b5701a36d30895a10a9a2e4b64fc0b354694cb Mon Sep 17 00:00:00 2001 From: Joshua Blanch Date: Wed, 13 May 2026 18:53:30 +0000 Subject: [PATCH 1/2] cli: add cobra CLI move server startup behind a cobra command tree. helps deploy ceph-api as a single binary and also later to add commands to query endpoints or create users/keys Signed-off-by: Joshua Blanch --- cmd/ceph-api/main.go | 42 ++--------------------- cmd/ceph-api/root.go | 30 +++++++++++++++++ cmd/ceph-api/serve.go | 64 ++++++++++++++++++++++++++++++++++++ cmd/ceph-api/version.go | 18 ++++++++++ cmd/ceph-api/version_test.go | 30 +++++++++++++++++ go.mod | 2 +- pkg/rados/production_conn.go | 11 ++++--- 7 files changed, 152 insertions(+), 45 deletions(-) create mode 100644 cmd/ceph-api/root.go create mode 100644 cmd/ceph-api/serve.go create mode 100644 cmd/ceph-api/version.go create mode 100644 cmd/ceph-api/version_test.go diff --git a/cmd/ceph-api/main.go b/cmd/ceph-api/main.go index edbf6f1..42d601f 100644 --- a/cmd/ceph-api/main.go +++ b/cmd/ceph-api/main.go @@ -1,55 +1,19 @@ package main import ( - "context" - "flag" "os" - "os/signal" - "syscall" - "github.com/clyso/ceph-api/pkg/app" - "github.com/clyso/ceph-api/pkg/config" - "github.com/rs/zerolog" stdlog "github.com/rs/zerolog/log" ) // this information will be collected when built, by -ldflags="-X 'main.version=$(tag)' -X 'main.commit=$(commit)'". var ( - version = "development" - commit = "not set" - configPath = flag.String("config", "", "set path to config directory") - configOverridePath = flag.String("config-override", "", "set path to config override directory") + version = "development" + commit = "not set" ) func main() { - flag.Parse() - var configs []config.Src - if configPath != nil && *configPath != "" { - configs = append(configs, config.Path(*configPath)) - } - if configOverridePath != nil && *configOverridePath != "" { - configs = append(configs, config.Path(*configOverridePath)) - } - - ctx, cancel := context.WithCancel(context.Background()) - signals := make(chan os.Signal, 1) - signal.Notify(signals, syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGABRT, syscall.SIGTERM) - go func() { - <-signals - zerolog.Ctx(ctx).Info().Msg("received shutdown signal.") - cancel() - }() - var conf config.Config - err := config.Get(&conf, configs...) - if err != nil { - stdlog.Fatal().Err(err).Msg("critical error. Unable to read app config") - } - - err = app.Start(ctx, conf, config.Build{ - Version: version, - Commit: commit, - }) - if err != nil { + if err := newRootCmd().Execute(); err != nil { stdlog.Err(err).Msg("critical error. Shutdown application") os.Exit(1) } diff --git a/cmd/ceph-api/root.go b/cmd/ceph-api/root.go new file mode 100644 index 0000000..a0def6a --- /dev/null +++ b/cmd/ceph-api/root.go @@ -0,0 +1,30 @@ +package main + +import "github.com/spf13/cobra" + +type rootOptions struct { + configPath string + configOverridePath string +} + +func newRootCmd() *cobra.Command { + opts := &rootOptions{} + + cmd := &cobra.Command{ + Use: "ceph-api", + Short: "Ceph API server and client", + SilenceUsage: true, + SilenceErrors: true, + RunE: func(cmd *cobra.Command, args []string) error { + return runServe(cmd.Context(), opts) + }, + } + + cmd.PersistentFlags().StringVar(&opts.configPath, "config", "", "set path to config directory") + cmd.PersistentFlags().StringVar(&opts.configOverridePath, "config-override", "", "set path to config override directory") + + cmd.AddCommand(newServeCmd(opts)) + cmd.AddCommand(newVersionCmd()) + + return cmd +} diff --git a/cmd/ceph-api/serve.go b/cmd/ceph-api/serve.go new file mode 100644 index 0000000..497b308 --- /dev/null +++ b/cmd/ceph-api/serve.go @@ -0,0 +1,64 @@ +package main + +import ( + "context" + "os" + "os/signal" + "syscall" + + "github.com/clyso/ceph-api/pkg/app" + "github.com/clyso/ceph-api/pkg/config" + "github.com/rs/zerolog" + "github.com/spf13/cobra" +) + +func newServeCmd(opts *rootOptions) *cobra.Command { + return &cobra.Command{ + Use: "serve", + Short: "Start the ceph-api server", + RunE: func(cmd *cobra.Command, args []string) error { + return runServe(cmd.Context(), opts) + }, + } +} + +func runServe(parent context.Context, opts *rootOptions) error { + var configs []config.Src + if opts.configPath != "" { + configs = append(configs, config.Path(opts.configPath)) + } + if opts.configOverridePath != "" { + configs = append(configs, config.Path(opts.configOverridePath)) + } + + ctx, cancel := signalContext(parent) + defer cancel() + + var conf config.Config + if err := config.Get(&conf, configs...); err != nil { + return err + } + + return app.Start(ctx, conf, config.Build{ + Version: version, + Commit: commit, + }) +} + +func signalContext(parent context.Context) (context.Context, context.CancelFunc) { + ctx, cancel := context.WithCancel(parent) + signals := make(chan os.Signal, 1) + signal.Notify(signals, syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGABRT, syscall.SIGTERM) + + go func() { + select { + case <-signals: + zerolog.Ctx(ctx).Info().Msg("received shutdown signal.") + cancel() + case <-ctx.Done(): + } + signal.Stop(signals) + }() + + return ctx, cancel +} diff --git a/cmd/ceph-api/version.go b/cmd/ceph-api/version.go new file mode 100644 index 0000000..072735c --- /dev/null +++ b/cmd/ceph-api/version.go @@ -0,0 +1,18 @@ +package main + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +func newVersionCmd() *cobra.Command { + return &cobra.Command{ + Use: "version", + Short: "Print version information", + RunE: func(cmd *cobra.Command, args []string) error { + _, err := fmt.Fprintf(cmd.OutOrStdout(), "ceph-api %s (%s)\n", version, commit) + return err + }, + } +} diff --git a/cmd/ceph-api/version_test.go b/cmd/ceph-api/version_test.go new file mode 100644 index 0000000..100d798 --- /dev/null +++ b/cmd/ceph-api/version_test.go @@ -0,0 +1,30 @@ +package main + +import ( + "bytes" + "testing" +) + +func TestVersionCommand(t *testing.T) { + oldVersion := version + oldCommit := commit + version = "v1.2.3" + commit = "abc123" + t.Cleanup(func() { + version = oldVersion + commit = oldCommit + }) + + cmd := newRootCmd() + buf := new(bytes.Buffer) + cmd.SetOut(buf) + cmd.SetArgs([]string{"version"}) + + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute() error = %v", err) + } + + if got, want := buf.String(), "ceph-api v1.2.3 (abc123)\n"; got != want { + t.Fatalf("version output = %q, want %q", got, want) + } +} diff --git a/go.mod b/go.mod index 389894c..ba7a10c 100644 --- a/go.mod +++ b/go.mod @@ -33,6 +33,7 @@ require ( require ( github.com/soheilhy/cmux v0.1.5 + github.com/spf13/cobra v1.8.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 golang.org/x/oauth2 v0.20.0 ) @@ -87,7 +88,6 @@ require ( github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect - github.com/spf13/cobra v1.8.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.6.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.49.0 // indirect diff --git a/pkg/rados/production_conn.go b/pkg/rados/production_conn.go index 97c2631..46a8651 100644 --- a/pkg/rados/production_conn.go +++ b/pkg/rados/production_conn.go @@ -3,6 +3,7 @@ package rados import ( + "fmt" "strconv" "github.com/ceph/go-ceph/rados" @@ -21,7 +22,7 @@ func NewRadosConn(conf Config) (RadosConnInterface, error) { // Create a real connection. conn, err := rados.NewConnWithUser(conf.User) if err != nil { - return nil, err + return nil, fmt.Errorf("initialize Ceph/RADOS client for user %q: ensure Ceph libraries are installed and available: %w", conf.User, err) } if conf.MonHost == "" || conf.UserKeyring == "" || conf.RadosTimeout == 0 { err = conn.ReadDefaultConfigFile() @@ -29,18 +30,18 @@ func NewRadosConn(conf Config) (RadosConnInterface, error) { err = conn.ParseCmdLineArgs([]string{"--mon-host", conf.MonHost, "--key", conf.UserKeyring, "--client_mount_timeout", "3"}) } if err != nil { - return nil, err + return nil, fmt.Errorf("load Ceph/RADOS configuration: provide rados.monHost and rados.userKeyring, or ensure the default Ceph config/keyring files are readable: %w", err) } timeout := strconv.FormatFloat(conf.RadosTimeout.Seconds(), 'f', -1, 64) if err = conn.SetConfigOption("rados_osd_op_timeout", timeout); err != nil { - return nil, err + return nil, fmt.Errorf("configure Ceph/RADOS OSD operation timeout: %w", err) } if err = conn.SetConfigOption("rados_mon_op_timeout", timeout); err != nil { - return nil, err + return nil, fmt.Errorf("configure Ceph/RADOS monitor operation timeout: %w", err) } if err = conn.Connect(); err != nil { - return nil, err + return nil, fmt.Errorf("connect to Ceph/RADOS cluster: ensure Ceph config/keyring are valid and monitors are reachable: %w", err) } // Wrap the real connection. From 29c5b0b5921a92beeddcf01992fb9061b69eb659 Mon Sep 17 00:00:00 2001 From: Joshua Blanch Date: Wed, 13 May 2026 20:32:22 +0000 Subject: [PATCH 2/2] deploy: entrypoint of container and helm chart to use CLI Assisted-by: Claude Opus 4.7 Signed-off-by: Joshua Blanch --- Dockerfile | 2 +- README.md | 20 +++++++++++++++----- deploy/ceph-api/templates/deployment.yaml | 5 +++-- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/Dockerfile b/Dockerfile index bf5ad6e..9809fe9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -68,4 +68,4 @@ COPY --from=builder /build/dependencies/lib/ /lib/ COPY --from=builder /build/ceph-api /bin/ceph-api WORKDIR /bin -CMD ["ceph-api"] +ENTRYPOINT ["/bin/ceph-api", "serve"] diff --git a/README.md b/README.md index 1c6fece..4662b2b 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ In this way Ceph API could be deployed anywhere with access to Ceph `mon`. CEPH_DEMO_TAG=latest docker-compose up ``` - Ceph-API docker container starts with both REST and grpc APIs on port `:9969` and creates default admin account with both username `admin` and password `yoursecretpass`. + Ceph-API docker container runs `ceph-api serve`, starts both REST and grpc APIs on port `:9969`, and creates default admin account with both username `admin` and password `yoursecretpass`. 2. Get API Access token: @@ -70,7 +70,17 @@ In this way Ceph API could be deployed anywhere with access to Ceph `mon`. ## Config By default, both gRPC and REST API are exposed on the same port `:9969`. See all default configurations in [`pkg/config/config.yaml`](./pkg/config/config.yaml). -To override default configuration, create similar YAML file and provide its path to application binary as `-config` or `-config-override` arguments. The latter will override the former and can be useful to manage secret values (mount k8s secret to this path). +Start the server with `ceph-api serve`. To override default configuration, create similar YAML file and provide its path with the `--config` or `--config-override` arguments. The latter will override the former and can be useful to manage secret values (mount k8s secret to this path). + +```shell +ceph-api serve --config /path/to/config.yaml --config-override /path/to/override.yaml +``` + +The Docker image uses `/bin/ceph-api serve` as its entrypoint, so server configuration flags can be passed directly after the image name: + +```shell +docker run ghcr.io/clyso/ceph-api:latest --config /path/to/config.yaml +``` Additionally, any config parameter can be set with envar in following format: `CFG_=`. For example: @@ -85,8 +95,8 @@ app: API config uses the following precedence order: 1. Default [`config.yaml`](./pkg/config/config.yaml) -2. YAML file provided in `-config` -3. YAML file provided in `-config-override` +2. YAML file provided in `--config` +3. YAML file provided in `--config-override` 4. Envars ## Mock Mode @@ -94,7 +104,7 @@ API config uses the following precedence order: To run Ceph API in mock mode without a real Ceph cluster: ```shell -CFG_APP_CREATEADMIN=true CFG_APP_ADMINUSERNAME=admin CFG_APP_ADMINPASSWORD=yoursecretpass go run -tags=mock ./cmd/ceph-api/main.go +CFG_APP_CREATEADMIN=true CFG_APP_ADMINUSERNAME=admin CFG_APP_ADMINPASSWORD=yoursecretpass go run -tags=mock ./cmd/ceph-api serve ``` ## Security diff --git a/deploy/ceph-api/templates/deployment.yaml b/deploy/ceph-api/templates/deployment.yaml index 988aead..9ca0beb 100644 --- a/deploy/ceph-api/templates/deployment.yaml +++ b/deploy/ceph-api/templates/deployment.yaml @@ -38,9 +38,10 @@ spec: {{- end }} command: - "ceph-api" - - "-config" + - "serve" + - "--config" - "/bin/config/config.yaml" - - "-config-override" + - "--config-override" - "/bin/config/override.yaml" volumeMounts: - mountPath: /bin/config/config.yaml