github.com/willyham/dosa@v2.3.1-0.20171024181418-1e446d37ee71+incompatible/cmd/dosa/schema.go (about)

     1  // Copyright (c) 2017 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package main
    22  
    23  import (
    24  	"context"
    25  	"fmt"
    26  	"os"
    27  	"path/filepath"
    28  	"strings"
    29  
    30  	"github.com/pkg/errors"
    31  	"github.com/uber-go/dosa"
    32  	"github.com/uber-go/dosa/connectors/devnull"
    33  	"github.com/uber-go/dosa/schema/avro"
    34  	"github.com/uber-go/dosa/schema/cql"
    35  	"github.com/uber-go/dosa/schema/uql"
    36  )
    37  
    38  var (
    39  	schemaDumpOutputTypes = map[string]bool{
    40  		"cql":  true,
    41  		"uql":  true,
    42  		"avro": true,
    43  	}
    44  )
    45  
    46  type scopeFlag string
    47  
    48  func (s *scopeFlag) setString(value string) {
    49  	*s = scopeFlag(strings.Replace(value, ".", "_", -1))
    50  }
    51  
    52  // String implements the stringer interface
    53  func (s *scopeFlag) String() string {
    54  	return string(*s)
    55  }
    56  
    57  func (s *scopeFlag) UnmarshalFlag(value string) error {
    58  	s.setString(value)
    59  	return nil
    60  }
    61  
    62  // SchemaOptions contains configuration for schema command flags.
    63  type SchemaOptions struct {
    64  	Excludes []string `short:"e" long:"exclude" description:"Exclude files matching pattern."`
    65  	Verbose  bool     `short:"v" long:"verbose"`
    66  }
    67  
    68  // SchemaCmd is a placeholder for all schema commands
    69  type SchemaCmd struct {
    70  	*SchemaOptions
    71  	Scope      scopeFlag `short:"s" long:"scope" description:"Storage scope for the given operation."`
    72  	NamePrefix string    `long:"prefix" description:"Name prefix for schema types." required:"true"`
    73  }
    74  
    75  func (c *SchemaCmd) doSchemaOp(name string, f func(dosa.AdminClient, context.Context, string) (*dosa.SchemaStatus, error), args []string) error {
    76  	if c.Verbose {
    77  		fmt.Printf("executing %s with %v\n", name, args)
    78  		fmt.Printf("options are %+v\n", *c)
    79  		fmt.Printf("global options are %+v\n", options)
    80  	}
    81  
    82  	// if not given, set the service name dynamically based on scope
    83  	if options.ServiceName == "" {
    84  		options.ServiceName = _defServiceName
    85  		if c.Scope == _prodScope {
    86  			options.ServiceName = _prodServiceName
    87  		}
    88  	}
    89  
    90  	client, err := getAdminClient(options)
    91  	if err != nil {
    92  		return err
    93  	}
    94  	if len(args) != 0 {
    95  		dirs, err := expandDirectories(args)
    96  		if err != nil {
    97  			return errors.Wrap(err, "could not expand directories")
    98  		}
    99  		client.Directories(dirs)
   100  	}
   101  	if len(c.Excludes) != 0 {
   102  		client.Excludes(c.Excludes)
   103  	}
   104  	if c.Scope != "" {
   105  		client.Scope(c.Scope.String())
   106  	}
   107  
   108  	ctx, cancel := context.WithTimeout(context.Background(), options.Timeout.Duration())
   109  	defer cancel()
   110  
   111  	status, err := f(client, ctx, c.NamePrefix)
   112  	if err != nil {
   113  		if c.Verbose {
   114  			fmt.Printf("detail:%+v\n", err)
   115  		}
   116  		fmt.Println("Status: NOT OK")
   117  		return err
   118  	}
   119  	fmt.Printf("Version: %d\n", status.Version)
   120  	fmt.Printf("Status: %s\n", status.Status)
   121  	return nil
   122  }
   123  
   124  // SchemaCheck holds the options for 'schema check'
   125  type SchemaCheck struct {
   126  	*SchemaCmd
   127  	Args struct {
   128  		Paths []string `positional-arg-name:"paths"`
   129  	} `positional-args:"yes"`
   130  }
   131  
   132  // Execute executes a schema check command
   133  func (c *SchemaCheck) Execute(args []string) error {
   134  	return c.doSchemaOp("schema check", dosa.AdminClient.CheckSchema, c.Args.Paths)
   135  }
   136  
   137  // SchemaUpsert contains data for executing schema upsert command.
   138  type SchemaUpsert struct {
   139  	*SchemaCmd
   140  	Args struct {
   141  		Paths []string `positional-arg-name:"paths"`
   142  	} `positional-args:"yes"`
   143  }
   144  
   145  // Execute executes a schema upsert command
   146  func (c *SchemaUpsert) Execute(args []string) error {
   147  	return c.doSchemaOp("schema upsert", dosa.AdminClient.UpsertSchema, c.Args.Paths)
   148  }
   149  
   150  // SchemaStatus contains data for executing schema status command
   151  type SchemaStatus struct {
   152  	*SchemaCmd
   153  	Version int32 `long:"version" description:"Specify schema version."`
   154  }
   155  
   156  // Execute executes a schema status command
   157  func (c *SchemaStatus) Execute(args []string) error {
   158  	if c.Verbose {
   159  		fmt.Printf("executing schema status with %v\n", args)
   160  		fmt.Printf("options are %+v\n", *c)
   161  		fmt.Printf("global options are %+v\n", options)
   162  	}
   163  
   164  	// if not given, set the service name dynamically based on scope
   165  	if options.ServiceName == "" {
   166  		options.ServiceName = _defServiceName
   167  		if c.Scope == _prodScope {
   168  			options.ServiceName = _prodServiceName
   169  		}
   170  	}
   171  
   172  	client, err := getAdminClient(options)
   173  	if err != nil {
   174  		return err
   175  	}
   176  
   177  	if c.Scope.String() != "" {
   178  		client.Scope(c.Scope.String())
   179  	}
   180  
   181  	ctx, cancel := context.WithTimeout(context.Background(), options.Timeout.Duration())
   182  	defer cancel()
   183  
   184  	status, err := client.CheckSchemaStatus(ctx, c.NamePrefix, c.Version)
   185  	if err != nil {
   186  		if c.Verbose {
   187  			fmt.Printf("detail:%+v\n", err)
   188  		}
   189  		fmt.Println("Status: NOT OK")
   190  		return err
   191  	}
   192  	fmt.Printf("Version: %d\n", status.Version)
   193  	fmt.Printf("Status: %s\n", status.Status)
   194  	return nil
   195  }
   196  
   197  // SchemaDump contains data for executing the schema dump command
   198  type SchemaDump struct {
   199  	*SchemaOptions
   200  	Format string `long:"format" short:"f" description:"output format" choice:"cql" choice:"uql" choice:"avro" default:"cql"`
   201  	Args   struct {
   202  		Paths []string `positional-arg-name:"paths"`
   203  	} `positional-args:"yes"`
   204  }
   205  
   206  // Execute executes a schema dump command
   207  func (c *SchemaDump) Execute(args []string) error {
   208  	if c.Verbose {
   209  		fmt.Printf("executing schema dump with %v\n", args)
   210  		fmt.Printf("options are %+v\n", *c)
   211  		fmt.Printf("global options are %+v\n", options)
   212  	}
   213  
   214  	// no connection necessary
   215  	client := dosa.NewAdminClient(&devnull.Connector{})
   216  	if len(c.Args.Paths) != 0 {
   217  		dirs, err := expandDirectories(c.Args.Paths)
   218  		if err != nil {
   219  			return errors.Wrap(err, "could not expand directories")
   220  		}
   221  		client.Directories(dirs)
   222  	}
   223  	if len(c.Excludes) != 0 {
   224  		client.Excludes(c.Excludes)
   225  	}
   226  
   227  	// try to parse entities in each directory
   228  	defs, err := client.GetSchema()
   229  	if err != nil {
   230  		return err
   231  	}
   232  
   233  	// for each of those entities, format it in the specified way
   234  	for _, d := range defs {
   235  		switch c.Format {
   236  		case "cql":
   237  			fmt.Println(cql.ToCQL(d))
   238  		case "uql":
   239  			fmt.Println(uql.ToUQL(d))
   240  		case "avro":
   241  			s, err := avro.ToAvro("TODO", d)
   242  			fmt.Println(string(s), err)
   243  		}
   244  	}
   245  
   246  	return nil
   247  }
   248  
   249  // expandDirectory verifies that each argument is actually a directory or
   250  // uses the special go suffix of /... to mean recursively walk from here
   251  // example: ./... means the current directory and all subdirectories
   252  func expandDirectories(dirs []string) ([]string, error) {
   253  	const recursiveMarker = "/..."
   254  	resultSet := make([]string, 0)
   255  	for _, dir := range dirs {
   256  		if strings.HasSuffix(dir, recursiveMarker) {
   257  			err := filepath.Walk(strings.TrimSuffix(dir, recursiveMarker), func(path string, info os.FileInfo, err error) error {
   258  				if info.IsDir() {
   259  					resultSet = append(resultSet, path)
   260  				}
   261  				return nil
   262  			})
   263  			if err != nil {
   264  				return nil, err
   265  			}
   266  		} else {
   267  			info, err := os.Stat(dir)
   268  			if err != nil {
   269  				return nil, err
   270  			}
   271  			if !info.IsDir() {
   272  				return nil, fmt.Errorf("%q is not a directory", dir)
   273  			}
   274  			resultSet = append(resultSet, dir)
   275  		}
   276  	}
   277  	if len(resultSet) == 0 {
   278  		// treat an empty list as a search in the current directory (think "ls")
   279  		return []string{"."}, nil
   280  	}
   281  
   282  	return resultSet, nil
   283  }