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  }