1 Star 0 Fork 0

DaMeng/Atlas

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
文件
克隆/下载
project.go 16.12 KB
一键复制 编辑 原始数据 按行查看 历史
DaMeng 提交于 2024-10-24 15:32 +08:00 . :art:修改mod名称
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591
// Copyright 2021-present The Atlas Authors. All rights reserved.
// This source code is licensed under the Apache 2.0 license found
// in the LICENSE file in the root directory of this source tree.
package cmdapi
import (
"fmt"
"net/url"
"os"
"path/filepath"
"reflect"
"strings"
"sync"
"gitee.com/damengde/atlas/cmd/atlas/internal/cloudapi"
"gitee.com/damengde/atlas/cmd/atlas/internal/cmdext"
cmdmigrate "gitee.com/damengde/atlas/cmd/atlas/internal/migrate"
"gitee.com/damengde/atlas/schemahcl"
"gitee.com/damengde/atlas/sql/schema"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclparse"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/spf13/cobra"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
)
type (
// Project represents an atlas.hcl project config file.
Project struct {
Envs []*Env `spec:"env"` // List of environments
Lint *Lint `spec:"lint"` // Optional global lint policy
Diff *Diff `spec:"diff"` // Optional global diff policy
cfg *cmdext.AtlasConfig
}
// Env represents an Atlas environment.
Env struct {
// Name for this environment.
Name string `spec:"name,name"`
// URL of the database.
URL string `spec:"url"`
// URL of the dev-database for this environment.
// See: https://atlasgo.io/dev-database
DevURL string `spec:"dev"`
// List of schemas in this database that are managed by Atlas.
Schemas []string `spec:"schemas"`
// Exclude defines a list of glob patterns used to filter
// resources on inspection.
Exclude []string `spec:"exclude"`
// Migration containing the migration configuration of the env.
Migration *Migration `spec:"migration"`
// Diff policy of the environment.
Diff *Diff `spec:"diff"`
// Lint policy of the environment.
Lint *Lint `spec:"lint"`
// Format of the environment.
Format Format `spec:"format"`
schemahcl.DefaultExtension
cfg *cmdext.AtlasConfig
}
// Migration represents the migration directory for the Env.
Migration struct {
Dir string `spec:"dir"`
Format string `spec:"format"`
Baseline string `spec:"baseline"`
ExecOrder string `spec:"exec_order"`
LockTimeout string `spec:"lock_timeout"`
RevisionsSchema string `spec:"revisions_schema"`
}
// Lint represents the configuration of migration linting.
Lint struct {
// Format configures the --format option.
Format string `spec:"log"`
// Latest configures the --latest option.
Latest int `spec:"latest"`
Git struct {
// Dir configures the --git-dir option.
Dir string `spec:"dir"`
// Base configures the --git-base option.
Base string `spec:"base"`
} `spec:"git"`
// Review defines when Atlas will ask the user to review and approve the changes.
Review string `spec:"review"`
schemahcl.DefaultExtension
}
// Diff represents the schema diffing policy.
Diff struct {
// SkipChanges configures the skip changes policy.
SkipChanges *SkipChanges `spec:"skip"`
schemahcl.DefaultExtension
}
// SkipChanges represents the skip changes policy.
SkipChanges struct {
AddSchema bool `spec:"add_schema"`
DropSchema bool `spec:"drop_schema"`
ModifySchema bool `spec:"modify_schema"`
AddTable bool `spec:"add_table"`
DropTable bool `spec:"drop_table"`
ModifyTable bool `spec:"modify_table"`
RenameTable bool `spec:"rename_table"`
AddColumn bool `spec:"add_column"`
DropColumn bool `spec:"drop_column"`
ModifyColumn bool `spec:"modify_column"`
AddIndex bool `spec:"add_index"`
DropIndex bool `spec:"drop_index"`
ModifyIndex bool `spec:"modify_index"`
AddForeignKey bool `spec:"add_foreign_key"`
DropForeignKey bool `spec:"drop_foreign_key"`
ModifyForeignKey bool `spec:"modify_foreign_key"`
AddView bool `spec:"add_view"`
DropView bool `spec:"drop_view"`
ModifyView bool `spec:"modify_view"`
RenameView bool `spec:"rename_view"`
AddFunc bool `spec:"add_func"`
DropFunc bool `spec:"drop_func"`
ModifyFunc bool `spec:"modify_func"`
RenameFunc bool `spec:"rename_func"`
AddProc bool `spec:"add_proc"`
DropProc bool `spec:"drop_proc"`
ModifyProc bool `spec:"modify_proc"`
RenameProc bool `spec:"rename_proc"`
AddTrigger bool `spec:"add_trigger"`
DropTrigger bool `spec:"drop_trigger"`
ModifyTrigger bool `spec:"modify_trigger"`
RenameTrigger bool `spec:"rename_trigger"`
}
// Format represents the output formatting configuration of an environment.
Format struct {
Migrate struct {
// Apply configures the formatting for 'migrate apply'.
Apply string `spec:"apply"`
// Lint configures the formatting for 'migrate lint'.
Lint string `spec:"lint"`
// Status configures the formatting for 'migrate status'.
Status string `spec:"status"`
// Apply configures the formatting for 'migrate diff'.
Diff string `spec:"diff"`
} `spec:"migrate"`
Schema struct {
// Apply configures the formatting for 'schema apply'.
Apply string `spec:"apply"`
// Apply configures the formatting for 'schema diff'.
Diff string `spec:"diff"`
} `spec:"schema"`
schemahcl.DefaultExtension
}
)
// envScheme defines the scheme that can be used to reference env attributes.
const envAttrScheme = "env"
// VarFromURL returns the string variable (env attribute) from the URL.
func (e *Env) VarFromURL(s string) (string, error) {
u, err := url.Parse(s)
if err != nil {
return "", err
}
if u.Host == "" || u.Path != "" || u.RawQuery != "" {
return "", fmt.Errorf("invalid env:// variable %q", s)
}
var sv string
switch u.Host {
case "url":
sv = e.URL
case "dev":
sv = e.DevURL
default:
attr, ok := e.Attr(u.Host)
if !ok {
return "", fmt.Errorf("env://%s (attribute) not found in env.%s", u.Host, e.Name)
}
if sv, err = attr.String(); err != nil {
return "", fmt.Errorf("env://%s: %w", u.Host, err)
}
}
if strings.HasPrefix(sv, envAttrScheme+"://") {
return "", fmt.Errorf("env://%s (attribute) cannot reference another env://", s)
}
return sv, nil
}
// support backward compatibility with the 'log' attribute.
func (e *Env) remainedLog() error {
r, ok := e.Remain().Resource("log")
if ok {
return r.As(&e.Format)
}
return nil
}
// Extend allows extending environment blocks with
// a global one. For example:
//
// lint {
// log = <<EOS
// ...
// EOS
// }
//
// env "local" {
// ...
// lint {
// latest = 1
// }
// }
//
// env "ci" {
// ...
// lint {
// git {
// dir = "../"
// base = "master"
// }
// }
// }
func (l *Lint) Extend(global *Lint) *Lint {
if l == nil {
return global
}
if l.Review == "" {
l.Review = global.Review
}
if l.Format == "" {
l.Format = global.Format
}
l.Extra = global.Extra
switch {
// Changes detector was configured on the env.
case l.Git.Dir != "" && l.Git.Base != "" || l.Latest != 0:
// Inherit global git detection.
case global.Git.Dir != "" || global.Git.Base != "":
if global.Git.Dir != "" {
l.Git.Dir = global.Git.Dir
}
if global.Git.Base != "" {
l.Git.Base = global.Git.Base
}
// Inherit latest files configuration.
case global.Latest != 0:
l.Latest = global.Latest
}
return l
}
// support backward compatibility with the 'log' attribute.
func (l *Lint) remainedLog() error {
at, ok := l.Remain().Attr("log")
if !ok {
return nil
}
if l.Format != "" {
return fmt.Errorf("cannot use both 'log' and 'format' in the same lint block")
}
s, err := at.String()
if err != nil {
return err
}
l.Format = s
return nil
}
// Extend allows extending environment blocks with
// a global one. For example:
//
// diff {
// skip {
// drop_schema = true
// }
// }
//
// env "local" {
// ...
// diff {
// concurrent_index {
// create = true
// drop = true
// }
// }
// }
func (d *Diff) Extend(global *Diff) *Diff {
if d == nil {
return global
}
if d.SkipChanges == nil {
d.SkipChanges = global.SkipChanges
}
return d
}
// Options converts the diff policy into options.
func (d *Diff) Options() (opts []schema.DiffOption) {
// Per-driver configuration.
opts = append(opts, func(opts *schema.DiffOptions) {
opts.Extra = d.DefaultExtension
})
if d.SkipChanges == nil {
return
}
var (
changes schema.Changes
rv = reflect.ValueOf(d.SkipChanges).Elem()
)
for _, c := range []schema.Change{
&schema.AddSchema{}, &schema.DropSchema{}, &schema.ModifySchema{},
&schema.AddView{}, &schema.DropView{}, &schema.ModifyView{}, &schema.RenameView{},
&schema.AddFunc{}, &schema.DropFunc{}, &schema.ModifyFunc{}, &schema.RenameFunc{},
&schema.AddProc{}, &schema.DropProc{}, &schema.ModifyProc{}, &schema.RenameProc{},
&schema.AddTrigger{}, &schema.DropTrigger{}, &schema.ModifyTrigger{}, &schema.RenameTrigger{},
&schema.AddTable{}, &schema.DropTable{}, &schema.ModifyTable{}, &schema.RenameTable{},
&schema.AddColumn{}, &schema.DropColumn{}, &schema.ModifyColumn{}, &schema.AddIndex{},
&schema.DropIndex{}, &schema.ModifyIndex{}, &schema.AddForeignKey{}, &schema.DropForeignKey{},
&schema.ModifyForeignKey{},
} {
if rt := reflect.TypeOf(c).Elem(); rv.FieldByName(rt.Name()).Bool() {
changes = append(changes, c)
}
}
if len(changes) > 0 {
opts = append(opts, schema.DiffSkipChanges(changes...))
}
return opts
}
// DiffOptions returns the diff options configured for the environment,
// or nil if no environment or diff policy were set.
func (e *Env) DiffOptions() []schema.DiffOption {
if e == nil || e.Diff == nil {
return nil
}
return e.Diff.Options()
}
// Sources returns the paths containing the Atlas schema.
func (e *Env) Sources() ([]string, error) {
attr, exists := e.Attr("src")
if !exists {
return nil, nil
}
switch attr.V.Type() {
case cty.String:
s, err := attr.String()
if err != nil {
return nil, err
}
return []string{s}, nil
case cty.List(cty.String):
return attr.Strings()
default:
return nil, fmt.Errorf("expected src to be either a string or strings, got: %s", attr.V.Type().FriendlyName())
}
}
// asMap returns the extra attributes stored in the Env as a map[string]string.
func (e *Env) asMap() (map[string]string, error) {
m := make(map[string]string, len(e.Extra.Attrs))
for _, attr := range e.Extra.Attrs {
if attr.K == "src" {
continue
}
if v, err := attr.String(); err == nil {
m[attr.K] = v
continue
}
return nil, fmt.Errorf("expecting attr %q to be a literal, got: %T", attr.K, attr.V)
}
return m, nil
}
// EnvByName parses and returns the project configuration with selected environments.
func EnvByName(cmd *cobra.Command, name string, vars map[string]cty.Value) (*Project, []*Env, error) {
envs := make(map[string][]*Env)
defer func() {
setEnvs(cmd.Context(), envs[name])
}()
if p, e, ok := envsCache.load(GlobalFlags.ConfigURL, name, vars); ok {
return p, e, nil
}
u, err := url.Parse(GlobalFlags.ConfigURL)
if err != nil {
return nil, nil, err
}
if u.Scheme != "file" {
return nil, nil, fmt.Errorf("unsupported config file driver %q", u.Scheme)
}
path := filepath.Join(u.Host, u.Path)
if _, err := os.Stat(path); err != nil {
if os.IsNotExist(err) {
err = fmt.Errorf("config file %q was not found: %w", path, err)
}
return nil, nil, err
}
project, err := parseConfig(path, name, vars)
if err != nil {
return nil, nil, err
}
// The project token predates 'atlas login' command. If exists,
// attach it to the context to indicate the user is authenticated.
if project.cfg.Token != "" && project.cfg.Client != nil {
ctx, err := withTokenContext(cmd.Context(), project.cfg.Token, project.cfg.Client)
if err != nil {
return nil, nil, err
}
cmd.SetContext(ctx)
}
if err := project.Lint.remainedLog(); err != nil {
return nil, nil, err
}
for _, e := range project.Envs {
if e.Name == "" {
return nil, nil, fmt.Errorf("all envs must have names on file %q", path)
}
if _, err := e.Sources(); err != nil {
return nil, nil, err
}
if e.Migration == nil {
e.Migration = &Migration{}
}
if err := e.remainedLog(); err != nil {
return nil, nil, err
}
e.Diff = e.Diff.Extend(project.Diff)
e.Lint = e.Lint.Extend(project.Lint)
if err := e.Lint.remainedLog(); err != nil {
return nil, nil, err
}
envs[e.Name] = append(envs[e.Name], e)
}
envsCache.store(GlobalFlags.ConfigURL, name, vars, project, envs[name])
switch {
case name == "":
// If no env was selected,
// return only the project.
return project, nil, nil
case len(envs[name]) == 0:
return nil, nil, fmt.Errorf("env %q not defined in project file", name)
default:
return project, envs[name], nil
}
}
type (
envCacheK struct {
path, env, vars string
}
envCacheV struct {
p *Project
e []*Env
}
envCache struct {
sync.RWMutex
m map[envCacheK]envCacheV
}
)
var envsCache = &envCache{m: make(map[envCacheK]envCacheV)}
func (c *envCache) load(path, env string, vars Vars) (*Project, []*Env, bool) {
c.RLock()
v, ok := c.m[envCacheK{path: path, env: env, vars: vars.String()}]
c.RUnlock()
return v.p, v.e, ok
}
func (c *envCache) store(path, env string, vars Vars, p *Project, e []*Env) {
c.Lock()
c.m[envCacheK{path: path, env: env, vars: vars.String()}] = envCacheV{p: p, e: e}
c.Unlock()
}
const (
blockEnv = "env"
refAtlas = "atlas"
defaultConfigPath = "file://atlas.hcl"
)
func parseConfig(path, env string, vars map[string]cty.Value) (*Project, error) {
pr, err := partialParse(path, env)
if err != nil {
return nil, err
}
base, err := filepath.Abs(filepath.Dir(path))
if err != nil {
return nil, err
}
cfg := &cmdext.AtlasConfig{
Project: cloudapi.DefaultProjectName,
}
state := schemahcl.New(
append(
append(cmdext.SpecOptions, specOptions...),
cfg.InitBlock(),
schemahcl.WithScopedEnums("env.migration.format", cmdmigrate.Formats...),
schemahcl.WithScopedEnums("env.migration.exec_order", "LINEAR", "LINEAR_SKIP", "NON_LINEAR"),
schemahcl.WithScopedEnums("env.lint.review", ReviewModes...),
schemahcl.WithScopedEnums("lint.review", ReviewModes...),
schemahcl.WithVariables(map[string]cty.Value{
refAtlas: cty.ObjectVal(map[string]cty.Value{
blockEnv: cty.StringVal(env),
}),
}),
schemahcl.WithFunctions(map[string]function.Function{
"file": schemahcl.MakeFileFunc(base),
"getenv": function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "key",
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.String),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
return cty.StringVal(os.Getenv(args[0].AsString())), nil
},
}),
}),
)...,
)
p := &Project{Lint: &Lint{}, Diff: &Diff{}, cfg: cfg}
if err := state.Eval(pr, p, vars); err != nil {
return nil, err
}
for _, e := range p.Envs {
e.cfg = cfg
}
return p, nil
}
func init() {
cloudapi.SetVersion(version, flavor)
schemahcl.Register(blockEnv, &Env{})
}
func partialParse(path, env string) (*hclparse.Parser, error) {
parser := hclparse.NewParser()
fi, err := parser.ParseHCLFile(path)
if err != nil {
return nil, err
}
var used []*hclsyntax.Block
for _, b := range fi.Body.(*hclsyntax.Body).Blocks {
switch b.Type {
case blockEnv:
switch n := len(b.Labels); {
// No env was selected.
case env == "" && n == 0:
// Exact env was selected.
case n == 1 && b.Labels[0] == env:
used = append(used, b)
// Dynamic env selection.
case n == 0 && b.Body != nil && b.Body.Attributes[schemahcl.AttrName] != nil:
t, ok := b.Body.Attributes[schemahcl.AttrName].Expr.(*hclsyntax.ScopeTraversalExpr)
if ok && len(t.Traversal) == 2 && t.Traversal.RootName() == refAtlas && t.Traversal[1].(hcl.TraverseAttr).Name == blockEnv {
used = append(used, b)
}
}
default:
used = append(used, b)
}
}
fi.Body = &hclsyntax.Body{
Blocks: used,
Attributes: fi.Body.(*hclsyntax.Body).Attributes,
}
return parser, nil
}
// Review modes for 'schema apply'.
const (
ReviewAlways = "ALWAYS" // Always review changes. The default mode.
ReviewWarning = "WARNING" // Review changes only if there are any diagnostics (including warnings).
ReviewError = "ERROR" // Review changes only if there are severe diagnostics (error level).
)
var ReviewModes = []string{ReviewAlways, ReviewWarning, ReviewError}
Loading...
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
1
https://gitee.com/damengde/atlas.git
git@gitee.com:damengde/atlas.git
damengde
atlas
Atlas
13ce33acf38b

搜索帮助