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 }