Improve ServiceProfile validation in linkerd check (#2218)

The `linkerd check` command was doing limited validation on
ServiceProfiles.

Make ServiceProfile validation more complete, specifically validate:
- types of all fields
- presence of required fields
- presence of unknown fields
- recursive fields

Also move all validation code into a new `Validate` function in the
profiles package.

Validation of field types and required fields is handled via
`yaml.UnmarshalStrict` in the `Validate` function. This motivated
migrating from github.com/ghodss/yaml to a fork, sigs.k8s.io/yaml.

Fixes #2190
This commit is contained in:
Andrew Seigner 2019-02-07 14:35:47 -08:00 committed by GitHub
parent 72812baf99
commit 907f01fba6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 743 additions and 68 deletions

View File

@ -1336,6 +1336,7 @@
"k8s.io/helm/pkg/renderutil",
"k8s.io/helm/pkg/timeconv",
"k8s.io/klog",
"sigs.k8s.io/yaml",
]
solver-name = "gps-cdcl"
solver-version = 1

View File

@ -285,6 +285,19 @@ spec:
required:
- routes
properties:
retryBudget:
required:
- minRetriesPerSecond
- retryRatio
- ttl
type: object
properties:
minRetriesPerSecond:
type: integer
retryRatio:
type: number
ttl:
type: string
routes:
type: array
items:
@ -295,6 +308,8 @@ spec:
properties:
name:
type: string
timeout:
type: string
condition:
type: object
minProperties: 1

View File

@ -1,5 +1,5 @@
## compile binaries
FROM gcr.io/linkerd-io/go-deps:4abae893 as golang
FROM gcr.io/linkerd-io/go-deps:b457d5cb as golang
WORKDIR /go/src/github.com/linkerd/linkerd2
COPY cli cli
COPY chart chart

View File

@ -8,7 +8,6 @@ import (
"strconv"
"strings"
"github.com/ghodss/yaml"
"github.com/linkerd/linkerd2/pkg/healthcheck"
"github.com/linkerd/linkerd2/pkg/k8s"
"github.com/spf13/cobra"
@ -17,6 +16,7 @@ import (
k8sResource "k8s.io/apimachinery/pkg/api/resource"
metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"sigs.k8s.io/yaml"
)
const (

View File

@ -8,7 +8,6 @@ import (
"os"
"path/filepath"
"github.com/ghodss/yaml"
"github.com/linkerd/linkerd2/pkg/k8s"
appsV1 "k8s.io/api/apps/v1"
batchV1 "k8s.io/api/batch/v1"
@ -17,6 +16,7 @@ import (
metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
yamlDecoder "k8s.io/apimachinery/pkg/util/yaml"
"sigs.k8s.io/yaml"
)
type resourceTransformer interface {

View File

@ -9,7 +9,6 @@ import (
"path"
"strings"
"github.com/ghodss/yaml"
"github.com/linkerd/linkerd2/cli/static"
"github.com/linkerd/linkerd2/pkg/k8s"
uuid "github.com/satori/go.uuid"
@ -19,6 +18,7 @@ import (
"k8s.io/helm/pkg/proto/hapi/chart"
"k8s.io/helm/pkg/renderutil"
"k8s.io/helm/pkg/timeconv"
"sigs.k8s.io/yaml"
)
type installConfig struct {

View File

@ -6,9 +6,9 @@ import (
"fmt"
"testing"
"github.com/ghodss/yaml"
"github.com/linkerd/linkerd2/controller/gen/apis/serviceprofile/v1alpha1"
"github.com/linkerd/linkerd2/pkg/profiles"
"sigs.k8s.io/yaml"
)
func TestParseProfile(t *testing.T) {

View File

@ -317,6 +317,19 @@ spec:
required:
- routes
properties:
retryBudget:
required:
- minRetriesPerSecond
- retryRatio
- ttl
type: object
properties:
minRetriesPerSecond:
type: integer
retryRatio:
type: number
ttl:
type: string
routes:
type: array
items:
@ -327,6 +340,8 @@ spec:
properties:
name:
type: string
timeout:
type: string
condition:
type: object
minProperties: 1

View File

@ -329,6 +329,19 @@ spec:
required:
- routes
properties:
retryBudget:
required:
- minRetriesPerSecond
- retryRatio
- ttl
type: object
properties:
minRetriesPerSecond:
type: integer
retryRatio:
type: number
ttl:
type: string
routes:
type: array
items:
@ -339,6 +352,8 @@ spec:
properties:
name:
type: string
timeout:
type: string
condition:
type: object
minProperties: 1

View File

@ -329,6 +329,19 @@ spec:
required:
- routes
properties:
retryBudget:
required:
- minRetriesPerSecond
- retryRatio
- ttl
type: object
properties:
minRetriesPerSecond:
type: integer
retryRatio:
type: number
ttl:
type: string
routes:
type: array
items:
@ -339,6 +352,8 @@ spec:
properties:
name:
type: string
timeout:
type: string
condition:
type: object
minProperties: 1

View File

@ -295,6 +295,19 @@ spec:
required:
- routes
properties:
retryBudget:
required:
- minRetriesPerSecond
- retryRatio
- ttl
type: object
properties:
minRetriesPerSecond:
type: integer
retryRatio:
type: number
ttl:
type: string
routes:
type: array
items:
@ -305,6 +318,8 @@ spec:
properties:
name:
type: string
timeout:
type: string
condition:
type: object
minProperties: 1

View File

@ -325,6 +325,19 @@ spec:
required:
- routes
properties:
retryBudget:
required:
- minRetriesPerSecond
- retryRatio
- ttl
type: object
properties:
minRetriesPerSecond:
type: integer
retryRatio:
type: number
ttl:
type: string
routes:
type: array
items:
@ -335,6 +348,8 @@ spec:
properties:
name:
type: string
timeout:
type: string
condition:
type: object
minProperties: 1

View File

@ -320,6 +320,19 @@ spec:
required:
- routes
properties:
retryBudget:
required:
- minRetriesPerSecond
- retryRatio
- ttl
type: object
properties:
minRetriesPerSecond:
type: integer
retryRatio:
type: number
ttl:
type: string
routes:
type: array
items:
@ -330,6 +343,8 @@ spec:
properties:
name:
type: string
timeout:
type: string
condition:
type: object
minProperties: 1

View File

@ -6,11 +6,11 @@ import (
"os"
"strings"
"github.com/ghodss/yaml"
"github.com/linkerd/linkerd2/pkg/k8s"
"github.com/spf13/cobra"
"k8s.io/api/core/v1"
metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/yaml"
)
type resourceTransformerUninject struct{}

View File

@ -1,5 +1,5 @@
## compile cni-plugin utility
FROM gcr.io/linkerd-io/go-deps:4abae893 as golang
FROM gcr.io/linkerd-io/go-deps:b457d5cb as golang
WORKDIR /go/src/github.com/linkerd/linkerd2
COPY proxy-init proxy-init
COPY pkg pkg

View File

@ -1,5 +1,5 @@
## compile controller services
FROM gcr.io/linkerd-io/go-deps:4abae893 as golang
FROM gcr.io/linkerd-io/go-deps:b457d5cb as golang
WORKDIR /go/src/github.com/linkerd/linkerd2
COPY controller/gen controller/gen
COPY pkg pkg

View File

@ -4,10 +4,10 @@ import (
"io/ioutil"
"path/filepath"
yaml "github.com/ghodss/yaml"
admissionv1beta1 "k8s.io/api/admission/v1beta1"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/yaml"
)
// These constants provide default, fake strings for testing proxy-injector.

View File

@ -6,7 +6,6 @@ import (
"io/ioutil"
"strings"
yaml "github.com/ghodss/yaml"
"github.com/linkerd/linkerd2/pkg/healthcheck"
k8sPkg "github.com/linkerd/linkerd2/pkg/k8s"
log "github.com/sirupsen/logrus"
@ -17,6 +16,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/client-go/kubernetes"
"sigs.k8s.io/yaml"
)
const (

View File

@ -5,7 +5,6 @@ import (
"encoding/base64"
"text/template"
yaml "github.com/ghodss/yaml"
"github.com/linkerd/linkerd2/controller/proxy-injector/tmpl"
k8sPkg "github.com/linkerd/linkerd2/pkg/k8s"
"github.com/linkerd/linkerd2/pkg/tls"
@ -14,6 +13,7 @@ import (
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"sigs.k8s.io/yaml"
)
// WebhookConfig creates the MutatingWebhookConfiguration of the webhook.

View File

@ -102,9 +102,8 @@ const (
)
var (
retryWindow = 5 * time.Second
requestTimeout = 30 * time.Second
clusterZoneSuffix = []string{"svc", "cluster", "local"}
retryWindow = 5 * time.Second
requestTimeout = 30 * time.Second
)
type checker struct {
@ -827,47 +826,26 @@ func (hc *HealthChecker) validateServiceProfiles() error {
}
for _, p := range svcProfiles.Items {
nameParts := strings.Split(p.Name, ".")
if len(nameParts) != 2+len(clusterZoneSuffix) {
return fmt.Errorf("ServiceProfile \"%s\" has invalid name (must be \"<service>.<namespace>.svc.cluster.local\")", p.Name)
service, namespace, err := profiles.ValidateName(p.Name)
if err != nil {
return err
}
for i, part := range nameParts[2:] {
if part != clusterZoneSuffix[i] {
return fmt.Errorf("ServiceProfile \"%s\" has invalid name (must be \"<service>.<namespace>.svc.cluster.local\")", p.Name)
}
}
service := nameParts[0]
namespace := nameParts[1]
_, err := hc.clientset.Core().Services(namespace).Get(service, meta_v1.GetOptions{})
_, err = hc.clientset.CoreV1().Services(namespace).Get(service, meta_v1.GetOptions{})
if err != nil {
return fmt.Errorf("ServiceProfile \"%s\" has unknown service: %s", p.Name, err)
}
for _, route := range p.Spec.Routes {
if route.Name == "" {
return fmt.Errorf("ServiceProfile \"%s\" has a route with no name", p.Name)
}
if route.Timeout != "" {
_, err := time.ParseDuration(route.Timeout)
if err != nil {
return fmt.Errorf("ServiceProfile \"%s\" has a route with an invalid timeout: %s", p.Name, err)
}
}
if route.Condition == nil {
return fmt.Errorf("ServiceProfile \"%s\" has a route with no condition", p.Name)
}
err = profiles.ValidateRequestMatch(route.Condition)
if err != nil {
return fmt.Errorf("ServiceProfile \"%s\" has a route with an invalid condition: %s", p.Name, err)
}
for _, rc := range route.ResponseClasses {
if rc.Condition == nil {
return fmt.Errorf("ServiceProfile \"%s\" has a response class with no condition", p.Name)
}
err = profiles.ValidateResponseMatch(rc.Condition)
if err != nil {
return fmt.Errorf("ServiceProfile \"%s\" has a response class with an invalid condition: %s", p.Name, err)
}
}
// TODO: remove this check once we implement ServiceProfile validation via a
// ValidatingAdmissionWebhook
result := hc.spClientset.RESTClient().Get().RequestURI(p.GetSelfLink()).Do()
raw, err := result.Raw()
if err != nil {
return err
}
err = profiles.Validate(raw)
if err != nil {
return fmt.Errorf("%s: %s", p.Name, err)
}
}
return nil

View File

@ -9,10 +9,10 @@ import (
"regexp"
"sort"
"github.com/ghodss/yaml"
"github.com/go-openapi/spec"
sp "github.com/linkerd/linkerd2/controller/gen/apis/serviceprofile/v1alpha1"
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/yaml"
)
var pathParamRegex = regexp.MustCompile(`\\{[^\}]*\\}`)

View File

@ -6,16 +6,17 @@ import (
"fmt"
"io"
"os"
"strings"
"text/template"
"time"
"github.com/ghodss/yaml"
"github.com/golang/protobuf/ptypes/duration"
pb "github.com/linkerd/linkerd2-proxy-api/go/destination"
sp "github.com/linkerd/linkerd2/controller/gen/apis/serviceprofile/v1alpha1"
"github.com/linkerd/linkerd2/pkg/util"
log "github.com/sirupsen/logrus"
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/yaml"
)
type profileTemplateConfig struct {
@ -47,6 +48,14 @@ var (
// DefaultRouteTimeout is the default timeout for routes that do not specify
// one.
DefaultRouteTimeout = 10 * time.Second
minStatus uint32 = 100
maxStatus uint32 = 599
clusterZoneSuffix = []string{"svc", "cluster", "local"}
errRequestMatchField = errors.New("A request match must have a field set")
errResponseMatchField = errors.New("A response match must have a field set")
)
func toDuration(d time.Duration) *duration.Duration {
@ -198,7 +207,7 @@ func ToResponseMatch(rspMatch *sp.ResponseMatch) (*pb.ResponseMatch, error) {
}
if len(matches) == 0 {
return nil, errors.New("A response match must have a field set")
return nil, errResponseMatchField
}
if len(matches) == 1 {
return matches[0], nil
@ -292,7 +301,7 @@ func ToRequestMatch(reqMatch *sp.RequestMatch) (*pb.RequestMatch, error) {
}
if len(matches) == 0 {
return nil, errors.New("A request match must have a field set")
return nil, errRequestMatchField
}
if len(matches) == 1 {
return matches[0], nil
@ -306,6 +315,96 @@ func ToRequestMatch(reqMatch *sp.RequestMatch) (*pb.RequestMatch, error) {
}, nil
}
// Validate validates the structure of a ServiceProfile. This code is a superset
// of the validation provided by the `openAPIV3Schema`, defined in the
// ServiceProfile CRD.
// openAPIV3Schema validates:
// - types of non-recursive fields
// - presence of required fields
// This function validates:
// - types of all fields
// - presence of required fields
// - presence of unknown fields
// - recursive fields
func Validate(data []byte) error {
var serviceProfile sp.ServiceProfile
err := yaml.UnmarshalStrict([]byte(data), &serviceProfile)
if err != nil {
return fmt.Errorf("failed to validate ServiceProfile: %s", err)
}
_, _, err = ValidateName(serviceProfile.Name)
if err != nil {
return err
}
if len(serviceProfile.Spec.Routes) == 0 {
return fmt.Errorf("ServiceProfile \"%s\" has no routes", serviceProfile.Name)
}
for _, route := range serviceProfile.Spec.Routes {
if route.Name == "" {
return fmt.Errorf("ServiceProfile \"%s\" has a route with no name", serviceProfile.Name)
}
if route.Timeout != "" {
_, err := time.ParseDuration(route.Timeout)
if err != nil {
return fmt.Errorf("ServiceProfile \"%s\" has a route with an invalid timeout: %s", serviceProfile.Name, err)
}
}
if route.Condition == nil {
return fmt.Errorf("ServiceProfile \"%s\" has a route with no condition", serviceProfile.Name)
}
err := ValidateRequestMatch(route.Condition)
if err != nil {
return fmt.Errorf("ServiceProfile \"%s\" has a route with an invalid condition: %s", serviceProfile.Name, err)
}
for _, rc := range route.ResponseClasses {
if rc.Condition == nil {
return fmt.Errorf("ServiceProfile \"%s\" has a response class with no condition", serviceProfile.Name)
}
err = ValidateResponseMatch(rc.Condition)
if err != nil {
return fmt.Errorf("ServiceProfile \"%s\" has a response class with an invalid condition: %s", serviceProfile.Name, err)
}
}
}
rb := serviceProfile.Spec.RetryBudget
if rb != nil {
if rb.RetryRatio < 0 {
return fmt.Errorf("ServiceProfile \"%s\" RetryBudget RetryRatio must be non-negative: %f", serviceProfile.Name, rb.RetryRatio)
}
if rb.TTL == "" {
return fmt.Errorf("ServiceProfile \"%s\" RetryBudget missing TTL field", serviceProfile.Name)
}
_, err := time.ParseDuration(rb.TTL)
if err != nil {
return fmt.Errorf("ServiceProfile \"%s\" RetryBudget: %s", serviceProfile.Name, err)
}
}
return nil
}
// ValidateName validates that a ServiceProfile's name is of the form:
// <service>.<namespace>.svc.cluster.local
func ValidateName(name string) (string, string, error) {
nameParts := strings.Split(name, ".")
if len(nameParts) != 2+len(clusterZoneSuffix) {
return "", "", fmt.Errorf("ServiceProfile \"%s\" has invalid name (must be \"<service>.<namespace>.svc.cluster.local\")", name)
}
for i, part := range nameParts[2:] {
if part != clusterZoneSuffix[i] {
return "", "", fmt.Errorf("ServiceProfile \"%s\" has invalid name (must be \"<service>.<namespace>.svc.cluster.local\")", name)
}
}
return nameParts[0], nameParts[1], nil
}
// ValidateRequestMatch validates whether a ServiceProfile RequestMatch has at
// least one field set.
func ValidateRequestMatch(reqMatch *sp.RequestMatch) error {
@ -343,7 +442,7 @@ func ValidateRequestMatch(reqMatch *sp.RequestMatch) error {
}
if !matchKindSet {
return errors.New("A request match must have a field set")
return errRequestMatchField
}
return nil
@ -352,7 +451,6 @@ func ValidateRequestMatch(reqMatch *sp.RequestMatch) error {
// ValidateResponseMatch validates whether a ServiceProfile ResponseMatch has at
// least one field set, and sanity checks the Status Range.
func ValidateResponseMatch(rspMatch *sp.ResponseMatch) error {
invalidRangeErr := errors.New("Range maximum cannot be smaller than minimum")
matchKindSet := false
if rspMatch.All != nil {
matchKindSet = true
@ -373,8 +471,12 @@ func ValidateResponseMatch(rspMatch *sp.ResponseMatch) error {
}
}
if rspMatch.Status != nil {
if rspMatch.Status.Max != 0 && rspMatch.Status.Min != 0 && rspMatch.Status.Max < rspMatch.Status.Min {
return invalidRangeErr
if rspMatch.Status.Min != 0 && (rspMatch.Status.Min < minStatus || rspMatch.Status.Min > maxStatus) {
return fmt.Errorf("Range minimum must be between %d and %d, inclusive", minStatus, maxStatus)
} else if rspMatch.Status.Max != 0 && (rspMatch.Status.Max < minStatus || rspMatch.Status.Max > maxStatus) {
return fmt.Errorf("Range maximum must be between %d and %d, inclusive", minStatus, maxStatus)
} else if rspMatch.Status.Max != 0 && rspMatch.Status.Min != 0 && rspMatch.Status.Max < rspMatch.Status.Min {
return errors.New("Range maximum cannot be smaller than minimum")
}
matchKindSet = true
}
@ -387,7 +489,7 @@ func ValidateResponseMatch(rspMatch *sp.ResponseMatch) error {
}
if !matchKindSet {
return errors.New("A response match must have a field set")
return errResponseMatchField
}
return nil
@ -398,7 +500,7 @@ func buildConfig(namespace, service, controlPlaneNamespace string) *profileTempl
ControlPlaneNamespace: controlPlaneNamespace,
ServiceNamespace: namespace,
ServiceName: service,
ClusterZone: "svc.cluster.local",
ClusterZone: strings.Join(clusterZoneSuffix, "."),
}
}

View File

@ -0,0 +1,489 @@
package profiles
import (
"errors"
"fmt"
"testing"
)
type spExp struct {
err error
sp string
}
func TestValidate(t *testing.T) {
expectations := []spExp{
{
err: nil,
sp: `apiVersion: linkerd.io/v1alpha1
kind: ServiceProfile
metadata:
name: name.ns.svc.cluster.local
namespace: linkerd-ns
spec:
routes:
- name: name-1
condition:
method: GET
pathRegex: /route-1`,
},
{
err: nil,
sp: `apiVersion: linkerd.io/v1alpha1
kind: ServiceProfile
metadata:
name: name.ns.svc.cluster.local
namespace: linkerd-ns
spec:
retryBudget:
minRetriesPerSecond: 5
retryRatio: 0.2
ttl: 10ms
routes:
- name: name-1
condition:
method: GET
pathRegex: /route-1
any:
- all:
- method: POST
- pathRegex: '/authors/\d+'
- all:
- not:
method: DELETE
- pathRegex: /info.txt
responseClasses:
- condition:
status:
min: 500
max: 599
all:
- status:
min: 500
max: 599
- not:
status:
min: 503`,
},
{
err: errors.New("ServiceProfile \"bad.svc.cluster.local\" has invalid name (must be \"<service>.<namespace>.svc.cluster.local\")"),
sp: `apiVersion: linkerd.io/v1alpha1
kind: ServiceProfile
metadata:
name: bad.svc.cluster.local
namespace: linkerd-ns
spec:
routes:
- name: name-1
condition:
method: GET
pathRegex: /route-1`,
},
{
err: errors.New("ServiceProfile \"name.ns.svc.cluster\" has invalid name (must be \"<service>.<namespace>.svc.cluster.local\")"),
sp: `apiVersion: linkerd.io/v1alpha1
kind: ServiceProfile
metadata:
name: name.ns.svc.cluster
namespace: linkerd-ns
spec:
routes:
- name: name-1
condition:
method: GET
pathRegex: /route-1`,
},
{
err: errors.New("failed to validate ServiceProfile: error unmarshaling JSON: while decoding JSON: json: unknown field \"foo\""),
sp: `apiVersion: linkerd.io/v1alpha1
kind: ServiceProfile
metadata:
name: name.ns.svc.cluster.local
namespace: linkerd-ns
spec:
foo: bar
routes:
- name: name-1
condition:
method: GET
pathRegex: /route-1`,
},
{
err: errors.New("failed to validate ServiceProfile: error unmarshaling JSON: while decoding JSON: json: unknown field \"foo\""),
sp: `apiVersion: linkerd.io/v1alpha1
kind: ServiceProfile
metadata:
name: name.ns.svc.cluster.local
namespace: linkerd-ns
spec:
routes:
- name: name-1
foo: bar
condition:
method: GET
pathRegex: /route-1`,
},
{
err: errors.New("failed to validate ServiceProfile: error unmarshaling JSON: while decoding JSON: json: unknown field \"foo\""),
sp: `apiVersion: linkerd.io/v1alpha1
kind: ServiceProfile
metadata:
name: name.ns.svc.cluster.local
namespace: linkerd-ns
spec:
routes:
- name: name-1
condition:
foo: bar
method: GET
pathRegex: /route-1`,
},
{
err: errors.New("ServiceProfile \"name.ns.svc.cluster.local\" has no routes"),
sp: `apiVersion: linkerd.io/v1alpha1
kind: ServiceProfile
metadata:
name: name.ns.svc.cluster.local
namespace: linkerd-ns
spec:`,
},
{
err: errors.New("ServiceProfile \"name.ns.svc.cluster.local\" has a route with no condition"),
sp: `apiVersion: linkerd.io/v1alpha1
kind: ServiceProfile
metadata:
name: name.ns.svc.cluster.local
namespace: linkerd-ns
spec:
routes:
- name: name-1`,
},
{
err: errors.New("ServiceProfile \"name.ns.svc.cluster.local\" has a route with no name"),
sp: `apiVersion: linkerd.io/v1alpha1
kind: ServiceProfile
metadata:
name: name.ns.svc.cluster.local
namespace: linkerd-ns
spec:
routes:
- condition:
method: GET
pathRegex: /route-1`,
},
{
err: errors.New("failed to validate ServiceProfile: error unmarshaling JSON: while decoding JSON: json: unknown field \"foo\""),
sp: `apiVersion: linkerd.io/v1alpha1
kind: ServiceProfile
metadata:
name: name.ns.svc.cluster.local
namespace: linkerd-ns
spec:
routes:
- name: name-1
condition:
foo: bar
method: GET
pathRegex: /route-1
not:
method: GET`,
},
{
err: errors.New("ServiceProfile \"name.ns.svc.cluster.local\" has a route with no condition"),
sp: `apiVersion: linkerd.io/v1alpha1
kind: ServiceProfile
metadata:
name: name.ns.svc.cluster.local
namespace: linkerd-ns
spec:
routes:
- name: name-1
condition:`,
},
{
err: errors.New("ServiceProfile \"name.ns.svc.cluster.local\" has a route with an invalid condition: A request match must have a field set"),
sp: `apiVersion: linkerd.io/v1alpha1
kind: ServiceProfile
metadata:
name: name.ns.svc.cluster.local
namespace: linkerd-ns
spec:
routes:
- name: name-1
condition:
method:`,
},
{
err: errors.New("ServiceProfile \"name.ns.svc.cluster.local\" has a response class with no condition"),
sp: `apiVersion: linkerd.io/v1alpha1
kind: ServiceProfile
metadata:
name: name.ns.svc.cluster.local
namespace: linkerd-ns
spec:
routes:
- name: name-1
condition:
method: GET
pathRegex: /route-1
responseClasses:
- condition:`,
},
{
err: errors.New("ServiceProfile \"name.ns.svc.cluster.local\" has a response class with an invalid condition: A response match must have a field set"),
sp: `apiVersion: linkerd.io/v1alpha1
kind: ServiceProfile
metadata:
name: name.ns.svc.cluster.local
namespace: linkerd-ns
spec:
routes:
- name: name-1
condition:
method: GET
pathRegex: /route-1
responseClasses:
- condition:
status:
min: 500
max: 599
all:
- status:`,
},
{
err: errors.New("ServiceProfile \"name.ns.svc.cluster.local\" has a response class with an invalid condition: Range maximum must be between 100 and 599, inclusive"),
sp: `apiVersion: linkerd.io/v1alpha1
kind: ServiceProfile
metadata:
name: name.ns.svc.cluster.local
namespace: linkerd-ns
spec:
routes:
- name: name-1
condition:
method: GET
pathRegex: /route-1
responseClasses:
- condition:
status:
min: 500
max: 600`,
},
{
err: errors.New("ServiceProfile \"name.ns.svc.cluster.local\" has a response class with an invalid condition: Range maximum cannot be smaller than minimum"),
sp: `apiVersion: linkerd.io/v1alpha1
kind: ServiceProfile
metadata:
name: name.ns.svc.cluster.local
namespace: linkerd-ns
spec:
routes:
- name: name-1
condition:
method: GET
pathRegex: /route-1
responseClasses:
- condition:
status:
min: 500
max: 599
all:
- status:
min: 500
max: 599
- not:
status:
min: 300
max: 200`,
},
{
err: errors.New("ServiceProfile \"name.ns.svc.cluster.local\" has a response class with an invalid condition: Range minimum must be between 100 and 599, inclusive"),
sp: `apiVersion: linkerd.io/v1alpha1
kind: ServiceProfile
metadata:
name: name.ns.svc.cluster.local
namespace: linkerd-ns
spec:
routes:
- name: name-1
condition:
method: GET
pathRegex: /route-1
responseClasses:
- condition:
status:
min: 500
max: 599
all:
- status:
min: 500
max: 599
- not:
status:
min: 1`,
},
{
err: errors.New("failed to validate ServiceProfile: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal bool into Go struct field Range.min of type uint32"),
sp: `apiVersion: linkerd.io/v1alpha1
kind: ServiceProfile
metadata:
name: name.ns.svc.cluster.local
namespace: linkerd-ns
spec:
routes:
- name: name-1
condition:
method: GET
pathRegex: /route-1
responseClasses:
- condition:
status:
min: 500
max: 599
all:
- status:
min: 500
max: 599
- not:
status:
min: false`,
},
{
err: errors.New("ServiceProfile \"name.ns.svc.cluster.local\" RetryBudget missing TTL field"),
sp: `apiVersion: linkerd.io/v1alpha1
kind: ServiceProfile
metadata:
name: name.ns.svc.cluster.local
namespace: linkerd-ns
spec:
retryBudget:
minRetriesPerSecond: 5
retryRatio: 0.2
routes:
- name: name-1
condition:
method: GET
pathRegex: /route-1`,
},
{
err: errors.New("ServiceProfile \"name.ns.svc.cluster.local\" RetryBudget: time: invalid duration foo"),
sp: `apiVersion: linkerd.io/v1alpha1
kind: ServiceProfile
metadata:
name: name.ns.svc.cluster.local
namespace: linkerd-ns
spec:
retryBudget:
minRetriesPerSecond: 5
retryRatio: 0.2
ttl: foo
routes:
- name: name-1
condition:
method: GET
pathRegex: /route-1`,
},
{
err: errors.New("ServiceProfile \"name.ns.svc.cluster.local\" RetryBudget RetryRatio must be non-negative: -0.200000"),
sp: `apiVersion: linkerd.io/v1alpha1
kind: ServiceProfile
metadata:
name: name.ns.svc.cluster.local
namespace: linkerd-ns
spec:
retryBudget:
minRetriesPerSecond: 5
retryRatio: -0.2
ttl: 10s
routes:
- name: name-1
condition:
method: GET
pathRegex: /route-1`,
},
{
err: errors.New("failed to validate ServiceProfile: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal number -5 into Go struct field RetryBudget.minRetriesPerSecond of type uint32"),
sp: `apiVersion: linkerd.io/v1alpha1
kind: ServiceProfile
metadata:
name: name.ns.svc.cluster.local
namespace: linkerd-ns
spec:
retryBudget:
minRetriesPerSecond: -5
retryRatio: 0.2
ttl: 10s
routes:
- name: name-1
condition:
method: GET
pathRegex: /route-1`,
},
}
for id, exp := range expectations {
t.Run(fmt.Sprintf("%d", id), func(t *testing.T) {
err := Validate([]byte(exp.sp))
if err != nil || exp.err != nil {
if (err == nil && exp.err != nil) ||
(err != nil && exp.err == nil) ||
(err.Error() != exp.err.Error()) {
t.Fatalf("Unexpected error (Expected: %s, Got: %s)", exp.err, err)
}
}
})
}
}
func TestValidateName(t *testing.T) {
expectations := []struct {
err error
name string
service string
namespace string
}{
{
nil,
"service.ns.svc.cluster.local",
"service",
"ns",
},
{
errors.New("ServiceProfile \"bad.name\" has invalid name (must be \"<service>.<namespace>.svc.cluster.local\")"),
"bad.name",
"",
"",
},
{
errors.New("ServiceProfile \"bad.svc.cluster.local\" has invalid name (must be \"<service>.<namespace>.svc.cluster.local\")"),
"bad.svc.cluster.local",
"",
"",
},
{
errors.New("ServiceProfile \"service.ns.svc.cluster.foo\" has invalid name (must be \"<service>.<namespace>.svc.cluster.local\")"),
"service.ns.svc.cluster.foo",
"",
"",
},
}
for id, exp := range expectations {
t.Run(fmt.Sprintf("%d", id), func(t *testing.T) {
service, namespace, err := ValidateName(exp.name)
if service != exp.service {
t.Fatalf("Unexpected service (Expected: %s, Got: %s)", exp.service, service)
}
if namespace != exp.namespace {
t.Fatalf("Unexpected namespace (Expected: %s, Got: %s)", exp.namespace, namespace)
}
if err != nil || exp.err != nil {
if (err == nil && exp.err != nil) ||
(err != nil && exp.err == nil) ||
(err.Error() != exp.err.Error()) {
t.Fatalf("Unexpected error (Expected: %s, Got: %s)", exp.err, err)
}
}
})
}
}

View File

@ -4,9 +4,9 @@ import (
"fmt"
"reflect"
"github.com/ghodss/yaml"
"github.com/linkerd/linkerd2/controller/gen/apis/serviceprofile/v1alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/yaml"
)
// GenServiceProfile generates a mock ServiceProfile.
@ -45,15 +45,15 @@ func GenServiceProfile(service, namespace, controlPlaneNamespace string) v1alpha
// ServiceProfileYamlEquals validates whether two ServiceProfiles are equal.
func ServiceProfileYamlEquals(actual, expected v1alpha1.ServiceProfile) error {
if !reflect.DeepEqual(actual, expected) {
acutalYaml, err := yaml.Marshal(actual)
actualYaml, err := yaml.Marshal(actual)
if err != nil {
return fmt.Errorf("Service profile mismatch but failed to marshal actual service profile: %v", err)
}
expectedYaml, err := yaml.Marshal(expected)
if err != nil {
return fmt.Errorf("Serivce profile mismatch but failed to marshal expected service profile: %v", err)
return fmt.Errorf("Service profile mismatch but failed to marshal expected service profile: %v", err)
}
return fmt.Errorf("Expected [%s] but got [%s]", string(expectedYaml), string(acutalYaml))
return fmt.Errorf("Expected [%s] but got [%s]", string(expectedYaml), string(actualYaml))
}
return nil
}

View File

@ -1,5 +1,5 @@
## compile proxy-init utility
FROM gcr.io/linkerd-io/go-deps:4abae893 as golang
FROM gcr.io/linkerd-io/go-deps:b457d5cb as golang
WORKDIR /go/src/github.com/linkerd/linkerd2
COPY ./proxy-init ./proxy-init
RUN CGO_ENABLED=0 GOOS=linux go install -v ./proxy-init/

View File

@ -26,7 +26,7 @@ COPY web/app .
RUN $ROOT/bin/web build
## compile go server
FROM gcr.io/linkerd-io/go-deps:4abae893 as golang
FROM gcr.io/linkerd-io/go-deps:b457d5cb as golang
WORKDIR /go/src/github.com/linkerd/linkerd2
RUN mkdir -p web
COPY web/main.go web

View File

@ -7,12 +7,12 @@ import (
"strings"
"testing"
"github.com/ghodss/yaml"
"github.com/julienschmidt/httprouter"
"github.com/linkerd/linkerd2/controller/api/public"
"github.com/linkerd/linkerd2/controller/gen/apis/serviceprofile/v1alpha1"
pb "github.com/linkerd/linkerd2/controller/gen/public"
helpers "github.com/linkerd/linkerd2/pkg/profiles"
"sigs.k8s.io/yaml"
)
func TestHandleIndex(t *testing.T) {