Skip to content

Commit

Permalink
feat: ftl schema diff (#2043)
Browse files Browse the repository at this point in the history
Fixes #1989

```
ftl schema diff --help
Usage: ftl schema diff <other-endpoint> [flags]

Print any schema differences between this cluster and another cluster. Returns
an exit code of 1 if there are differences.

Arguments:
  <other-endpoint>    Other endpoint URL to compare against.

  --endpoint=http://127.0.0.1:8892     FTL endpoint to bind/connect to ($FTL_ENDPOINT).
  --color    Enable colored output regardless of TTY.
```

- Compares `<other-endpoint>` vs `--endpoint` into a unified diff with
color.
- Exits with 1 if there is a difference.
- Detects tty and will not emit ANSI unless `--color` is passed in.

<img width="1104" alt="image"
src="https://github.com/TBD54566975/ftl/assets/31338/7aa704ae-9df0-48c7-90d5-56f97f4d556c">
  • Loading branch information
gak authored Jul 11, 2024
1 parent c68eced commit 09bfe4b
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 1 deletion.
1 change: 1 addition & 0 deletions cmd/ftl/cmd_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

type schemaCmd struct {
Get getSchemaCmd `default:"" cmd:"" help:"Retrieve the cluster FTL schema."`
Diff schemaDiffCmd `cmd:"" help:"Print any schema differences between this cluster and another cluster. Returns an exit code of 1 if there are differences."`
Protobuf schemaProtobufCmd `cmd:"" help:"Generate protobuf schema mirroring the FTL schema structure."`
Generate schemaGenerateCmd `cmd:"" help:"Stream the schema from the cluster and generate files from the template."`
Import schemaImportCmd `cmd:"" help:"Import messages to the FTL schema."`
Expand Down
85 changes: 85 additions & 0 deletions cmd/ftl/cmd_schema_diff.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package main

import (
"context"
"fmt"
"net/url"
"os"

"connectrpc.com/connect"
"github.com/hexops/gotextdiff"
"github.com/hexops/gotextdiff/myers"
"github.com/hexops/gotextdiff/span"
"github.com/mattn/go-isatty"

ftlv1 "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1"
"github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1/ftlv1connect"
schemapb "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1/schema"
"github.com/TBD54566975/ftl/backend/schema"
"github.com/TBD54566975/ftl/internal/log"
"github.com/TBD54566975/ftl/internal/rpc"
"github.com/alecthomas/chroma/v2/quick"
)

type schemaDiffCmd struct {
OtherEndpoint url.URL `arg:"" help:"Other endpoint URL to compare against."`
Color bool `help:"Enable colored output regardless of TTY."`
}

func (d *schemaDiffCmd) Run(ctx context.Context, currentURL *url.URL) error {
other, err := schemaForURL(ctx, d.OtherEndpoint)
if err != nil {
return fmt.Errorf("failed to get other schema: %w", err)
}
current, err := schemaForURL(ctx, *currentURL)
if err != nil {
return fmt.Errorf("failed to get current schema: %w", err)
}

edits := myers.ComputeEdits(span.URIFromPath(""), other.String(), current.String())
diff := fmt.Sprint(gotextdiff.ToUnified(d.OtherEndpoint.String(), currentURL.String(), other.String(), edits))

color := d.Color || isatty.IsTerminal(os.Stdout.Fd())
if color {
err = quick.Highlight(os.Stdout, diff, "diff", "terminal256", "solarized-dark")
if err != nil {
return fmt.Errorf("failed to highlight diff: %w", err)
}
} else {
fmt.Print(diff)
}

// Similar to the `diff` command, exit with 1 if there are differences.
if diff != "" {
os.Exit(1)
}

return nil
}

func schemaForURL(ctx context.Context, url url.URL) (*schema.Schema, error) {
client := rpc.Dial(ftlv1connect.NewControllerServiceClient, url.String(), log.Error)
resp, err := client.PullSchema(ctx, connect.NewRequest(&ftlv1.PullSchemaRequest{}))
if err != nil {
return nil, fmt.Errorf("url %s: failed to pull schema: %w", url.String(), err)
}

pb := &schemapb.Schema{}
for resp.Receive() {
msg := resp.Msg()
pb.Modules = append(pb.Modules, msg.Schema)
if !msg.More {
break
}
}
if resp.Err() != nil {
return nil, fmt.Errorf("url %s: failed to receive schema: %w", url.String(), resp.Err())
}

s, err := schema.FromProto(pb)
if err != nil {
return nil, fmt.Errorf("url %s: failed to parse schema: %w", url.String(), err)
}

return s, nil
}
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
github.com/TBD54566975/scaffolder v1.0.0
github.com/alecthomas/assert/v2 v2.10.0
github.com/alecthomas/atomic v0.1.0-alpha2
github.com/alecthomas/chroma/v2 v2.14.0
github.com/alecthomas/concurrency v0.0.2
github.com/alecthomas/kong v0.9.0
github.com/alecthomas/kong-toml v0.2.0
Expand Down Expand Up @@ -121,7 +122,7 @@ require (
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
github.com/hexops/gotextdiff v1.0.3 // indirect
github.com/hexops/gotextdiff v1.0.3
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 09bfe4b

Please sign in to comment.