Repository: ariga/atlas
Pull request #618 (2022-03-03 12:21:36)

cmd/atlas: add atlas serve docs
Adds documentation for atlas serve command.



Patch:
diff --git a/cmd/atlas/doc.tmpl b/cmd/atlas/doc.tmpl
index 79850144..b1ee0d21 100644
--- a/cmd/atlas/doc.tmpl
+++ b/cmd/atlas/doc.tmpl
@@ -61,4 +61,26 @@ go get ariga.io/atlas/cmd/atlas
 ```
 {{ end }}
 {{ end }}
+## atlas serve
+
+Run Atlas web UI in a standalone mode
+
+#### Usage
+```
+atlas serve [flags]
+```
+
+#### Details
+'atlas serve' runs the Atlas web UI in a standalone mode with optional persistent storage.
+If you do not specify the storage, it will be stored in-memory.
+Atlas encrypts sensitive data such as passwords using the generated keyset.json.
+
+#### Flags
+```
+--addr string       listen address for atlas serve (default ":8080")
+--storage string    data store url using the dsn format:
+                    [driver://username:password@protocol(address)/dbname?param=value] (default "in-memory")
+-h, --help          help for serve
+```
+
 {{ end }}
diff --git a/doc/md/cli/reference.md b/doc/md/cli/reference.md
index 69d824a4..5ea5fb76 100644
--- a/doc/md/cli/reference.md
+++ b/doc/md/cli/reference.md
@@ -195,3 +195,25 @@ atlas version
 ```
 
 
+## atlas serve
+
+Run Atlas web UI in a standalone mode
+
+#### Usage
+```
+atlas serve [flags]
+```
+
+#### Details
+'atlas serve' runs the Atlas web UI in a standalone mode with optional persistent storage.
+If you do not specify the storage, it will be stored in-memory.
+Atlas encrypts sensitive data such as passwords using the generated keyset.json.
+
+#### Flags
+```
+--addr string       listen address for atlas serve (default ":8080")
+--storage string    data store url using the dsn format:
+                    [driver://username:password@protocol(address)/dbname?param=value] (default "in-memory")
+-h, --help          help for serve
+```
+
diff --git a/doc/package-lock.json b/doc/package-lock.json
deleted file mode 100644
index 5819c008..00000000
--- a/doc/package-lock.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
-  "name": "doc",
-  "lockfileVersion": 2,
-  "requires": true,
-  "packages": {}
-}
diff --git a/sql/migrate/migrate.go b/sql/migrate/migrate.go
index 65e7d4bf..909b38ec 100644
--- a/sql/migrate/migrate.go
+++ b/sql/migrate/migrate.go
@@ -128,7 +128,7 @@ func Schema(s *schema.Schema) StateReader {
 }
 
 type (
-	// Dir describes the methods needed to work with the Planner.
+	// Dir describes the methods needed for a Planner to manage migration files.
 	Dir interface {
 		fs.FS
 
@@ -152,25 +152,49 @@ type (
 	// Planner can plan the steps to take to migrate from one state to another. It uses the enclosed FS to write
 	// those changes to versioned migration files.
 	Planner struct {
-		drv Driver
-		dir Dir
-		fmt Formatter
+		drv Driver      // driver to use
+		dir Dir         // where migration files are stored and read from
+		fmt Formatter   // how to format a plan to migration files
+		dsr StateReader // how to read a state from the migration directory
 	}
+
+	// PlannerOption allows managing a Planner using functional arguments.
+	PlannerOption func(*Planner)
 )
 
-// New creates a new Planner.
-func New(drv Driver, dir Dir, fmt Formatter) *Planner {
-	return &Planner{
-		drv: drv,
-		dir: dir,
-		fmt: fmt,
+// NewPlanner creates a new Planner.
+func NewPlanner(drv Driver, dir Dir, opts ...PlannerOption) *Planner {
+	p := &Planner{drv: drv, dir: dir}
+	for _, opt := range opts {
+		opt(p)
+	}
+	if p.fmt == nil {
+		p.fmt = DefaultFormatter
+	}
+	if p.dsr == nil {
+		p.dsr = GlobStateReader(p.dir, p.drv, "*.sql")
+	}
+	return p
+}
+
+// WithFormatter sets the Formatter of a Planner.
+func WithFormatter(fmt Formatter) PlannerOption {
+	return func(p *Planner) {
+		p.fmt = fmt
+	}
+}
+
+// WithStateReader sets the StateReader of a Planner.
+func WithStateReader(dsr StateReader) PlannerOption {
+	return func(p *Planner) {
+		p.dsr = dsr
 	}
 }
 
 // Plan calculates the migration Plan required for moving the current state (from) state to
 // the next state (to). A StateReader can be a directory, static schema elements or a Driver connection.
-func (p *Planner) Plan(ctx context.Context, name string, from StateReader, to StateReader) (*Plan, error) {
-	current, err := from.ReadState(ctx)
+func (p *Planner) Plan(ctx context.Context, name string, to StateReader) (*Plan, error) {
+	current, err := p.dsr.ReadState(ctx)
 	if err != nil {
 		return nil, err
 	}
@@ -214,7 +238,11 @@ type LocalDir struct {
 // NewLocalDir returns a new the Dir used by a Planner to work on the given local path.
 func NewLocalDir(path string) (*LocalDir, error) {
 	fi, err := os.Stat(path)
-	if err != nil {
+	if err == os.ErrNotExist {
+		if err := os.MkdirAll(path, 0755); err != nil {
+			return nil, err
+		}
+	} else if err != nil {
 		return nil, err
 	}
 	if !fi.IsDir() {
@@ -225,9 +253,9 @@ func NewLocalDir(path string) (*LocalDir, error) {
 
 // GlobStateReader creates a StateReader that loads all files from Dir matching
 // glob in lexicographic order and uses the Driver to create a migration state.
-func (d *LocalDir) GlobStateReader(drv Driver, glob string) StateReaderFunc {
+func GlobStateReader(dir Dir, drv Driver, glob string) StateReaderFunc {
 	return func(ctx context.Context) (*schema.Realm, error) {
-		names, err := fs.Glob(d, glob)
+		names, err := fs.Glob(dir, glob)
 		if err != nil {
 			return nil, err
 		}
@@ -236,7 +264,7 @@ func (d *LocalDir) GlobStateReader(drv Driver, glob string) StateReaderFunc {
 			return names[i] < names[j]
 		})
 		for _, n := range names {
-			f, err := d.Open(n)
+			f, err := dir.Open(n)
 			if err != nil {
 				return nil, err
 			}
@@ -263,6 +291,8 @@ func (d *LocalDir) WriteFile(name string, b []byte) error {
 	return os.WriteFile(filepath.Join(d.dir, name), b, 0600)
 }
 
+var _ Dir = (*LocalDir)(nil)
+
 var (
 	// templateFuncs contains the template.FuncMap for the DefaultFormatter.
 	templateFuncs = template.FuncMap{


Test patch:
diff --git a/sql/migrate/migrate_test.go b/sql/migrate/migrate_test.go
index b0004251..a15087bf 100644
--- a/sql/migrate/migrate_test.go
+++ b/sql/migrate/migrate_test.go
@@ -33,7 +33,7 @@ func TestPlanner_WritePlan(t *testing.T) {
 	}
 
 	// DefaultFormatter
-	pl := migrate.New(nil, mfs, migrate.DefaultFormatter)
+	pl := migrate.NewPlanner(nil, mfs)
 	require.NotNil(t, pl)
 	require.NoError(t, pl.WritePlan(plan))
 	v := strconv.FormatInt(time.Now().Unix(), 10)
@@ -53,7 +53,7 @@ func TestPlanner_WritePlan(t *testing.T) {
 		template.Must(template.New("").Parse("{{range .Changes}}{{println .Cmd}}{{end}}")),
 	)
 	require.NoError(t, err)
-	pl = migrate.New(nil, mfs, fmt)
+	pl = migrate.NewPlanner(nil, mfs, migrate.WithFormatter(fmt))
 	require.NotNil(t, pl)
 	require.NoError(t, pl.WritePlan(plan))
 	require.Len(t, mfs.files, 3)
@@ -71,8 +71,8 @@ func TestPlanner_Plan(t *testing.T) {
 	)
 
 	// nothing to do
-	pl := migrate.New(drv, mfs, migrate.DefaultFormatter)
-	plan, err := pl.Plan(ctx, "empty", migrate.Realm(nil), migrate.Realm(nil))
+	pl := migrate.NewPlanner(drv, mfs)
+	plan, err := pl.Plan(ctx, "empty", migrate.Realm(nil))
 	require.ErrorIs(t, err, migrate.ErrNoPlan)
 	require.Nil(t, plan)
 
@@ -87,7 +87,7 @@ func TestPlanner_Plan(t *testing.T) {
 			{Cmd: "CREATE TABLE t2(c int);"},
 		},
 	}
-	plan, err = pl.Plan(ctx, "", migrate.Realm(nil), migrate.Realm(nil))
+	plan, err = pl.Plan(ctx, "", migrate.Realm(nil))
 	require.NoError(t, err)
 	require.Equal(t, drv.plan, plan)
 }
@@ -100,11 +100,11 @@ func TestGlobStateReader(t *testing.T) {
 	localFS, err := migrate.NewLocalDir("testdata")
 	require.NoError(t, err)
 
-	_, err = localFS.GlobStateReader(drv, "*.up.sql").ReadState(ctx)
+	_, err = migrate.GlobStateReader(localFS, drv, "*.up.sql").ReadState(ctx)
 	require.NoError(t, err)
 	require.Equal(t, drv.executed, []string{"CREATE TABLE t(c int);"})
 
-	_, err = localFS.GlobStateReader(drv, "*.down.sql").ReadState(ctx)
+	_, err = migrate.GlobStateReader(localFS, drv, "*.down.sql").ReadState(ctx)
 	require.NoError(t, err)
 	require.Equal(t, drv.executed, []string{"CREATE TABLE t(c int);", "DROP TABLE IF EXISTS t;"})
 }