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
37 changes: 37 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- `kasapi-cli databases add` and `… update` now bind disjoint flag
sets (mirroring the ddnsuser slice). The flag names are identical
on both subcommands, but the help text of each reflects its own
action semantics ("initial password" / "required" on add;
"replacement password" on update) and cobra rejects an unknown flag
at parse time. The regenerated `docs/cli/kasapi-cli_databases_*.md`
pages now stop claiming "required for add" on the update help
output.
- `kasapi-cli databases list` renders the `used_database_space`
column with a " MB" suffix as part of the value (matching how the
singular detail view already rendered it) and drops the `USED_MB`
header in favour of a bare `USED` header. List and singular views
now share a single unit-rendering convention.
- The `database` package's `Database.in_progress` JSON/YAML field is
no longer marked `omitempty`, aligning with the majority of read
modules (`mailaccount`, `mailinglist`, `sambauser`, `ftpuser`,
`account`). The KAS API has returned `in_progress` on every
captured fixture row, so the previous omitempty added drift without
shielding callers from a missing key.
- `database.Client.Add`'s domain-level validation now emits per-field
errors ("requires a non-empty password" / "comment") instead of a
single combined message, so callers who hit the domain validator
(rather than the CLI's per-flag required-flag checks) can tell
which field actually broke. `AllowedHosts` is no longer required —
an empty value is the KAS API's documented "any host may connect"
wildcard, not a missing parameter; `kasapi-cli databases add`'s
`--allowed-hosts` flag is therefore optional and the empty-string
wildcard is sent verbatim on the wire.
- `(cli.ConfirmAction).Summary` is now exported (was `summary`), so
tests can pin the rendered prompt (and the per-slice loudness verb)
without instantiating a real terminal.

- `kasapi-cli ddnsusers add` and `… update` now bind disjoint flag
sets — `add` carries `--zone` / `--label` / `--target-ip`, `update`
carries `--target-ipv4` / `--target-ipv6` instead — so each
Expand Down Expand Up @@ -36,6 +68,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- `database.InProgressFalse` / `database.InProgressTrue` package
constants for the literal `"FALSE"` / `"TRUE"` strings the KAS API
uses to encode the async-write flag, so mapping code and tests
share one source of truth rather than re-typing literals.

- Database write endpoints (#122, #13 write slice):
`kasapi-cli databases add --password <pw> --comment <text>
--allowed-hosts <hosts>`,
Expand Down
20 changes: 16 additions & 4 deletions docs/cli/kasapi-cli_databases_add.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,29 @@

Create a database (add_database; the login is generated by KAS)

### Synopsis

Create a database via add_database. KAS generates the login and the
command prints it on success.

--allowed-hosts is optional: an empty value (or omitting the flag) is
the KAS API's documented "any host may connect" wildcard — it is
sent on the wire as an empty database_allowed_hosts parameter, which
the API interprets as unrestricted access. Pass an explicit
comma-separated list of host names / IPs / CIDR blocks to restrict
access.

```
kasapi-cli databases add --password <pw> --comment <text> --allowed-hosts <hosts> [flags]
kasapi-cli databases add --password <pw> --comment <text> [--allowed-hosts <hosts>] [flags]
```

### Options

```
--allowed-hosts string comma-separated list of hosts allowed to connect (required for add; e.g. "localhost, 192.168.100.10, 192.168.100.10/32")
--comment string user comment / label (required for add)
--allowed-hosts string comma-separated list of hosts allowed to connect (optional; an empty value is the KAS API's documented "any host may connect" wildcard, e.g. "localhost, 192.168.100.10, 192.168.100.10/32")
--comment string human-readable comment / label for the new database (required)
-h, --help help for add
--password string database password (required for add; new password for update)
--password string initial database password (required)
```

### Options inherited from parent commands
Expand Down
6 changes: 3 additions & 3 deletions docs/cli/kasapi-cli_databases_update.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ kasapi-cli databases update <database-login> [password/comment/allowed-hosts fla
### Options

```
--allowed-hosts string comma-separated list of hosts allowed to connect (required for add; e.g. "localhost, 192.168.100.10, 192.168.100.10/32")
--comment string user comment / label (required for add)
--allowed-hosts string replacement comma-separated list of hosts allowed to connect
--comment string replacement comment / label
-h, --help help for update
--password string database password (required for add; new password for update)
--password string replacement database password (sent as database_new_password)
```

### Options inherited from parent commands
Expand Down
8 changes: 6 additions & 2 deletions internal/cli/confirm.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,11 @@ type ConfirmAction struct {
ID string // identifier of the target resource
}

func (a ConfirmAction) summary() string {
// Summary renders the one-line description shown to the user before
// the [y/N] prompt. Exported so tests can pin the rendered prompt
// (and the loudness verb each slice chose) without instantiating a
// real terminal.
func (a ConfirmAction) Summary() string {
return fmt.Sprintf("About to %s %s %q. This cannot be undone.", a.Verb, a.Resource, a.ID)
}

Expand All @@ -87,7 +91,7 @@ func GateDestructive(in io.Reader, out io.Writer, isTTY, yes bool, a ConfirmActi
if !isTTY {
return UserError(ErrConfirmationRequired, "")
}
ok, err := Confirm(in, out, a.summary())
ok, err := Confirm(in, out, a.Summary())
if err != nil {
return UserError(err, "confirm")
}
Expand Down
98 changes: 67 additions & 31 deletions internal/cli/databases.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,27 +56,28 @@ func newDatabasesGetCmd(opts *RootOptions) *cobra.Command {
}
}

// databaseWriteFlags binds the shared add_database / update_database
// request fields to a command. The same flag set serves both: add
// reads every value, update sends only the flags the user explicitly
// changed (see databaseChangedFields). The password flag maps to a
// different KAS key per action (add_database: database_password,
// update_database: database_new_password) — see spec() and
// databaseChangedFields.
type databaseWriteFlags struct {
// databaseAddFlags binds the add_database request fields. add and
// update bind disjoint flag sets (same wire-key surface, but the help
// text of each subcommand reflects only its own action semantics, and
// cobra rejects a wrong-subcommand flag at parse time rather than
// silently ignoring it). The password flag maps to database_password
// (database_password is the add-only key; update has its own
// --password flag that maps to database_new_password — see
// databaseUpdateFlags).
type databaseAddFlags struct {
password string
comment string
allowedHosts string
}

func (f *databaseWriteFlags) bind(cmd *cobra.Command) {
func (f *databaseAddFlags) bind(cmd *cobra.Command) {
fl := cmd.Flags()
fl.StringVar(&f.password, "password", "", "database password (required for add; new password for update)")
fl.StringVar(&f.comment, "comment", "", "user comment / label (required for add)")
fl.StringVar(&f.allowedHosts, "allowed-hosts", "", "comma-separated list of hosts allowed to connect (required for add; e.g. \"localhost, 192.168.100.10, 192.168.100.10/32\")")
fl.StringVar(&f.password, "password", "", "initial database password (required)")
fl.StringVar(&f.comment, "comment", "", "human-readable comment / label for the new database (required)")
fl.StringVar(&f.allowedHosts, "allowed-hosts", "", "comma-separated list of hosts allowed to connect (optional; an empty value is the KAS API's documented \"any host may connect\" wildcard, e.g. \"localhost, 192.168.100.10, 192.168.100.10/32\")")
}

func (f *databaseWriteFlags) spec() database.Spec {
func (f *databaseAddFlags) spec() database.Spec {
return database.Spec{
Password: f.password,
Comment: f.comment,
Expand All @@ -85,21 +86,27 @@ func (f *databaseWriteFlags) spec() database.Spec {
}

func newDatabasesAddCmd(opts *RootOptions) *cobra.Command {
f := &databaseWriteFlags{}
f := &databaseAddFlags{}
cmd := &cobra.Command{
Use: "add --password <pw> --comment <text> --allowed-hosts <hosts>",
Use: "add --password <pw> --comment <text> [--allowed-hosts <hosts>]",
Short: "Create a database (add_database; the login is generated by KAS)",
Args: cobra.NoArgs,
Long: `Create a database via add_database. KAS generates the login and the
command prints it on success.

--allowed-hosts is optional: an empty value (or omitting the flag) is
the KAS API's documented "any host may connect" wildcard — it is
sent on the wire as an empty database_allowed_hosts parameter, which
the API interprets as unrestricted access. Pass an explicit
comma-separated list of host names / IPs / CIDR blocks to restrict
access.`,
Args: cobra.NoArgs,
RunE: runWriteE(opts, func([]string) (writeSpec, error) {
if f.password == "" {
return writeSpec{}, fmt.Errorf("--password is required")
}
if f.comment == "" {
return writeSpec{}, fmt.Errorf("--comment is required")
}
if f.allowedHosts == "" {
return writeSpec{}, fmt.Errorf("--allowed-hosts is required")
}
s := f.spec()
return writeSpec{
action: "add_database",
Expand All @@ -120,15 +127,36 @@ func newDatabasesAddCmd(opts *RootOptions) *cobra.Command {
return cmd
}

// databaseChangedFields collects only the write flags the user
// databaseUpdateFlags binds the update_database mutable surface.
// Disjoint from databaseAddFlags so update's --help describes the
// flags as replacements (not "required for add") and cobra rejects an
// add-only flag at parse time.
//
// The password flag maps to database_new_password on this subcommand
// (the update_database key) rather than the add-only
// database_password — see databaseUpdateChangedFields.
type databaseUpdateFlags struct {
password string
comment string
allowedHosts string
}

func (f *databaseUpdateFlags) bind(cmd *cobra.Command) {
fl := cmd.Flags()
fl.StringVar(&f.password, "password", "", "replacement database password (sent as database_new_password)")
fl.StringVar(&f.comment, "comment", "", "replacement comment / label")
fl.StringVar(&f.allowedHosts, "allowed-hosts", "", "replacement comma-separated list of hosts allowed to connect")
}

// databaseUpdateChangedFields collects only the flags the user
// explicitly set into the update_database field map (keyed on the
// database.Field* constants). Each field is a wholesale replacement
// and an empty value is a meaningful set, so presence is keyed on
// cobra Changed, not on the value being non-empty — the same pattern
// the ftpuser/sambauser updates use. The password flag maps to
// database_new_password here (update_database's key) rather than the
// add-only database_password.
func databaseChangedFields(cmd *cobra.Command, f *databaseWriteFlags) map[string]string {
func databaseUpdateChangedFields(cmd *cobra.Command, f *databaseUpdateFlags) map[string]string {
fields := map[string]string{}
if cmd.Flags().Changed("password") {
fields[database.FieldNewPassword] = f.password
Expand All @@ -143,15 +171,15 @@ func databaseChangedFields(cmd *cobra.Command, f *databaseWriteFlags) map[string
}

func newDatabasesUpdateCmd(opts *RootOptions) *cobra.Command {
f := &databaseWriteFlags{}
f := &databaseUpdateFlags{}
cmd := &cobra.Command{
Use: "update <database-login> [password/comment/allowed-hosts flags]",
Short: "Replace mutable fields of a database (update_database)",
Args: cobra.ExactArgs(1),
}
cmd.RunE = runWriteE(opts, func(args []string) (writeSpec, error) {
login := args[0]
fields := databaseChangedFields(cmd, f)
fields := databaseUpdateChangedFields(cmd, f)
if len(fields) == 0 {
return writeSpec{}, fmt.Errorf("at least one field flag (e.g. --password/--comment/--allowed-hosts) is required")
}
Expand All @@ -172,6 +200,20 @@ func newDatabasesUpdateCmd(opts *RootOptions) *cobra.Command {
return cmd
}

// databaseDeleteConfirm builds the ConfirmAction shown before
// delete_database is dispatched. Extracted from the RunE closure so
// the loudness adjustment (Verb: "permanently delete" instead of the
// bare "delete" every other slice uses) is structurally pinned by a
// test rather than only by a source-code comment.
//
// delete_database drops the database AND every row it contains, which
// is the loudest data-loss surface of the v0.2.0 write phase. The
// prompt template adds "This cannot be undone." regardless of the
// verb.
func databaseDeleteConfirm(login string) ConfirmAction {
return ConfirmAction{Verb: "permanently delete", Resource: "database", ID: login}
}

func newDatabasesDeleteCmd(opts *RootOptions) *cobra.Command {
return &cobra.Command{
Use: "delete <database-login>",
Expand All @@ -182,14 +224,8 @@ func newDatabasesDeleteCmd(opts *RootOptions) *cobra.Command {
return writeSpec{
action: "delete_database",
destructive: true,
// "permanently delete" is intentionally more emphatic
// than the bare "delete" verb the other slices use:
// delete_database drops the database AND every row it
// contains, which is the loudest data-loss surface of
// the v0.2.0 write phase. The prompt template adds
// "This cannot be undone." regardless.
confirm: ConfirmAction{Verb: "permanently delete", Resource: "database", ID: login},
params: database.DeleteParams(login),
confirm: databaseDeleteConfirm(login),
params: database.DeleteParams(login),
dispatch: func(c *api.Client, ctx context.Context) (string, error) {
if derr := database.NewClient(c).Delete(ctx, login); derr != nil {
return "", derr
Expand Down
Loading
Loading