github.com/mavryk-network/mvgo@v1.19.9/internal/compose/alpha/spec.go (about) 1 // Copyright (c) 2023 Blockwatch Data Inc. 2 // Author: alex@blockwatch.cc, abdul@blockwatch.cc 3 4 package alpha 5 6 import ( 7 "encoding/hex" 8 "encoding/json" 9 "fmt" 10 "hash/fnv" 11 "net/url" 12 "os" 13 "path/filepath" 14 "strings" 15 16 "github.com/mavryk-network/mvgo/internal/compose" 17 "github.com/mavryk-network/mvgo/mavryk" 18 "github.com/mavryk-network/mvgo/micheline" 19 20 "gopkg.in/yaml.v3" 21 ) 22 23 type Spec struct { 24 Version string `yaml:"version"` 25 Accounts []Account `yaml:"accounts,omitempty"` 26 Variables map[string]string `yaml:"variables,omitempty"` 27 Pipelines PipelineList `yaml:"pipelines"` 28 } 29 30 func (s Spec) Validate(ctx compose.Context) error { 31 if s.Version == "" { 32 return compose.ErrNoVersion 33 } 34 if len(s.Pipelines) == 0 { 35 return compose.ErrNoPipeline 36 } 37 for _, v := range s.Pipelines { 38 if err := v.Validate(ctx); err != nil { 39 return fmt.Errorf("pipeline %s: %v", v.Name, err) 40 } 41 } 42 return nil 43 } 44 45 type Account struct { 46 Name string `yaml:"name"` 47 Id uint `yaml:"id,omitempty"` 48 } 49 50 type Pipeline struct { 51 Name string `yaml:"-"` 52 Tasks []Task `yaml:",inline"` 53 } 54 55 type PipelineList []Pipeline 56 57 func (l *PipelineList) UnmarshalYAML(node *yaml.Node) error { 58 // decode named map into list 59 *l = make([]Pipeline, len(node.Content)/2) 60 for i, v := range node.Content { 61 switch v.Kind { 62 case yaml.ScalarNode: 63 (*l)[i/2].Name = v.Value 64 case yaml.SequenceNode: 65 if err := v.Decode(&(*l)[i/2].Tasks); err != nil { 66 return err 67 } 68 default: 69 return fmt.Errorf("unexpected yaml node kind=%s value=%s", v.Tag[2:], v.Value) 70 } 71 } 72 73 return nil 74 } 75 76 func (l PipelineList) MarshalYAML() (any, error) { 77 // manualy create a list of named map nodes 78 node := &yaml.Node{ 79 Kind: yaml.MappingNode, 80 Tag: "!!map", 81 Content: []*yaml.Node{}, 82 } 83 for _, v := range l { 84 var child yaml.Node 85 if err := child.Encode(v.Tasks); err != nil { 86 return nil, err 87 } 88 node.Content = append(node.Content, 89 // key 90 &yaml.Node{ 91 Kind: yaml.ScalarNode, 92 Tag: "!!str", 93 Value: v.Name, 94 }, 95 // value 96 &child, 97 ) 98 } 99 return node, nil 100 } 101 102 func (p Pipeline) Hash64() uint64 { 103 h := fnv.New64() 104 enc := json.NewEncoder(h) 105 enc.Encode(p) 106 return h.Sum64() 107 } 108 109 func (p Pipeline) Len() int { 110 return len(p.Tasks) 111 } 112 113 func (p Pipeline) Validate(ctx compose.Context) error { 114 if p.Name == "" { 115 return compose.ErrNoPipelineName 116 } 117 for i, v := range p.Tasks { 118 if err := v.Validate(ctx); err != nil { 119 return fmt.Errorf("task %d (%s): %v", i, v.Type, err) 120 } 121 } 122 return nil 123 } 124 125 type Task struct { 126 Type string `yaml:"task"` 127 Alias string `yaml:"alias,omitempty"` 128 Skip bool `yaml:"skip,omitempty"` 129 Amount uint64 `yaml:"amount,omitempty"` 130 Script *Script `yaml:"script,omitempty"` // deploy only 131 Params *Params `yaml:"params,omitempty"` // call only 132 Source string `yaml:"source,omitempty"` 133 Destination string `yaml:"destination,omitempty"` 134 Args map[string]any `yaml:"args,omitempty"` // token_* only 135 Contents []Task `yaml:"contents,omitempty"` // batch only 136 WaitMode WaitMode `yaml:"for,omitempty"` // wait only 137 Value string `yaml:"value,omitempty"` // wait only 138 Log string `yaml:"log,omitempty"` // log level override 139 OnError ErrorMode `yaml:"on_error,omitempty"` // how to handle errors: fail|warn|ignore 140 } 141 142 func (t Task) Validate(ctx compose.Context) error { 143 if t.Type == "" { 144 return compose.ErrNoTaskType 145 } 146 if t.Params != nil { 147 if err := t.Params.Validate(ctx); err != nil { 148 return fmt.Errorf("params: %v", err) 149 } 150 } 151 if _, ok := ctx.Variables[t.Alias]; !ok && t.Alias != "" { 152 ctx.AddVariable(t.Alias, mavryk.ZeroAddress.String()) 153 } 154 if t.Script != nil { 155 if err := t.Script.Validate(ctx); err != nil { 156 return fmt.Errorf("script: %v", err) 157 } 158 } 159 if t.Type == "batch" { 160 if len(t.Contents) == 0 { 161 return fmt.Errorf("empty contents") 162 } 163 for _, v := range t.Contents { 164 if v.Type == "batch" { 165 return fmt.Errorf("nested batch tasks not allowed") 166 } 167 if v.Contents != nil { 168 return fmt.Errorf("nested contents not allowed") 169 } 170 if v.Source != "" && v.Source != t.Source { 171 return fmt.Errorf("switching source is not allowed in batch contents") 172 } 173 if err := v.Validate(ctx); err != nil { 174 return fmt.Errorf("contents: %v", err) 175 } 176 } 177 } else if t.Contents != nil { 178 return fmt.Errorf("contents only allowed in batch tasks") 179 } 180 return nil 181 } 182 183 type ErrorMode byte 184 185 const ( 186 ErrorModeFail ErrorMode = iota 187 ErrorModeWarn 188 ErrorModeIgnore 189 ) 190 191 func (m *ErrorMode) UnmarshalYAML(node *yaml.Node) error { 192 return m.UnmarshalText([]byte(node.Value)) 193 } 194 195 func (m *ErrorMode) UnmarshalText(buf []byte) error { 196 switch string(buf) { 197 case "fail": 198 *m = ErrorModeFail 199 case "warn": 200 *m = ErrorModeWarn 201 case "ignore": 202 *m = ErrorModeIgnore 203 default: 204 return fmt.Errorf("invalid error mode %q", string(buf)) 205 } 206 return nil 207 } 208 209 type WaitMode byte 210 211 const ( 212 WaitModeInvalid WaitMode = iota 213 WaitModeCycle 214 WaitModeBlock 215 WaitModeTime 216 ) 217 218 func (m *WaitMode) UnmarshalYAML(node *yaml.Node) error { 219 return m.UnmarshalText([]byte(node.Value)) 220 } 221 222 func (m *WaitMode) UnmarshalText(buf []byte) error { 223 switch string(buf) { 224 case "cycle": 225 *m = WaitModeCycle 226 case "block": 227 *m = WaitModeBlock 228 case "time": 229 *m = WaitModeTime 230 case "": 231 *m = WaitModeInvalid 232 default: 233 return fmt.Errorf("invalid wait mode %q", string(buf)) 234 } 235 return nil 236 } 237 238 type ValueSource struct { 239 File string `yaml:"file,omitempty"` 240 Url string `yaml:"url,omitempty"` 241 Value string `yaml:"value,omitempty"` 242 } 243 244 func (v ValueSource) IsUsed() bool { 245 return len(v.Url)+len(v.File)+len(v.Value) > 0 246 } 247 248 func (v ValueSource) Validate(ctx compose.Context) error { 249 if !v.IsUsed() { 250 return fmt.Errorf("required file, url or value") 251 } 252 switch { 253 case v.Url != "": 254 if u, err := url.Parse(v.Url); err != nil { 255 return fmt.Errorf("url: %v", err) 256 } else if u.Host == "" { 257 return fmt.Errorf("missing url host") 258 } 259 case v.File != "": 260 fname, _, _ := strings.Cut(v.File, "#") 261 fname = filepath.Join(ctx.Filepath(), fname) 262 if _, err := os.Stat(fname); err != nil { 263 return fmt.Errorf("file: %v", err) 264 } 265 case v.Value != "": 266 if !isHex(v.Value) { 267 var x any 268 if err := json.Unmarshal([]byte(v.Value), &x); err != nil { 269 return fmt.Errorf("json value: %v", err) 270 } 271 } else { 272 buf, err := hex.DecodeString(v.Value) 273 if err != nil { 274 return fmt.Errorf("bin value: %v", err) 275 } 276 var prim micheline.Prim 277 if err := prim.UnmarshalBinary(buf); err != nil { 278 return fmt.Errorf("bin value: %v", err) 279 } 280 } 281 } 282 return nil 283 } 284 285 type Script struct { 286 ValueSource `yaml:",inline"` 287 Code *Code `yaml:"code,omitempty"` 288 Storage *Storage `yaml:"storage,omitempty"` 289 } 290 291 type Code struct { 292 ValueSource `yaml:",inline"` 293 // Patch []Patch `yaml:"patch,omitempty"` 294 } 295 296 type Storage struct { 297 ValueSource `yaml:",inline"` 298 Args any `yaml:"args,omitempty"` 299 Patch []Patch `yaml:"patch,omitempty"` 300 } 301 302 func (s Script) Validate(ctx compose.Context) error { 303 // either script-level source or both storage+code sources are required 304 if s.ValueSource.IsUsed() { 305 if err := s.ValueSource.Validate(ctx); err != nil { 306 return err 307 } 308 } else { 309 if s.Code == nil { 310 return fmt.Errorf("missing code section") 311 } 312 if err := s.Code.ValueSource.Validate(ctx); err != nil { 313 return err 314 } 315 if s.Storage == nil { 316 return fmt.Errorf("missing storage section") 317 } 318 if s.Storage.Args == nil { 319 // without args, storage must come from a valid source 320 if err := s.Storage.ValueSource.Validate(ctx); err != nil { 321 return fmt.Errorf("storage: %v", err) 322 } 323 } 324 } 325 // if s.Code != nil { 326 // for _, v := range s.Code.Patch { 327 // if err := v.Validate(ctx); err != nil { 328 // return err 329 // } 330 // } 331 // } 332 if s.Storage != nil { 333 for _, v := range s.Storage.Patch { 334 if err := v.Validate(ctx); err != nil { 335 return err 336 } 337 } 338 if err := checkArgs(ctx, s.Storage.Args); err != nil { 339 return err 340 } 341 } 342 return nil 343 } 344 345 func checkArgs(ctx compose.Context, args any) error { 346 if args == nil { 347 return nil 348 } 349 switch v := args.(type) { 350 case map[string]any: 351 for n, v := range v { 352 if err := checkArgs(ctx, v); err != nil { 353 return fmt.Errorf("arg %s: %v", n, err) 354 } 355 } 356 case []any: 357 for i, v := range v { 358 if err := checkArgs(ctx, v); err != nil { 359 return fmt.Errorf("arg %d: %v", i, err) 360 } 361 } 362 case string: 363 if _, err := ctx.ResolveString(v); err != nil { 364 return err 365 } 366 case int: 367 // skip 368 case bool: 369 // skip 370 default: 371 return fmt.Errorf("unchecked arg type %T", args) 372 } 373 return nil 374 } 375 376 type Params struct { 377 ValueSource `yaml:",inline"` 378 Entrypoint string `yaml:"entrypoint"` 379 Args any `yaml:"args,omitempty"` 380 Patch []Patch `yaml:"patch,omitempty"` 381 } 382 383 func (p Params) Validate(ctx compose.Context) error { 384 if p.Args == nil { 385 if err := p.ValueSource.Validate(ctx); err != nil && p.Args == nil { 386 return err 387 } 388 } else { 389 if err := checkArgs(ctx, p.Args); err != nil { 390 return err 391 } 392 } 393 if p.Entrypoint == "" { 394 return fmt.Errorf("missing entrypoint") 395 } 396 for _, v := range p.Patch { 397 if err := v.Validate(ctx); err != nil { 398 return err 399 } 400 } 401 return nil 402 } 403 404 type Patch struct { 405 Type string `yaml:"type"` 406 Key *string `yaml:"key,omitempty"` 407 Path *string `yaml:"path,omitempty"` 408 Value *string `yaml:"value"` 409 Optimized bool `yaml:"optimized"` 410 } 411 412 func (p Patch) Validate(ctx compose.Context) error { 413 // accept empty string 414 if p.Key == nil && p.Path == nil { 415 return fmt.Errorf("patch: required key or path") 416 } 417 // accept empty string 418 if p.Value == nil { 419 return fmt.Errorf("patch: required value") 420 } 421 // type must be correct 422 oc, err := micheline.ParseOpCode(p.Type) 423 if err != nil { 424 return fmt.Errorf("patch: %v", err) 425 } 426 if !oc.IsTypeCode() { 427 return fmt.Errorf("patch: %s is not a valid type code", p.Type) 428 } 429 // value must resolve 430 val, err := ctx.ResolveString(*p.Value) 431 if err != nil { 432 return fmt.Errorf("patch: %v", err) 433 } 434 // value must parse against type code 435 if _, err := compose.ParseValue(oc, val); err != nil { 436 return fmt.Errorf("patch: %v", err) 437 } 438 return nil 439 }