github.com/m3db/m3@v1.5.0/src/metrics/pipeline/type.go (about)

     1  // Copyright (c) 2018 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 pipeline
    22  
    23  import (
    24  	"bytes"
    25  	"encoding/json"
    26  	"errors"
    27  	"fmt"
    28  	"sort"
    29  	"strings"
    30  
    31  	"github.com/m3db/m3/src/metrics/aggregation"
    32  	"github.com/m3db/m3/src/metrics/generated/proto/pipelinepb"
    33  	"github.com/m3db/m3/src/metrics/transformation"
    34  	xbytes "github.com/m3db/m3/src/metrics/x/bytes"
    35  )
    36  
    37  var (
    38  	errNilAggregationOpProto    = errors.New("nil aggregation op proto message")
    39  	errNilTransformationOpProto = errors.New("nil transformation op proto message")
    40  	errNilRollupOpProto         = errors.New("nil rollup op proto message")
    41  	errNilPipelineProto         = errors.New("nil pipeline proto message")
    42  	errNoOpInUnionMarshaler     = errors.New("no operation in union JSON value")
    43  )
    44  
    45  const (
    46  	templateMetricNameVar        = ".MetricName"
    47  	templateOpen                 = "{{"
    48  	templateClose                = "}}"
    49  	templateMetricNameExactMatch = templateOpen + " " + templateMetricNameVar + " " + templateClose
    50  )
    51  
    52  var (
    53  	templateMetricNameExactMatchBytes = []byte(templateMetricNameExactMatch)
    54  	templateAllowed                   = []string{templateMetricNameExactMatch}
    55  )
    56  
    57  func maybeContainsTemplate(str string) bool {
    58  	return strings.Contains(str, templateOpen) || strings.Contains(str, templateClose)
    59  }
    60  
    61  // OpType defines the type of an operation.
    62  type OpType int
    63  
    64  // List of supported operation types.
    65  const (
    66  	UnknownOpType OpType = iota
    67  	AggregationOpType
    68  	TransformationOpType
    69  	RollupOpType
    70  )
    71  
    72  // AggregationOp is an aggregation operation.
    73  type AggregationOp struct {
    74  	// Type of aggregation performed.
    75  	Type aggregation.Type
    76  }
    77  
    78  // NewAggregationOpFromProto creates a new aggregation op from proto.
    79  func NewAggregationOpFromProto(pb *pipelinepb.AggregationOp) (AggregationOp, error) {
    80  	var agg AggregationOp
    81  	if pb == nil {
    82  		return agg, errNilAggregationOpProto
    83  	}
    84  	aggType, err := aggregation.NewTypeFromProto(pb.Type)
    85  	if err != nil {
    86  		return agg, err
    87  	}
    88  	agg.Type = aggType
    89  	return agg, nil
    90  }
    91  
    92  // Clone clones the aggregation operation.
    93  func (op AggregationOp) Clone() AggregationOp {
    94  	return op
    95  }
    96  
    97  // Equal determines whether two aggregation operations are equal.
    98  func (op AggregationOp) Equal(other AggregationOp) bool {
    99  	return op.Type == other.Type
   100  }
   101  
   102  // Proto returns the proto message for the given aggregation operation.
   103  func (op AggregationOp) Proto() (*pipelinepb.AggregationOp, error) {
   104  	pbOpType, err := op.Type.Proto()
   105  	if err != nil {
   106  		return nil, err
   107  	}
   108  	return &pipelinepb.AggregationOp{Type: pbOpType}, nil
   109  }
   110  
   111  func (op AggregationOp) String() string {
   112  	return op.Type.String()
   113  }
   114  
   115  // MarshalText returns the text encoding of an aggregation operation.
   116  func (op AggregationOp) MarshalText() ([]byte, error) {
   117  	return op.Type.MarshalText()
   118  }
   119  
   120  // UnmarshalText unmarshals text-encoded data into an aggregation operation.
   121  func (op *AggregationOp) UnmarshalText(data []byte) error {
   122  	return op.Type.UnmarshalText(data)
   123  }
   124  
   125  // TransformationOp is a transformation operation.
   126  type TransformationOp struct {
   127  	// Type of transformation performed.
   128  	Type transformation.Type
   129  }
   130  
   131  // NewTransformationOpFromProto creates a new transformation op from proto.
   132  func NewTransformationOpFromProto(pb *pipelinepb.TransformationOp) (TransformationOp, error) {
   133  	var tf TransformationOp
   134  	if err := tf.FromProto(*pb); err != nil {
   135  		return TransformationOp{}, err
   136  	}
   137  	return tf, nil
   138  }
   139  
   140  // Equal determines whether two transformation operations are equal.
   141  func (op TransformationOp) Equal(other TransformationOp) bool {
   142  	return op.Type == other.Type
   143  }
   144  
   145  // Clone clones the transformation operation.
   146  func (op TransformationOp) Clone() TransformationOp {
   147  	return op
   148  }
   149  
   150  // Proto returns the proto message for the given transformation op.
   151  func (op TransformationOp) Proto() (*pipelinepb.TransformationOp, error) {
   152  	var pbOp pipelinepb.TransformationOp
   153  	if err := op.ToProto(&pbOp); err != nil {
   154  		return nil, err
   155  	}
   156  	return &pbOp, nil
   157  }
   158  
   159  func (op TransformationOp) String() string {
   160  	return op.Type.String()
   161  }
   162  
   163  // ToProto converts the transformation op to a protobuf message in place.
   164  func (op TransformationOp) ToProto(pb *pipelinepb.TransformationOp) error {
   165  	return op.Type.ToProto(&pb.Type)
   166  }
   167  
   168  // FromProto converts the protobuf message to a transformation in place.
   169  func (op *TransformationOp) FromProto(pb pipelinepb.TransformationOp) error {
   170  	return op.Type.FromProto(pb.Type)
   171  }
   172  
   173  // UnmarshalText extracts this type from its textual representation.
   174  func (op *TransformationOp) UnmarshalText(text []byte) error {
   175  	return op.Type.UnmarshalText(text)
   176  }
   177  
   178  // MarshalText serializes this type to its textual representation.
   179  func (op TransformationOp) MarshalText() (text []byte, err error) {
   180  	return op.Type.MarshalText()
   181  }
   182  
   183  // RollupType is the rollup type.
   184  // Note: Must match the protobuf enum definition since this is a direct cast.
   185  type RollupType int
   186  
   187  const (
   188  	// GroupByRollupType defines the group by rollup op type (default).
   189  	GroupByRollupType RollupType = iota
   190  	// ExcludeByRollupType defines the exclude by rollup op type.
   191  	ExcludeByRollupType
   192  )
   193  
   194  // RollupOp is a rollup operation.
   195  type RollupOp struct {
   196  	// Dimensions along which the rollup is performed.
   197  	Tags [][]byte
   198  	// New metric name generated as a result of the rollup.
   199  	newName []byte
   200  	// Type is the rollup type.
   201  	Type RollupType
   202  	// Types of aggregation performed within each unique dimension combination.
   203  	AggregationID    aggregation.ID
   204  	newNameTemplated bool
   205  }
   206  
   207  // NewRollupOpFromProto creates a new rollup op from proto.
   208  // NB: the rollup tags are always sorted on construction.
   209  func NewRollupOpFromProto(pb *pipelinepb.RollupOp) (RollupOp, error) {
   210  	var rollup RollupOp
   211  	if pb == nil {
   212  		return rollup, errNilRollupOpProto
   213  	}
   214  
   215  	aggregationID, err := aggregation.NewIDFromProto(pb.AggregationTypes)
   216  	if err != nil {
   217  		return rollup, err
   218  	}
   219  
   220  	return NewRollupOp(RollupType(pb.Type), pb.NewName, pb.Tags, aggregationID)
   221  }
   222  
   223  // NewRollupOp creates a new rollup op.
   224  func NewRollupOp(
   225  	rollupType RollupType,
   226  	rollupNewName string,
   227  	rollupTags []string,
   228  	rollupAggregationID aggregation.ID,
   229  ) (RollupOp, error) {
   230  	var rollup RollupOp
   231  
   232  	tags := make([]string, len(rollupTags))
   233  	copy(tags, rollupTags)
   234  	sort.Strings(tags)
   235  
   236  	var newNameTemplated bool
   237  	if maybeContainsTemplate(rollupNewName) {
   238  		// This metric might have a templated metric name.
   239  		newNameTemplated = true
   240  
   241  		// Right now only support "{{ .MetricName }}" to be able to generate
   242  		// the resulting metric name without using a Go template and only
   243  		// a single instance of it.
   244  		if n := strings.Count(rollupNewName, templateMetricNameExactMatch); n > 1 {
   245  			return rollup, fmt.Errorf(
   246  				"rollup contained template variable metric name more than once: "+
   247  					"input=%s, count_var_metric_name=%v", rollupNewName, n)
   248  		}
   249  
   250  		// Replace and see if all template tags resolved.
   251  		replacedNewName := strings.Replace(rollupNewName, templateMetricNameExactMatch, "", 1)
   252  
   253  		// Make sure fully replaced all instances of template usage, otherwise
   254  		// there are some other variables not supported or invalid use of
   255  		// template variable tags.
   256  		if maybeContainsTemplate(replacedNewName) {
   257  			return rollup, fmt.Errorf(
   258  				"rollup contained template tags but variables not resolved: "+
   259  					"input=%s, allowed=%v", rollupNewName, templateAllowed)
   260  		}
   261  	}
   262  
   263  	return RollupOp{
   264  		Type:             rollupType,
   265  		Tags:             xbytes.ArraysFromStringArray(tags),
   266  		AggregationID:    rollupAggregationID,
   267  		newName:          []byte(rollupNewName),
   268  		newNameTemplated: newNameTemplated,
   269  	}, nil
   270  }
   271  
   272  // NewName returns the new rollup name based on an existing name if
   273  // the new name uses a template, or otherwise the literal new name.
   274  func (op RollupOp) NewName(currName []byte) []byte {
   275  	if !op.newNameTemplated {
   276  		// No templated name, just return the "literal" new name.
   277  		return op.newName
   278  	}
   279  
   280  	out := make([]byte, 0, len(op.newName)+len(currName))
   281  	idx := bytes.Index(op.newName, templateMetricNameExactMatchBytes)
   282  	if idx == -1 {
   283  		return op.newName
   284  	}
   285  
   286  	out = append(out, op.newName[0:idx]...)
   287  	out = append(out, currName...)
   288  	out = append(out, op.newName[idx+len(templateMetricNameExactMatchBytes):]...)
   289  	return out
   290  }
   291  
   292  // SameTransform returns true if the two rollup operations have the same rollup transformation
   293  // (i.e., same new rollup metric name and same set of rollup tags).
   294  func (op RollupOp) SameTransform(other RollupOp) bool {
   295  	if len(op.Tags) != len(other.Tags) {
   296  		return false
   297  	}
   298  	if !bytes.Equal(op.newName, other.newName) {
   299  		return false
   300  	}
   301  	// Sort the tags and compare.
   302  	clonedTags := xbytes.ArraysToStringArray(op.Tags)
   303  	sort.Strings(clonedTags)
   304  	otherClonedTags := xbytes.ArraysToStringArray(other.Tags)
   305  	sort.Strings(otherClonedTags)
   306  	for i := 0; i < len(clonedTags); i++ {
   307  		if clonedTags[i] != otherClonedTags[i] {
   308  			return false
   309  		}
   310  	}
   311  	return true
   312  }
   313  
   314  // Equal returns true if two rollup operations are equal.
   315  func (op RollupOp) Equal(other RollupOp) bool {
   316  	if !op.AggregationID.Equal(other.AggregationID) {
   317  		return false
   318  	}
   319  	if op.Type != other.Type {
   320  		return false
   321  	}
   322  	return op.SameTransform(other)
   323  }
   324  
   325  // Clone clones the rollup operation.
   326  func (op RollupOp) Clone() RollupOp {
   327  	newName := make([]byte, len(op.newName))
   328  	copy(newName, op.newName)
   329  	return RollupOp{
   330  		Type:             op.Type,
   331  		Tags:             xbytes.ArrayCopy(op.Tags),
   332  		AggregationID:    op.AggregationID,
   333  		newName:          newName,
   334  		newNameTemplated: op.newNameTemplated,
   335  	}
   336  }
   337  
   338  // Proto returns the proto message for the given rollup op.
   339  func (op RollupOp) Proto() (*pipelinepb.RollupOp, error) {
   340  	aggTypes, err := op.AggregationID.Types()
   341  	if err != nil {
   342  		return nil, err
   343  	}
   344  	pbAggTypes, err := aggTypes.Proto()
   345  	if err != nil {
   346  		return nil, err
   347  	}
   348  	return &pipelinepb.RollupOp{
   349  		Type:             pipelinepb.RollupOp_Type(op.Type),
   350  		NewName:          string(op.newName),
   351  		Tags:             xbytes.ArraysToStringArray(op.Tags),
   352  		AggregationTypes: pbAggTypes,
   353  	}, nil
   354  }
   355  
   356  func (op RollupOp) String() string {
   357  	var b bytes.Buffer
   358  	b.WriteString("{")
   359  	fmt.Fprintf(&b, "name: %s, ", op.newName)
   360  	fmt.Fprintf(&b, "type: %v, ", op.Type)
   361  	b.WriteString("tags: [")
   362  	for i, t := range op.Tags {
   363  		fmt.Fprintf(&b, "%s", t)
   364  		if i < len(op.Tags)-1 {
   365  			b.WriteString(", ")
   366  		}
   367  	}
   368  	b.WriteString("], ")
   369  	fmt.Fprintf(&b, "aggregation: %v", op.AggregationID)
   370  	b.WriteString("}")
   371  	return b.String()
   372  }
   373  
   374  // MarshalJSON returns the JSON encoding of a rollup operation.
   375  func (op RollupOp) MarshalJSON() ([]byte, error) {
   376  	return json.Marshal(newRollupMarshaler(op))
   377  }
   378  
   379  // UnmarshalJSON unmarshals JSON-encoded data into a rollup operation.
   380  func (op *RollupOp) UnmarshalJSON(data []byte) error {
   381  	var converted rollupMarshaler
   382  	err := json.Unmarshal(data, &converted)
   383  	if err != nil {
   384  		return err
   385  	}
   386  	*op, err = converted.RollupOp()
   387  	return err
   388  }
   389  
   390  // UnmarshalYAML unmarshals YAML-encoded data into a rollup operation.
   391  func (op *RollupOp) UnmarshalYAML(unmarshal func(interface{}) error) error {
   392  	var converted rollupMarshaler
   393  	err := unmarshal(&converted)
   394  	if err != nil {
   395  		return err
   396  	}
   397  	*op, err = converted.RollupOp()
   398  	return err
   399  }
   400  
   401  // MarshalYAML returns the YAML representation of this type.
   402  func (op RollupOp) MarshalYAML() (interface{}, error) {
   403  	return newRollupMarshaler(op), nil
   404  }
   405  
   406  type rollupMarshaler struct {
   407  	Type          RollupType     `json:"type" yaml:"type"`
   408  	NewName       string         `json:"newName" yaml:"newName"`
   409  	Tags          []string       `json:"tags" yaml:"tags"`
   410  	AggregationID aggregation.ID `json:"aggregation,omitempty" yaml:"aggregation"`
   411  }
   412  
   413  func newRollupMarshaler(op RollupOp) rollupMarshaler {
   414  	return rollupMarshaler{
   415  		Type:          op.Type,
   416  		NewName:       string(op.newName),
   417  		Tags:          xbytes.ArraysToStringArray(op.Tags),
   418  		AggregationID: op.AggregationID,
   419  	}
   420  }
   421  
   422  func (m rollupMarshaler) RollupOp() (RollupOp, error) {
   423  	return NewRollupOp(m.Type, m.NewName, m.Tags, m.AggregationID)
   424  }
   425  
   426  // OpUnion is a union of different types of operation.
   427  type OpUnion struct {
   428  	Rollup         RollupOp
   429  	Type           OpType
   430  	Aggregation    AggregationOp
   431  	Transformation TransformationOp
   432  }
   433  
   434  // NewOpUnionFromProto creates a new operation union from proto.
   435  func NewOpUnionFromProto(pb pipelinepb.PipelineOp) (OpUnion, error) {
   436  	var (
   437  		u   OpUnion
   438  		err error
   439  	)
   440  	switch pb.Type {
   441  	case pipelinepb.PipelineOp_AGGREGATION:
   442  		u.Type = AggregationOpType
   443  		u.Aggregation, err = NewAggregationOpFromProto(pb.Aggregation)
   444  	case pipelinepb.PipelineOp_TRANSFORMATION:
   445  		u.Type = TransformationOpType
   446  		u.Transformation, err = NewTransformationOpFromProto(pb.Transformation)
   447  	case pipelinepb.PipelineOp_ROLLUP:
   448  		u.Type = RollupOpType
   449  		u.Rollup, err = NewRollupOpFromProto(pb.Rollup)
   450  	default:
   451  		err = fmt.Errorf("unknown op type in proto: %v", pb.Type)
   452  	}
   453  	return u, err
   454  }
   455  
   456  // Equal determines whether two operation unions are equal.
   457  func (u OpUnion) Equal(other OpUnion) bool {
   458  	if u.Type != other.Type {
   459  		return false
   460  	}
   461  	switch u.Type {
   462  	case AggregationOpType:
   463  		return u.Aggregation.Equal(other.Aggregation)
   464  	case TransformationOpType:
   465  		return u.Transformation.Equal(other.Transformation)
   466  	case RollupOpType:
   467  		return u.Rollup.Equal(other.Rollup)
   468  	}
   469  	return true
   470  }
   471  
   472  // Clone clones an operation union.
   473  func (u OpUnion) Clone() OpUnion {
   474  	clone := OpUnion{Type: u.Type}
   475  	switch u.Type {
   476  	case AggregationOpType:
   477  		clone.Aggregation = u.Aggregation.Clone()
   478  	case TransformationOpType:
   479  		clone.Transformation = u.Transformation.Clone()
   480  	case RollupOpType:
   481  		clone.Rollup = u.Rollup.Clone()
   482  	}
   483  	return clone
   484  }
   485  
   486  // Proto creates a proto message for the given operation.
   487  func (u OpUnion) Proto() (*pipelinepb.PipelineOp, error) {
   488  	var (
   489  		pbOp pipelinepb.PipelineOp
   490  		err  error
   491  	)
   492  	switch u.Type {
   493  	case AggregationOpType:
   494  		pbOp.Type = pipelinepb.PipelineOp_AGGREGATION
   495  		pbOp.Aggregation, err = u.Aggregation.Proto()
   496  	case TransformationOpType:
   497  		pbOp.Type = pipelinepb.PipelineOp_TRANSFORMATION
   498  		pbOp.Transformation, err = u.Transformation.Proto()
   499  	case RollupOpType:
   500  		pbOp.Type = pipelinepb.PipelineOp_ROLLUP
   501  		pbOp.Rollup, err = u.Rollup.Proto()
   502  	default:
   503  		err = fmt.Errorf("unknown op type: %v", u.Type)
   504  	}
   505  	return &pbOp, err
   506  }
   507  
   508  func (u OpUnion) String() string {
   509  	var b bytes.Buffer
   510  	b.WriteString("{")
   511  	switch u.Type {
   512  	case AggregationOpType:
   513  		fmt.Fprintf(&b, "aggregation: %s", u.Aggregation.String())
   514  	case TransformationOpType:
   515  		fmt.Fprintf(&b, "transformation: %s", u.Transformation.String())
   516  	case RollupOpType:
   517  		fmt.Fprintf(&b, "rollup: %s", u.Rollup.String())
   518  	default:
   519  		fmt.Fprintf(&b, "unknown op type: %v", u.Type)
   520  	}
   521  	b.WriteString("}")
   522  	return b.String()
   523  }
   524  
   525  // MarshalJSON returns the JSON encoding of an operation union.
   526  func (u OpUnion) MarshalJSON() ([]byte, error) {
   527  	converted, err := newUnionMarshaler(u)
   528  	if err != nil {
   529  		return nil, err
   530  	}
   531  	return json.Marshal(converted)
   532  }
   533  
   534  // UnmarshalJSON unmarshals JSON-encoded data into an operation union.
   535  func (u *OpUnion) UnmarshalJSON(data []byte) error {
   536  	var converted unionMarshaler
   537  	if err := json.Unmarshal(data, &converted); err != nil {
   538  		return err
   539  	}
   540  	union, err := converted.OpUnion()
   541  	if err != nil {
   542  		return err
   543  	}
   544  	*u = union
   545  	return nil
   546  }
   547  
   548  // MarshalJSON returns the JSON encoding of an operation union.
   549  func (u OpUnion) MarshalYAML() (interface{}, error) {
   550  	return newUnionMarshaler(u)
   551  }
   552  
   553  // UnmarshalYAML unmarshals YAML-encoded data into an operation union.
   554  func (u *OpUnion) UnmarshalYAML(unmarshal func(interface{}) error) error {
   555  	var converted unionMarshaler
   556  	if err := unmarshal(&converted); err != nil {
   557  		return err
   558  	}
   559  	union, err := converted.OpUnion()
   560  	if err != nil {
   561  		return err
   562  	}
   563  	*u = union
   564  	return nil
   565  }
   566  
   567  // unionMarshaler is a helper type to facilitate marshaling and unmarshaling operation unions.
   568  type unionMarshaler struct {
   569  	Aggregation    *AggregationOp    `json:"aggregation,omitempty" yaml:"aggregation"`
   570  	Transformation *TransformationOp `json:"transformation,omitempty" yaml:"transformation"`
   571  	Rollup         *RollupOp         `json:"rollup,omitempty" yaml:"rollup"`
   572  }
   573  
   574  func newUnionMarshaler(u OpUnion) (unionMarshaler, error) {
   575  	var converted unionMarshaler
   576  	switch u.Type {
   577  	case AggregationOpType:
   578  		converted.Aggregation = &u.Aggregation
   579  	case TransformationOpType:
   580  		converted.Transformation = &u.Transformation
   581  	case RollupOpType:
   582  		converted.Rollup = &u.Rollup
   583  	default:
   584  		return unionMarshaler{}, fmt.Errorf("unknown op type: %v", u.Type)
   585  	}
   586  	return converted, nil
   587  }
   588  
   589  func (m unionMarshaler) OpUnion() (OpUnion, error) {
   590  	if m.Aggregation != nil {
   591  		return OpUnion{Type: AggregationOpType, Aggregation: *m.Aggregation}, nil
   592  	}
   593  	if m.Transformation != nil {
   594  		return OpUnion{Type: TransformationOpType, Transformation: *m.Transformation}, nil
   595  	}
   596  	if m.Rollup != nil {
   597  		return OpUnion{Type: RollupOpType, Rollup: *m.Rollup}, nil
   598  	}
   599  	return OpUnion{}, errNoOpInUnionMarshaler
   600  }
   601  
   602  // Pipeline is a pipeline of operations.
   603  type Pipeline struct {
   604  	// a list of pipeline operations.
   605  	operations []OpUnion
   606  }
   607  
   608  // NewPipeline creates a new pipeline.
   609  func NewPipeline(ops []OpUnion) Pipeline {
   610  	return Pipeline{operations: ops}
   611  }
   612  
   613  // NewPipelineFromProto creates a new pipeline from proto.
   614  func NewPipelineFromProto(pb *pipelinepb.Pipeline) (Pipeline, error) {
   615  	if pb == nil {
   616  		return Pipeline{}, errNilPipelineProto
   617  	}
   618  	operations := make([]OpUnion, 0, len(pb.Ops))
   619  	for _, pbOp := range pb.Ops {
   620  		operation, err := NewOpUnionFromProto(pbOp)
   621  		if err != nil {
   622  			return Pipeline{}, err
   623  		}
   624  		operations = append(operations, operation)
   625  	}
   626  	return Pipeline{operations: operations}, nil
   627  }
   628  
   629  // Len returns the number of steps in a pipeline.
   630  func (p Pipeline) Len() int { return len(p.operations) }
   631  
   632  // IsEmpty determines whether a pipeline is empty.
   633  func (p Pipeline) IsEmpty() bool { return p.Len() == 0 }
   634  
   635  // At returns the operation at a given step.
   636  func (p Pipeline) At(i int) OpUnion { return p.operations[i] }
   637  
   638  // Equal determines whether two pipelines are equal.
   639  func (p Pipeline) Equal(other Pipeline) bool {
   640  	if len(p.operations) != len(other.operations) {
   641  		return false
   642  	}
   643  	for i := 0; i < len(p.operations); i++ {
   644  		if !p.operations[i].Equal(other.operations[i]) {
   645  			return false
   646  		}
   647  	}
   648  	return true
   649  }
   650  
   651  // SubPipeline returns a sub-pipeline containing operations between step `startInclusive`
   652  // and step `endExclusive` of the current pipeline.
   653  func (p Pipeline) SubPipeline(startInclusive int, endExclusive int) Pipeline {
   654  	return Pipeline{operations: p.operations[startInclusive:endExclusive]}
   655  }
   656  
   657  // Clone clones the pipeline.
   658  func (p Pipeline) Clone() Pipeline {
   659  	clone := make([]OpUnion, len(p.operations))
   660  	for i, op := range p.operations {
   661  		clone[i] = op.Clone()
   662  	}
   663  	return Pipeline{operations: clone}
   664  }
   665  
   666  // Proto returns the proto message for a given pipeline.
   667  func (p Pipeline) Proto() (*pipelinepb.Pipeline, error) {
   668  	pbOps := make([]pipelinepb.PipelineOp, 0, len(p.operations))
   669  	for _, op := range p.operations {
   670  		pbOp, err := op.Proto()
   671  		if err != nil {
   672  			return nil, err
   673  		}
   674  		pbOps = append(pbOps, *pbOp)
   675  	}
   676  	return &pipelinepb.Pipeline{Ops: pbOps}, nil
   677  }
   678  
   679  func (p Pipeline) String() string {
   680  	var b bytes.Buffer
   681  	b.WriteString("{operations: [")
   682  	for i, op := range p.operations {
   683  		b.WriteString(op.String())
   684  		if i < len(p.operations)-1 {
   685  			b.WriteString(", ")
   686  		}
   687  	}
   688  	b.WriteString("]}")
   689  	return b.String()
   690  }
   691  
   692  // MarshalJSON returns the JSON encoding of a pipeline.
   693  func (p Pipeline) MarshalJSON() ([]byte, error) {
   694  	return json.Marshal(p.operations)
   695  }
   696  
   697  // UnmarshalJSON unmarshals JSON-encoded data into a pipeline.
   698  func (p *Pipeline) UnmarshalJSON(data []byte) error {
   699  	var operations []OpUnion
   700  	if err := json.Unmarshal(data, &operations); err != nil {
   701  		return err
   702  	}
   703  	p.operations = operations
   704  	return nil
   705  }
   706  
   707  // UnmarshalYAML unmarshals YAML-encoded data into a pipeline.
   708  func (p *Pipeline) UnmarshalYAML(unmarshal func(interface{}) error) error {
   709  	var operations []OpUnion
   710  	if err := unmarshal(&operations); err != nil {
   711  		return err
   712  	}
   713  	p.operations = operations
   714  	return nil
   715  }
   716  
   717  // MarshalYAML returns the YAML representation.
   718  func (p Pipeline) MarshalYAML() (interface{}, error) {
   719  	return p.operations, nil
   720  }