Skip to content

Commit

Permalink
Improve examples and docs
Browse files Browse the repository at this point in the history
  • Loading branch information
plar committed Feb 15, 2024
1 parent a65e3d5 commit 79168d0
Show file tree
Hide file tree
Showing 11 changed files with 463 additions and 26 deletions.
5 changes: 5 additions & 0 deletions command/arg.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ func (noopArgument) Apply(safecli.CommandAppender) error {
return nil
}

// NewNoopArgument creates a new argument that does nothing when applied.
func NewNoopArgument() Applier {
return noopArgument{}
}

// argument defines an argument with the given name.
// If the argument is redacted, it is appended as redacted.
type argument struct {
Expand Down
107 changes: 107 additions & 0 deletions examples/kopia/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Kopia Command Line Interface(CLI) Builder

This example demonstrates how to use [safecli](https://github.com/kanisterio/safecli) for programmatically building a CLI for [Kopia](https://github.com/kopia/kopia), focusing on `kopia repository create` and `kopia repository connect` commands.

## Building the CLI

### Common Arguments

Kopia repository commands share a set of common arguments, such as `--config-file`, `--log-dir`, `--log-level`, and `--password`. These common arguments are defined in the [args](args/common_args.go) package and are utilized in both `create` and `connect` commands.

### Repository Commands

The commands related to repositories are contained within the [examples/kopia/repository](repository/) package.


#### Define Repository Creation Arguments

First, define the arguments for the `kopia repository create` command as [repository.CreateArgs](repository/repository_create.go) structure, which embeds the common arguments and adds the `Location`, `Hostname`, and `Username` fields:

```go
// CreateArgs represents the arguments for the `kopia repository create` command.
type CreateArgs struct {
args.Common // Embeds common arguments
Location Location // Filesystem, S3, etc.
Hostname string // The hostname of the repository
Username string // The username of the repository
}
```

#### Define the Repository Creation Function

Next, define the [repository.Create](repository/repository_create.go) function to create a new [safecli.Builder](https://github.com/kanisterio/safecli/blob/main/safecli.go) for the command using arguments from `CreateArgs` structure:

```go
// Create creates a new safecli.Builder for the `kopia repository create` command.
func Create(args CreateArgs) (*safecli.Builder, error) {
return internal.NewKopiaCommand(
opts.Common(args.Common),
cmdRepository, subcmdCreate,
optHostname(args.Hostname),
optUsername(args.Username),
optStorage(args.Location),
)
}
```

This function calls `internal.NewKopiaCommand` from the [examples/kopia/internal](internal/kopia.go) package to create a `safecli.Builder`, converting `CreateArgs` to CLI options through `opts.Common`, `optHostname`, `optUsername`, and `optStorage`.

Common options are defined in [examples/kopia/internal/opts/common_opts.go](internal/opts/common_opts.go) and repository options are defined in [examples/kopia/repository/opts.go](repository/opts.go) files.

#### Example Usage

To build the `kopia repository create ...` command from your Go code, you must use the `Create` function:

```go
package main

import (
"fmt"

"github.com/kanisterio/safecli/examples/kopia/args"
"github.com/kanisterio/safecli/examples/kopia/repository"
)

func main() {
args := repository.CreateArgs{
Common: args.Common{
ConfigFilePath: "/path/to/config",
LogDirectory: "/path/to/log",
LogLevel: "error",
RepoPassword: "123456",
},
Location: repository.Location{
Provider: "filesystem",
MetaData: map[string][]byte{
"repoPath": []byte("/tmp/my-repository"),
},
},
Hostname: "localhost",
Username: "user",
}
cmd, err := repository.Create(args)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("exec=%#v\n", cmd.Build())
}
```

This code will print the command that you can run to create a new Kopia repository.

```bash
$ go run main.go
exec=[]string{"kopia", "--config-file=/path/to/config", "--log-dir=/path/to/log", "--log-level=error", "--password=123456", "repository", "create", "--override-hostname=localhost", "--override-username=user", "filesystem", "--path=/tmp/my-repository"}
```

#### Repository Connect command

The `repository connect` command is implemented in a similar way. You can find the complete example in the [examples/kopia/repository_connect.go](repository/repository_connect.go).

Usage example can be found in [examples/kopia/main.go](main.go).


## Bottom Line

This example demonstrates how to use `safecli` to programmatically build a CLI for Kopia, focusing on the `kopia repository create` and `kopia repository connect` commands. The same approach can be applied to construct other Kopia commands or any other CLI tool.
124 changes: 113 additions & 11 deletions examples/kopia/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,123 @@ func main() {
ConfigFilePath: "/path/to/config",
LogDirectory: "/path/to/log",
LogLevel: "error",
RepoPassword: "password",
RepoPassword: "123456",
},
Location: repository.Location{
Provider: "filesystem",
MetaData: map[string][]byte{
"repoPath": []byte("/tmp/my-repository"),
},
},
Hostname: "localhost",
Username: "user",
}
cmd, err := repository.Create(args)

fmt.Printf("exec=%#v\n", cmd.Build())
fmt.Printf("log=%#v\n", cmd) // make sure that password is redacted
fmt.Printf("log=%v\n", cmd) // make sure that password is redacted
fmt.Printf("err=%#v\n", err)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("exec=", cmd)
// exec= kopia --config-file=/path/to/config --log-dir=/path/to/log --log-level=error --password=<****> repository create --override-hostname=localhost --override-username=user filesystem --path=/tmp/my-repository
fmt.Printf("exec=%#v", cmd.Build())
// exec= kopia --config-file=/path/to/config --log-dir=/path/to/log --log-level=error --password=<****> repository create --override-hostname=localhost --override-username=user filesystem --path=/tmp/my-repository
}

// $ go run .
// exec=[]string{"kopia", "--config-file=/path/to/config", "--log-dir=/path/to/log", "--log-level=error", "--password=password", "repository", "create", "--override-hostname=localhost", "--override-username=user"}
// log=&safecli.Builder{Args:[]safecli.Argument{safecli.Argument{Key:"", Value:(*safecli.PlainValue)(0xc000096020)}, safecli.Argument{Key:"--config-file", Value:(*safecli.PlainValue)(0xc000096030)}, safecli.Argument{Key:"--log-dir", Value:(*safecli.PlainValue)(0xc000096040)}, safecli.Argument{Key:"--log-level", Value:(*safecli.PlainValue)(0xc000096050)}, safecli.Argument{Key:"--password", Value:<****>}, safecli.Argument{Key:"", Value:(*safecli.PlainValue)(0xc000096080)}, safecli.Argument{Key:"", Value:(*safecli.PlainValue)(0xc0000960a0)}, safecli.Argument{Key:"--override-hostname", Value:(*safecli.PlainValue)(0xc0000960b0)}, safecli.Argument{Key:"--override-username", Value:(*safecli.PlainValue)(0xc0000960c0)}}, Formatter:(safecli.ArgumentFormatter)(0x47cee0)}
// log=kopia --config-file=/path/to/config --log-dir=/path/to/log --log-level=error --password=<****> repository create --override-hostname=localhost --override-username=user
// err=<nil>
// package main

// import (
// "fmt"
// "time"

// "github.com/kanisterio/safecli/examples/kopia/args"
// "github.com/kanisterio/safecli/examples/kopia/repository"
// )

// func fsCreateArgs() repository.CreateArgs {
// return repository.CreateArgs{
// Common: args.Common{
// ConfigFilePath: "/path/to/config",
// LogDirectory: "/path/to/log",
// LogLevel: "error",
// RepoPassword: "password",
// },
// Location: repository.Location{
// Provider: repository.ProviderFilesystem,
// MetaData: map[string][]byte{
// "repoPath": []byte("/tmp/my-repository"),
// },
// },
// Hostname: "localhost",
// Username: "user",
// }
// }

// func fsConnectArgs() repository.ConnectArgs {
// return repository.ConnectArgs{
// Common: args.Common{
// ConfigFilePath: "/path/to/config",
// LogDirectory: "/path/to/log",
// LogLevel: "error",
// RepoPassword: "password",
// },
// Location: repository.Location{
// Provider: repository.ProviderFilesystem,
// MetaData: map[string][]byte{
// "repoPath": []byte("/tmp/my-repository"),
// },
// },
// Hostname: "localhost",
// Username: "user",
// ReadOnly: true,
// PointInTime: time.Date(2024, 2, 15, 14, 30, 0, 0, time.FixedZone("PST", -8*60*60)),
// }
// }

// func s3CreateArgs() repository.CreateArgs {
// return repository.CreateArgs{
// Common: args.Common{
// ConfigFilePath: "/path/to/config",
// LogDirectory: "/path/to/log",
// LogLevel: "error",
// RepoPassword: "password",
// },
// Location: repository.Location{
// Provider: repository.ProviderS3,
// MetaData: map[string][]byte{
// "region": []byte("us-west-1"),
// "bucket": []byte("my-bucket"),
// "prefix": []byte("my-repository"),
// "endpoint": []byte("http://localhost:9000"),
// "skipSSLVerify": []byte("true"),
// },
// },
// Hostname: "localhost",
// Username: "user",
// }
// }

// func RepoCreate(args repository.CreateArgs) {
// cmd, err := repository.Create(args)
// fmt.Println("exec=", cmd)
// fmt.Println("err=", err)
// }

// func RepoConnect(args repository.ConnectArgs) {
// cmd, err := repository.Connect(args)
// fmt.Println("exec=", cmd)
// fmt.Println("err=", err)
// }

// func main() {
// RepoCreate(fsCreateArgs())
// RepoCreate(s3CreateArgs())
// RepoConnect(fsConnectArgs())
// }

// // $ go run main.go
// // exec= kopia --config-file=/path/to/config --log-dir=/path/to/log --log-level=error --password=<****> repository create --override-hostname=localhost --override-username=user filesystem --path=/tmp/my-repository
// // err= <nil>
// // exec= kopia --config-file=/path/to/config --log-dir=/path/to/log --log-level=error --password=<****> repository create --override-hostname=localhost --override-username=user s3 --region=us-west-1 --bucket=my-bucket --endpoint=http://localhost:9000 --prefix=my-repository --disable-tls-verify
// // err= <nil>
// // exec= kopia --config-file=/path/to/config --log-dir=/path/to/log --log-level=error --password=<****> repository connect --override-hostname=localhost --override-username=user --read-only --point-in-time=2024-02-15T14:30:00-08:00 filesystem --path=/tmp/my-repository
// // err= <nil>
15 changes: 15 additions & 0 deletions examples/kopia/repository/location.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package repository

// Provider represents the storage provider for the repository.
type Provider string

const (
ProviderFilesystem Provider = "filesystem"
ProviderS3 Provider = "s3"
)

// Location represents the location of the repository.
type Location struct {
Provider Provider
MetaData map[string][]byte
}
51 changes: 42 additions & 9 deletions examples/kopia/repository/opts.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,53 @@

package repository

import "github.com/kanisterio/safecli/command"
import (
"fmt"
"time"

"github.com/kanisterio/safecli/command"
"github.com/kanisterio/safecli/examples/kopia/repository/storage/fs"
"github.com/kanisterio/safecli/examples/kopia/repository/storage/s3"
)

// cmdXXX represents different `kopia repository“ commands.
var (
cmdRepository = command.NewArgument("repository")
cmdCreate = command.NewArgument("create")

subcmdCreate = command.NewArgument("create")
subcmdConnect = command.NewArgument("connect")
)

// optHostname returns a new optHostname flag with a given optHostname.
func optHostname(hostname string) command.Applier {
return command.NewOptionWithArgument("--override-hostname", hostname)
// optHostname creates a new option for the hostname of the repository.
func optHostname(h string) command.Applier {
return command.NewOptionWithArgument("--override-hostname", h)
}

// optUsername creates a new option for the username of the repository.
func optUsername(u string) command.Applier {
return command.NewOptionWithArgument("--override-username", u)
}

// optReadOnly creates a new option for the read-only mode of the repository.
func optReadOnly(readOnly bool) command.Applier {
return command.NewOption("--read-only", readOnly)
}

// optPointInTime creates a new option for the point-in-time of the repository.
func optPointInTime(pit time.Time) command.Applier {
if pit.IsZero() {
return command.NewNoopArgument()
}
return command.NewOptionWithArgument("--point-in-time", pit.Format(time.RFC3339))
}

// optUsername returns a new optUsername flag with a given optUsername.
func optUsername(username string) command.Applier {
return command.NewOptionWithArgument("--override-username", username)
// optStorage creates a list of options for the specified storage location.
func optStorage(l Location) command.Applier {
switch l.Provider {
case ProviderFilesystem:
return fs.New(l.MetaData)
case ProviderS3:
return s3.New(l.MetaData)
default:
return command.NewErrorArgument(fmt.Errorf("unsupported storage provider: %s", l.Provider))
}
}
32 changes: 32 additions & 0 deletions examples/kopia/repository/repository_connect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package repository

import (
"time"

"github.com/kanisterio/safecli"
"github.com/kanisterio/safecli/examples/kopia/args"
"github.com/kanisterio/safecli/examples/kopia/internal"
"github.com/kanisterio/safecli/examples/kopia/internal/opts"
)

// ConnectArgs defines the arguments for the `kopia repository connect` command.
type ConnectArgs struct {
args.Common // common arguments
Location Location // filesystem, s3, etc
Hostname string // the hostname of the repository
Username string // the username of the repository
ReadOnly bool // connect to a repository in read-only mode
PointInTime time.Time // connect to a repository as it was at a specific point in time
}

// Connect creates a new safecli.Builder for the `kopia repository connect` command.
func Connect(args ConnectArgs) (*safecli.Builder, error) {
return internal.NewKopiaCommand(opts.Common(args.Common),
cmdRepository, subcmdConnect,
optHostname(args.Hostname),
optUsername(args.Username),
optReadOnly(args.ReadOnly),
optPointInTime(args.PointInTime),
optStorage(args.Location),
)
}
14 changes: 8 additions & 6 deletions examples/kopia/repository/repository_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,19 @@ import (

// CreateArgs represents the arguments for the `kopia repository create` command.
type CreateArgs struct {
args.Common

Hostname string
Username string
args.Common // common arguments
Location Location // filesystem, s3, etc
Hostname string // the hostname of the repository
Username string // the username of the repository
}

// Create creates a new safecli.Builder for the `kopia repository create` command.
func Create(args CreateArgs) (*safecli.Builder, error) {
return internal.NewKopiaCommand(opts.Common(args.Common),
cmdRepository, cmdCreate,
return internal.NewKopiaCommand(
opts.Common(args.Common),
cmdRepository, subcmdCreate,
optHostname(args.Hostname),
optUsername(args.Username),
optStorage(args.Location),
)
}
Loading

0 comments on commit 79168d0

Please sign in to comment.