Skip to content

Commit 7910885

Browse files
authored
cmd/atlascmd: support project file in inspect (#771)
1 parent 4c0e28e commit 7910885

File tree

7 files changed

+329
-50
lines changed

7 files changed

+329
-50
lines changed

cmd/atlascmd/project.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright 2021-present The Atlas Authors. All rights reserved.
2+
// This source code is licensed under the Apache 2.0 license found
3+
// in the LICENSE file in the root directory of this source tree.
4+
5+
package atlascmd
6+
7+
import (
8+
"fmt"
9+
"os"
10+
11+
"ariga.io/atlas/schema/schemaspec"
12+
"ariga.io/atlas/schema/schemaspec/schemahcl"
13+
)
14+
15+
const projectFileName = "atlas.hcl"
16+
17+
// projectFile represents an atlas.hcl file.
18+
type projectFile struct {
19+
Envs []*Env `spec:"env"`
20+
}
21+
22+
// Env represents an Atlas environment.
23+
type Env struct {
24+
// Name for this environment.
25+
Name string `spec:"name,name"`
26+
27+
// URL of the database.
28+
URL string `spec:"url"`
29+
30+
// URL of the dev-database for this environment.
31+
// See: https://atlasgo.io/dev-database
32+
DevURL string `spec:"dev"`
33+
34+
// Path to the file containing the desired schema of the environment.
35+
Source string `spec:"src"`
36+
37+
// List of schemas in this database that are managed by Atlas.
38+
Schemas []string `spec:"schemas"`
39+
schemaspec.DefaultExtension
40+
}
41+
42+
// LoadEnv reads the project file in path, and loads the environment
43+
// with the provided name into env.
44+
func LoadEnv(path string, name string) (*Env, error) {
45+
b, err := os.ReadFile(path)
46+
if err != nil {
47+
return nil, err
48+
}
49+
var project projectFile
50+
if err := schemahcl.New().Eval(b, &project, nil); err != nil {
51+
return nil, fmt.Errorf("error reading project file: %w", err)
52+
}
53+
projEnvs := make(map[string]*Env)
54+
for _, e := range project.Envs {
55+
if _, ok := projEnvs[e.Name]; ok {
56+
return nil, fmt.Errorf("duplicate environment name %q", e.Name)
57+
}
58+
if e.Name == "" {
59+
return nil, fmt.Errorf("all envs must have names on file %q", path)
60+
}
61+
if e.URL == "" {
62+
return nil, fmt.Errorf("no url set for e %q", e.Name)
63+
}
64+
projEnvs[e.Name] = e
65+
}
66+
selected, ok := projEnvs[name]
67+
if !ok {
68+
return nil, fmt.Errorf("env %q not defined in project file", name)
69+
}
70+
return selected, nil
71+
}
72+
73+
func init() {
74+
schemaspec.Register("env", &Env{})
75+
}

cmd/atlascmd/project_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright 2021-present The Atlas Authors. All rights reserved.
2+
// This source code is licensed under the Apache 2.0 license found
3+
// in the LICENSE file in the root directory of this source tree.
4+
5+
package atlascmd
6+
7+
import (
8+
"os"
9+
"path/filepath"
10+
"testing"
11+
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
func TestLoadEnv(t *testing.T) {
16+
d := t.TempDir()
17+
h := `
18+
env "local" {
19+
url = "mysql://root:pass@localhost:3306/"
20+
dev = "docker://mysql/8"
21+
src = "./app.hcl"
22+
schemas = ["hello", "world"]
23+
}
24+
`
25+
err := os.WriteFile(filepath.Join(d, projectFileName), []byte(h), 0600)
26+
require.NoError(t, err)
27+
path := filepath.Join(d, projectFileName)
28+
t.Run("ok", func(t *testing.T) {
29+
env := &Env{}
30+
env, err = LoadEnv(path, "local")
31+
require.NoError(t, err)
32+
require.EqualValues(t, &Env{
33+
Name: "local",
34+
URL: "mysql://root:pass@localhost:3306/",
35+
DevURL: "docker://mysql/8",
36+
Source: "./app.hcl",
37+
Schemas: []string{"hello", "world"},
38+
}, env)
39+
})
40+
t.Run("wrong env", func(t *testing.T) {
41+
_, err = LoadEnv(path, "home")
42+
require.EqualError(t, err, `env "home" not defined in project file`)
43+
})
44+
t.Run("wrong dir", func(t *testing.T) {
45+
wd, err := os.Getwd()
46+
require.NoError(t, err)
47+
_, err = LoadEnv(filepath.Join(wd, projectFileName), "home")
48+
require.ErrorContains(t, err, `no such file or directory`)
49+
})
50+
t.Run("duplicate env", func(t *testing.T) {
51+
dup := h + "\n" + h
52+
path := filepath.Join(d, "dup.hcl")
53+
err = os.WriteFile(path, []byte(dup), 0600)
54+
require.NoError(t, err)
55+
_, err = LoadEnv(path, "local")
56+
require.EqualError(t, err, `duplicate environment name "local"`)
57+
})
58+
}

cmd/atlascmd/schema.go

Lines changed: 87 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"bytes"
99
"context"
1010
"errors"
11+
"fmt"
1112
"io/fs"
1213
"io/ioutil"
1314
"os"
@@ -54,7 +55,7 @@ migration, Atlas will print the migration plan and prompt the user for approval.
5455
5556
If run with the "--dry-run" flag, atlas will exit after printing out the planned
5657
migration.`,
57-
Run: CmdApplyRun,
58+
RunE: CmdApplyRun,
5859
Example: ` atlas schema apply -u "mysql://user:pass@localhost/dbname" -f atlas.hcl
5960
atlas schema apply -u "mysql://localhost" -f atlas.hcl --schema prod --schema staging
6061
atlas schema apply -u "mysql://user:pass@localhost:3306/dbname" -f atlas.hcl --dry-run
@@ -114,6 +115,14 @@ const (
114115
answerAbort = "Abort"
115116
)
116117

118+
// selectEnv selects the environment config from the current directory project file.
119+
func selectEnv(args []string) (*Env, error) {
120+
if len(args) == 0 {
121+
return nil, nil
122+
}
123+
return LoadEnv(projectFileName, args[0])
124+
}
125+
117126
func init() {
118127
// Schema apply flags.
119128
schemaCmd.AddCommand(SchemaApply)
@@ -129,8 +138,7 @@ func init() {
129138
SchemaApply.Flags().BoolVarP(&ApplyFlags.Verbose, migrateDiffFlagVerbose, "", false, "enable verbose logging")
130139
SchemaApply.Flags().StringToStringVarP(&ApplyFlags.Vars, "var", "", nil, "input variables")
131140
cobra.CheckErr(SchemaApply.MarkFlagRequired("url"))
132-
cobra.CheckErr(SchemaApply.MarkFlagRequired("file"))
133-
dsn2url(SchemaApply, &ApplyFlags.URL)
141+
fixURLFlag(SchemaApply, &ApplyFlags.URL)
134142

135143
// Schema inspect flags.
136144
schemaCmd.AddCommand(SchemaInspect)
@@ -139,14 +147,14 @@ func init() {
139147
SchemaInspect.Flags().StringVarP(&InspectFlags.Addr, "addr", "", ":5800", "Used with -w, local address to bind the server to")
140148
SchemaInspect.Flags().StringSliceVarP(&InspectFlags.Schema, "schema", "s", nil, "Set schema name")
141149
cobra.CheckErr(SchemaInspect.MarkFlagRequired("url"))
142-
dsn2url(SchemaInspect, &InspectFlags.URL)
150+
fixURLFlag(SchemaInspect, &InspectFlags.URL)
143151

144152
// Schema fmt.
145153
schemaCmd.AddCommand(SchemaFmt)
146154
}
147155

148156
// CmdInspectRun is the command used when running CLI.
149-
func CmdInspectRun(cmd *cobra.Command, _ []string) {
157+
func CmdInspectRun(cmd *cobra.Command, args []string) {
150158
if InspectFlags.Web {
151159
schemaCmd.PrintErrln("The Alas UI is not available in this release.")
152160
return
@@ -155,6 +163,11 @@ func CmdInspectRun(cmd *cobra.Command, _ []string) {
155163
cobra.CheckErr(err)
156164
defer client.Close()
157165
schemas := InspectFlags.Schema
166+
activeEnv, err := selectEnv(args)
167+
cobra.CheckErr(err)
168+
if activeEnv != nil && len(activeEnv.Schemas) > 0 {
169+
schemas = activeEnv.Schemas
170+
}
158171
if client.URL.Schema != "" {
159172
schemas = append(schemas, client.URL.Schema)
160173
}
@@ -168,15 +181,37 @@ func CmdInspectRun(cmd *cobra.Command, _ []string) {
168181
}
169182

170183
// CmdApplyRun is the command used when running CLI.
171-
func CmdApplyRun(cmd *cobra.Command, _ []string) {
184+
func CmdApplyRun(cmd *cobra.Command, args []string) error {
172185
if ApplyFlags.Web {
173-
schemaCmd.PrintErrln("The Atlas UI is not available in this release.")
174-
return
186+
cmd.Println("The Atlas UI is not available in this release.")
187+
return errors.New("unavailable")
175188
}
176189
c, err := sqlclient.Open(cmd.Context(), ApplyFlags.URL)
177-
cobra.CheckErr(err)
190+
if err != nil {
191+
return err
192+
}
178193
defer c.Close()
179-
applyRun(cmd.Context(), c, ApplyFlags.File, ApplyFlags.DryRun, ApplyFlags.AutoApprove, ApplyFlags.Vars)
194+
devURL := ApplyFlags.DevURL
195+
activeEnv, err := selectEnv(args)
196+
if err != nil {
197+
return err
198+
}
199+
if activeEnv != nil && activeEnv.DevURL != "" {
200+
devURL = activeEnv.DevURL
201+
}
202+
var file string
203+
switch {
204+
case activeEnv != nil && activeEnv.Source != "":
205+
file = activeEnv.Source
206+
case ApplyFlags.File != "":
207+
file = ApplyFlags.File
208+
default:
209+
return fmt.Errorf("source file must be set via -f or project file")
210+
}
211+
if activeEnv != nil && activeEnv.Source != "" {
212+
file = activeEnv.Source
213+
}
214+
return applyRun(cmd.Context(), c, devURL, file, ApplyFlags.DryRun, ApplyFlags.AutoApprove, ApplyFlags.Vars)
180215
}
181216

182217
// CmdFmtRun formats all HCL files in a given directory using canonical HCL formatting
@@ -190,19 +225,25 @@ func CmdFmtRun(cmd *cobra.Command, args []string) {
190225
}
191226
}
192227

193-
func applyRun(ctx context.Context, client *sqlclient.Client, file string, dryRun, autoApprove bool, input map[string]string) {
228+
func applyRun(ctx context.Context, client *sqlclient.Client, devURL string, file string, dryRun, autoApprove bool, input map[string]string) error {
194229
schemas := ApplyFlags.Schema
195230
if client.URL.Schema != "" {
196231
schemas = append(schemas, client.URL.Schema)
197232
}
198233
realm, err := client.InspectRealm(ctx, &schema.InspectRealmOption{
199234
Schemas: schemas,
200235
})
201-
cobra.CheckErr(err)
236+
if err != nil {
237+
return err
238+
}
202239
f, err := ioutil.ReadFile(file)
203-
cobra.CheckErr(err)
240+
if err != nil {
241+
return err
242+
}
204243
desired := &schema.Realm{}
205-
cobra.CheckErr(client.Eval(f, desired, input))
244+
if err := client.Eval(f, desired, input); err != nil {
245+
return err
246+
}
206247
if len(schemas) > 0 {
207248
// Validate all schemas in file were selected by user.
208249
sm := make(map[string]bool, len(schemas))
@@ -211,26 +252,33 @@ func applyRun(ctx context.Context, client *sqlclient.Client, file string, dryRun
211252
}
212253
for _, s := range desired.Schemas {
213254
if !sm[s.Name] {
214-
schemaCmd.Printf("schema %q from file %q was not selected %q, all schemas defined in file must be selected\n", s.Name, file, schemas)
215-
return
255+
return fmt.Errorf("schema %q from file %q was not selected %q, all schemas defined in file must be selected", s.Name, file, schemas)
216256
}
217257
}
218258
}
219-
if _, ok := client.Driver.(schema.Normalizer); ok && ApplyFlags.DevURL != "" {
259+
if _, ok := client.Driver.(schema.Normalizer); ok && devURL != "" {
220260
dev, err := sqlclient.Open(ctx, ApplyFlags.DevURL)
221-
cobra.CheckErr(err)
261+
if err != nil {
262+
return err
263+
}
222264
defer dev.Close()
223265
desired, err = dev.Driver.(schema.Normalizer).NormalizeRealm(ctx, desired)
224-
cobra.CheckErr(err)
266+
if err != nil {
267+
return err
268+
}
225269
}
226270
changes, err := client.RealmDiff(realm, desired)
227-
cobra.CheckErr(err)
271+
if err != nil {
272+
return err
273+
}
228274
if len(changes) == 0 {
229275
schemaCmd.Println("Schema is synced, no changes to be made")
230-
return
276+
return nil
231277
}
232278
p, err := client.PlanChanges(ctx, "plan", changes)
233-
cobra.CheckErr(err)
279+
if err != nil {
280+
return err
281+
}
234282
schemaCmd.Println("-- Planned Changes:")
235283
for _, c := range p.Changes {
236284
if c.Comment != "" {
@@ -239,11 +287,14 @@ func applyRun(ctx context.Context, client *sqlclient.Client, file string, dryRun
239287
schemaCmd.Println(c.Cmd)
240288
}
241289
if dryRun {
242-
return
290+
return nil
243291
}
244292
if autoApprove || promptUser() {
245-
cobra.CheckErr(client.ApplyChanges(ctx, changes))
293+
if err := client.ApplyChanges(ctx, changes); err != nil {
294+
return err
295+
}
246296
}
297+
return nil
247298
}
248299

249300
func promptUser() bool {
@@ -256,12 +307,23 @@ func promptUser() bool {
256307
return result == answerApply
257308
}
258309

259-
func dsn2url(cmd *cobra.Command, p *string) {
310+
// fixURLFlag fixes the url flag by pulling its value either from the flag itself,
311+
// the (deprecated) dsn flag, or from the active environment.
312+
func fixURLFlag(cmd *cobra.Command, p *string) {
260313
cmd.Flags().StringVarP(p, "dsn", "d", "", "")
261314
cobra.CheckErr(cmd.Flags().MarkHidden("dsn"))
262315
cmd.PreRunE = func(cmd *cobra.Command, args []string) error {
316+
activeEnv, err := selectEnv(args)
317+
if err != nil {
318+
return err
319+
}
263320
dsnF, urlF := cmd.Flag("dsn"), cmd.Flag("url")
264321
switch {
322+
case activeEnv != nil && activeEnv.URL != "":
323+
urlF.Changed = true
324+
if err := urlF.Value.Set(activeEnv.URL); err != nil {
325+
return err
326+
}
265327
case !dsnF.Changed && !urlF.Changed:
266328
return errors.New(`required flag "url" was not set`)
267329
case dsnF.Changed && urlF.Changed:

0 commit comments

Comments
 (0)