...

Source file src/github.com/linkerd/linkerd2/pkg/profiles/profiles.go

Documentation: github.com/linkerd/linkerd2/pkg/profiles

     1  package profiles
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"path/filepath"
    10  	"regexp"
    11  	"text/template"
    12  	"time"
    13  
    14  	sp "github.com/linkerd/linkerd2/controller/gen/apis/serviceprofile/v1alpha2" // TODO: pkg/profiles should not depend on controller/gen
    15  	"github.com/linkerd/linkerd2/pkg/k8s"
    16  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    17  	"k8s.io/apimachinery/pkg/util/validation"
    18  	yamlDecoder "k8s.io/apimachinery/pkg/util/yaml"
    19  	"sigs.k8s.io/yaml"
    20  )
    21  
    22  var pathParamRegex = regexp.MustCompile(`\\{[^\}]*\\}`)
    23  
    24  type profileTemplateConfig struct {
    25  	ServiceNamespace string
    26  	ServiceName      string
    27  	ClusterDomain    string
    28  }
    29  
    30  var (
    31  	// ServiceProfileMeta is the TypeMeta for the ServiceProfile custom resource.
    32  	ServiceProfileMeta = metav1.TypeMeta{
    33  		APIVersion: k8s.ServiceProfileAPIVersion,
    34  		Kind:       k8s.ServiceProfileKind,
    35  	}
    36  
    37  	minStatus uint32 = 100
    38  	maxStatus uint32 = 599
    39  
    40  	errRequestMatchField  = errors.New("A request match must have a field set")
    41  	errResponseMatchField = errors.New("A response match must have a field set")
    42  )
    43  
    44  // Validate validates the structure of a ServiceProfile. This code is a superset
    45  // of the validation provided by the `openAPIV3Schema`, defined in the
    46  // ServiceProfile CRD.
    47  // openAPIV3Schema validates:
    48  // - types of non-recursive fields
    49  // - presence of required fields
    50  // This function validates:
    51  // - types of all fields
    52  // - presence of required fields
    53  // - presence of unknown fields
    54  // - recursive fields
    55  func Validate(data []byte) error {
    56  	var serviceProfile sp.ServiceProfile
    57  	err := yaml.UnmarshalStrict(data, &serviceProfile)
    58  	if err != nil {
    59  		return fmt.Errorf("failed to validate ServiceProfile: %w", err)
    60  	}
    61  
    62  	errs := validation.IsDNS1123Subdomain(serviceProfile.Name)
    63  	if len(errs) > 0 {
    64  		return fmt.Errorf("ServiceProfile %q has invalid name: %s", serviceProfile.Name, errs[0])
    65  	}
    66  
    67  	for _, route := range serviceProfile.Spec.Routes {
    68  		if route.Name == "" {
    69  			return fmt.Errorf("ServiceProfile %q has a route with no name", serviceProfile.Name)
    70  		}
    71  		if route.Timeout != "" {
    72  			_, err := time.ParseDuration(route.Timeout)
    73  			if err != nil {
    74  				return fmt.Errorf("ServiceProfile %q has a route with an invalid timeout: %w", serviceProfile.Name, err)
    75  			}
    76  		}
    77  		if route.Condition == nil {
    78  			return fmt.Errorf("ServiceProfile %q has a route with no condition", serviceProfile.Name)
    79  		}
    80  		err := ValidateRequestMatch(route.Condition)
    81  		if err != nil {
    82  			return fmt.Errorf("ServiceProfile %q has a route with an invalid condition: %w", serviceProfile.Name, err)
    83  		}
    84  		for _, rc := range route.ResponseClasses {
    85  			if rc.Condition == nil {
    86  				return fmt.Errorf("ServiceProfile %q has a response class with no condition", serviceProfile.Name)
    87  			}
    88  			err = ValidateResponseMatch(rc.Condition)
    89  			if err != nil {
    90  				return fmt.Errorf("ServiceProfile %q has a response class with an invalid condition: %w", serviceProfile.Name, err)
    91  			}
    92  		}
    93  	}
    94  
    95  	rb := serviceProfile.Spec.RetryBudget
    96  	if rb != nil {
    97  		if rb.RetryRatio < 0 {
    98  			return fmt.Errorf("ServiceProfile %q RetryBudget RetryRatio must be non-negative: %f", serviceProfile.Name, rb.RetryRatio)
    99  		}
   100  
   101  		if rb.TTL == "" {
   102  			return fmt.Errorf("ServiceProfile %q RetryBudget missing TTL field", serviceProfile.Name)
   103  		}
   104  
   105  		_, err := time.ParseDuration(rb.TTL)
   106  		if err != nil {
   107  			return fmt.Errorf("ServiceProfile %q RetryBudget: %w", serviceProfile.Name, err)
   108  		}
   109  	}
   110  
   111  	return nil
   112  }
   113  
   114  // ValidateRequestMatch validates whether a ServiceProfile RequestMatch has at
   115  // least one field set.
   116  func ValidateRequestMatch(reqMatch *sp.RequestMatch) error {
   117  	matchKindSet := false
   118  	if reqMatch.All != nil {
   119  		matchKindSet = true
   120  		for _, child := range reqMatch.All {
   121  			err := ValidateRequestMatch(child)
   122  			if err != nil {
   123  				return err
   124  			}
   125  		}
   126  	}
   127  	if reqMatch.Any != nil {
   128  		matchKindSet = true
   129  		for _, child := range reqMatch.Any {
   130  			err := ValidateRequestMatch(child)
   131  			if err != nil {
   132  				return err
   133  			}
   134  		}
   135  	}
   136  	if reqMatch.Method != "" {
   137  		matchKindSet = true
   138  	}
   139  	if reqMatch.Not != nil {
   140  		matchKindSet = true
   141  		err := ValidateRequestMatch(reqMatch.Not)
   142  		if err != nil {
   143  			return err
   144  		}
   145  	}
   146  	if reqMatch.PathRegex != "" {
   147  		matchKindSet = true
   148  	}
   149  
   150  	if !matchKindSet {
   151  		return errRequestMatchField
   152  	}
   153  
   154  	return nil
   155  }
   156  
   157  // ValidateResponseMatch validates whether a ServiceProfile ResponseMatch has at
   158  // least one field set, and sanity checks the Status Range.
   159  func ValidateResponseMatch(rspMatch *sp.ResponseMatch) error {
   160  	matchKindSet := false
   161  	if rspMatch.All != nil {
   162  		matchKindSet = true
   163  		for _, child := range rspMatch.All {
   164  			err := ValidateResponseMatch(child)
   165  			if err != nil {
   166  				return err
   167  			}
   168  		}
   169  	}
   170  	if rspMatch.Any != nil {
   171  		matchKindSet = true
   172  		for _, child := range rspMatch.Any {
   173  			err := ValidateResponseMatch(child)
   174  			if err != nil {
   175  				return err
   176  			}
   177  		}
   178  	}
   179  	if rspMatch.Status != nil {
   180  		if rspMatch.Status.Min != 0 && (rspMatch.Status.Min < minStatus || rspMatch.Status.Min > maxStatus) {
   181  			return fmt.Errorf("Range minimum must be between %d and %d, inclusive", minStatus, maxStatus)
   182  		} else if rspMatch.Status.Max != 0 && (rspMatch.Status.Max < minStatus || rspMatch.Status.Max > maxStatus) {
   183  			return fmt.Errorf("Range maximum must be between %d and %d, inclusive", minStatus, maxStatus)
   184  		} else if rspMatch.Status.Max != 0 && rspMatch.Status.Min != 0 && rspMatch.Status.Max < rspMatch.Status.Min {
   185  			return errors.New("Range maximum cannot be smaller than minimum")
   186  		}
   187  		matchKindSet = true
   188  	}
   189  	if rspMatch.Not != nil {
   190  		matchKindSet = true
   191  		err := ValidateResponseMatch(rspMatch.Not)
   192  		if err != nil {
   193  			return err
   194  		}
   195  	}
   196  
   197  	if !matchKindSet {
   198  		return errResponseMatchField
   199  	}
   200  
   201  	return nil
   202  }
   203  
   204  func buildConfig(namespace, service, clusterDomain string) *profileTemplateConfig {
   205  	return &profileTemplateConfig{
   206  		ServiceNamespace: namespace,
   207  		ServiceName:      service,
   208  		ClusterDomain:    clusterDomain,
   209  	}
   210  }
   211  
   212  // RenderProfileTemplate renders a ServiceProfile template to a buffer, given a
   213  // namespace, service, and control plane namespace.
   214  func RenderProfileTemplate(namespace, service, clusterDomain string, w io.Writer, format string) error {
   215  	config := buildConfig(namespace, service, clusterDomain)
   216  	template, err := template.New("profile").Parse(Template)
   217  	if err != nil {
   218  		return err
   219  	}
   220  	buf := &bytes.Buffer{}
   221  	err = template.Execute(buf, config)
   222  	if err != nil {
   223  		return err
   224  	}
   225  
   226  	if format == "json" {
   227  		bytes, err := yamlDecoder.ToJSON(buf.Bytes())
   228  		if err != nil {
   229  			return err
   230  		}
   231  		_, err = w.Write(append(bytes, '\n'))
   232  		return err
   233  	}
   234  	if format == "yaml" {
   235  		_, err = w.Write(buf.Bytes())
   236  		return err
   237  	}
   238  
   239  	return fmt.Errorf("unknown output format: %s", format)
   240  }
   241  
   242  func readFile(fileName string) (io.Reader, error) {
   243  	if fileName == "-" {
   244  		return os.Stdin, nil
   245  	}
   246  	return os.Open(filepath.Clean(fileName))
   247  }
   248  
   249  // PathToRegex converts a path into a regex.
   250  func PathToRegex(path string) string {
   251  	escaped := regexp.QuoteMeta(path)
   252  	return pathParamRegex.ReplaceAllLiteralString(escaped, "[^/]*")
   253  }
   254  

View as plain text