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"
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
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
45
46
47
48
49
50
51
52
53
54
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
115
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
158
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
213
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
250 func PathToRegex(path string) string {
251 escaped := regexp.QuoteMeta(path)
252 return pathParamRegex.ReplaceAllLiteralString(escaped, "[^/]*")
253 }
254
View as plain text