mirror of https://github.com/linkerd/linkerd2.git
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:
parent
72812baf99
commit
907f01fba6
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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{}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(`\\{[^\}]*\\}`)
|
||||
|
|
|
|||
|
|
@ -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, "."),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue