1
16
17 package create
18
19 import (
20 "context"
21 "fmt"
22 "regexp"
23 "strings"
24
25 "github.com/spf13/cobra"
26
27 networkingv1 "k8s.io/api/networking/v1"
28 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
29 "k8s.io/apimachinery/pkg/runtime"
30 "k8s.io/apimachinery/pkg/util/intstr"
31 "k8s.io/cli-runtime/pkg/genericclioptions"
32 "k8s.io/cli-runtime/pkg/genericiooptions"
33 networkingv1client "k8s.io/client-go/kubernetes/typed/networking/v1"
34 cmdutil "k8s.io/kubectl/pkg/cmd/util"
35 "k8s.io/kubectl/pkg/scheme"
36 "k8s.io/kubectl/pkg/util"
37 "k8s.io/kubectl/pkg/util/i18n"
38 "k8s.io/kubectl/pkg/util/templates"
39 )
40
41 var (
42
43
44
45
46
47
48
49 regexHostPathSvc = `^(?P<host>[\w\*\-\.]*)(?P<path>/.*)=(?P<svcname>[\w\-]+):(?P<svcport>[\w\-]+)`
50
51
52
53
54
55 regexTLS = `(,(?P<istls>tls)=?(?P<secretname>[\w\-]+)?)?`
56
57
58
59 ruleRegex = regexHostPathSvc + regexTLS
60
61 ingressLong = templates.LongDesc(i18n.T(`
62 Create an ingress with the specified name.`))
63
64 ingressExample = templates.Examples(i18n.T(`
65 # Create a single ingress called 'simple' that directs requests to foo.com/bar to svc
66 # svc1:8080 with a TLS secret "my-cert"
67 kubectl create ingress simple --rule="foo.com/bar=svc1:8080,tls=my-cert"
68
69 # Create a catch all ingress of "/path" pointing to service svc:port and Ingress Class as "otheringress"
70 kubectl create ingress catch-all --class=otheringress --rule="/path=svc:port"
71
72 # Create an ingress with two annotations: ingress.annotation1 and ingress.annotations2
73 kubectl create ingress annotated --class=default --rule="foo.com/bar=svc:port" \
74 --annotation ingress.annotation1=foo \
75 --annotation ingress.annotation2=bla
76
77 # Create an ingress with the same host and multiple paths
78 kubectl create ingress multipath --class=default \
79 --rule="foo.com/=svc:port" \
80 --rule="foo.com/admin/=svcadmin:portadmin"
81
82 # Create an ingress with multiple hosts and the pathType as Prefix
83 kubectl create ingress ingress1 --class=default \
84 --rule="foo.com/path*=svc:8080" \
85 --rule="bar.com/admin*=svc2:http"
86
87 # Create an ingress with TLS enabled using the default ingress certificate and different path types
88 kubectl create ingress ingtls --class=default \
89 --rule="foo.com/=svc:https,tls" \
90 --rule="foo.com/path/subpath*=othersvc:8080"
91
92 # Create an ingress with TLS enabled using a specific secret and pathType as Prefix
93 kubectl create ingress ingsecret --class=default \
94 --rule="foo.com/*=svc:8080,tls=secret1"
95
96 # Create an ingress with a default backend
97 kubectl create ingress ingdefault --class=default \
98 --default-backend=defaultsvc:http \
99 --rule="foo.com/*=svc:8080,tls=secret1"
100
101 `))
102 )
103
104
105 type CreateIngressOptions struct {
106 PrintFlags *genericclioptions.PrintFlags
107
108 PrintObj func(obj runtime.Object) error
109
110 Name string
111 IngressClass string
112 Rules []string
113 Annotations []string
114 DefaultBackend string
115 Namespace string
116 EnforceNamespace bool
117 CreateAnnotation bool
118
119 Client networkingv1client.NetworkingV1Interface
120 DryRunStrategy cmdutil.DryRunStrategy
121 ValidationDirective string
122
123 FieldManager string
124
125 genericiooptions.IOStreams
126 }
127
128
129 func NewCreateIngressOptions(ioStreams genericiooptions.IOStreams) *CreateIngressOptions {
130 return &CreateIngressOptions{
131 PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme),
132 IOStreams: ioStreams,
133 }
134 }
135
136
137
138 func NewCmdCreateIngress(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command {
139 o := NewCreateIngressOptions(ioStreams)
140
141 cmd := &cobra.Command{
142 Use: "ingress NAME --rule=host/path=service:port[,tls[=secret]] ",
143 DisableFlagsInUseLine: true,
144 Aliases: []string{"ing"},
145 Short: i18n.T("Create an ingress with the specified name"),
146 Long: ingressLong,
147 Example: ingressExample,
148 Run: func(cmd *cobra.Command, args []string) {
149 cmdutil.CheckErr(o.Complete(f, cmd, args))
150 cmdutil.CheckErr(o.Validate())
151 cmdutil.CheckErr(o.Run())
152 },
153 }
154
155 o.PrintFlags.AddFlags(cmd)
156
157 cmdutil.AddApplyAnnotationFlags(cmd)
158 cmdutil.AddValidateFlags(cmd)
159 cmdutil.AddDryRunFlag(cmd)
160 cmd.Flags().StringVar(&o.IngressClass, "class", o.IngressClass, "Ingress Class to be used")
161 cmd.Flags().StringArrayVar(&o.Rules, "rule", o.Rules, "Rule in format host/path=service:port[,tls=secretname]. Paths containing the leading character '*' are considered pathType=Prefix. tls argument is optional.")
162 cmd.Flags().StringVar(&o.DefaultBackend, "default-backend", o.DefaultBackend, "Default service for backend, in format of svcname:port")
163 cmd.Flags().StringArrayVar(&o.Annotations, "annotation", o.Annotations, "Annotation to insert in the ingress object, in the format annotation=value")
164 cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-create")
165
166 return cmd
167 }
168
169
170 func (o *CreateIngressOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
171 name, err := NameFromCommandArgs(cmd, args)
172 if err != nil {
173 return err
174 }
175 o.Name = name
176
177 clientConfig, err := f.ToRESTConfig()
178 if err != nil {
179 return err
180 }
181 o.Client, err = networkingv1client.NewForConfig(clientConfig)
182 if err != nil {
183 return err
184 }
185
186 o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
187 if err != nil {
188 return err
189 }
190
191 o.CreateAnnotation = cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag)
192
193 o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd)
194 if err != nil {
195 return err
196 }
197 cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy)
198
199 printer, err := o.PrintFlags.ToPrinter()
200 if err != nil {
201 return err
202 }
203 o.PrintObj = func(obj runtime.Object) error {
204 return printer.PrintObj(obj, o.Out)
205 }
206
207 o.ValidationDirective, err = cmdutil.GetValidationDirective(cmd)
208 return err
209 }
210
211
212 func (o *CreateIngressOptions) Validate() error {
213 if len(o.DefaultBackend) == 0 && len(o.Rules) == 0 {
214 return fmt.Errorf("not enough information provided: every ingress has to either specify a default-backend (which catches all traffic) or a list of rules (which catch specific paths)")
215 }
216
217 rulevalidation, err := regexp.Compile(ruleRegex)
218 if err != nil {
219 return fmt.Errorf("failed to compile the regex")
220 }
221
222 for _, rule := range o.Rules {
223 if match := rulevalidation.MatchString(rule); !match {
224 return fmt.Errorf("rule %s is invalid and should be in format host/path=svcname:svcport[,tls[=secret]]", rule)
225 }
226 }
227
228 for _, annotation := range o.Annotations {
229 if an := strings.SplitN(annotation, "=", 2); len(an) != 2 {
230 return fmt.Errorf("annotation %s is invalid and should be in format key=[value]", annotation)
231 }
232 }
233
234 if len(o.DefaultBackend) > 0 && len(strings.Split(o.DefaultBackend, ":")) != 2 {
235 return fmt.Errorf("default-backend should be in format servicename:serviceport")
236 }
237
238 return nil
239 }
240
241
242 func (o *CreateIngressOptions) Run() error {
243 ingress := o.createIngress()
244
245 if err := util.CreateOrUpdateAnnotation(o.CreateAnnotation, ingress, scheme.DefaultJSONEncoder()); err != nil {
246 return err
247 }
248
249 if o.DryRunStrategy != cmdutil.DryRunClient {
250 createOptions := metav1.CreateOptions{}
251 if o.FieldManager != "" {
252 createOptions.FieldManager = o.FieldManager
253 }
254 createOptions.FieldValidation = o.ValidationDirective
255 if o.DryRunStrategy == cmdutil.DryRunServer {
256 createOptions.DryRun = []string{metav1.DryRunAll}
257 }
258 var err error
259 ingress, err = o.Client.Ingresses(o.Namespace).Create(context.TODO(), ingress, createOptions)
260 if err != nil {
261 return fmt.Errorf("failed to create ingress: %v", err)
262 }
263 }
264 return o.PrintObj(ingress)
265 }
266
267 func (o *CreateIngressOptions) createIngress() *networkingv1.Ingress {
268 namespace := ""
269 if o.EnforceNamespace {
270 namespace = o.Namespace
271 }
272
273 annotations := o.buildAnnotations()
274 spec := o.buildIngressSpec()
275
276 ingress := &networkingv1.Ingress{
277 TypeMeta: metav1.TypeMeta{APIVersion: networkingv1.SchemeGroupVersion.String(), Kind: "Ingress"},
278 ObjectMeta: metav1.ObjectMeta{
279 Name: o.Name,
280 Namespace: namespace,
281 Annotations: annotations,
282 },
283 Spec: spec,
284 }
285 return ingress
286 }
287
288 func (o *CreateIngressOptions) buildAnnotations() map[string]string {
289
290 var annotations = make(map[string]string)
291
292 for _, annotation := range o.Annotations {
293 an := strings.SplitN(annotation, "=", 2)
294 annotations[an[0]] = an[1]
295 }
296 return annotations
297 }
298
299
300 func (o *CreateIngressOptions) buildIngressSpec() networkingv1.IngressSpec {
301 var ingressSpec networkingv1.IngressSpec
302
303 if len(o.IngressClass) > 0 {
304 ingressSpec.IngressClassName = &o.IngressClass
305 }
306
307 if len(o.DefaultBackend) > 0 {
308 defaultbackend := buildIngressBackendSvc(o.DefaultBackend)
309 ingressSpec.DefaultBackend = &defaultbackend
310 }
311 ingressSpec.TLS = o.buildTLSRules()
312 ingressSpec.Rules = o.buildIngressRules()
313
314 return ingressSpec
315 }
316
317 func (o *CreateIngressOptions) buildTLSRules() []networkingv1.IngressTLS {
318 hostAlreadyPresent := make(map[string]struct{})
319
320 ingressTLSs := []networkingv1.IngressTLS{}
321 var secret string
322
323 for _, rule := range o.Rules {
324 tls := strings.Split(rule, ",")
325
326 if len(tls) == 2 {
327 ingressTLS := networkingv1.IngressTLS{}
328 host := strings.SplitN(rule, "/", 2)[0]
329 secret = ""
330 secretName := strings.Split(tls[1], "=")
331
332 if len(secretName) > 1 {
333 secret = secretName[1]
334 }
335
336 idxSecret := getIndexSecret(secret, ingressTLSs)
337
338 if _, ok := hostAlreadyPresent[host]; !ok {
339 if idxSecret > -1 {
340 ingressTLSs[idxSecret].Hosts = append(ingressTLSs[idxSecret].Hosts, host)
341 hostAlreadyPresent[host] = struct{}{}
342 continue
343 }
344 if host != "" {
345 ingressTLS.Hosts = append(ingressTLS.Hosts, host)
346 }
347 if secret != "" {
348 ingressTLS.SecretName = secret
349 }
350 if len(ingressTLS.SecretName) > 0 || len(ingressTLS.Hosts) > 0 {
351 ingressTLSs = append(ingressTLSs, ingressTLS)
352 }
353 hostAlreadyPresent[host] = struct{}{}
354 }
355 }
356 }
357 return ingressTLSs
358 }
359
360
361 func (o *CreateIngressOptions) buildIngressRules() []networkingv1.IngressRule {
362 ingressRules := []networkingv1.IngressRule{}
363
364 for _, rule := range o.Rules {
365 removeTLS := strings.Split(rule, ",")[0]
366 hostSplit := strings.SplitN(removeTLS, "/", 2)
367 host := hostSplit[0]
368 ingressPath := buildHTTPIngressPath(hostSplit[1])
369 ingressRule := networkingv1.IngressRule{}
370
371 if host != "" {
372 ingressRule.Host = host
373 }
374
375 idxHost := getIndexHost(ingressRule.Host, ingressRules)
376 if idxHost > -1 {
377 ingressRules[idxHost].IngressRuleValue.HTTP.Paths = append(ingressRules[idxHost].IngressRuleValue.HTTP.Paths, ingressPath)
378 continue
379 }
380
381 ingressRule.IngressRuleValue = networkingv1.IngressRuleValue{
382 HTTP: &networkingv1.HTTPIngressRuleValue{
383 Paths: []networkingv1.HTTPIngressPath{
384 ingressPath,
385 },
386 },
387 }
388 ingressRules = append(ingressRules, ingressRule)
389 }
390 return ingressRules
391 }
392
393 func buildHTTPIngressPath(pathsvc string) networkingv1.HTTPIngressPath {
394 pathsvcsplit := strings.Split(pathsvc, "=")
395 path := "/" + pathsvcsplit[0]
396 service := pathsvcsplit[1]
397
398 var pathType networkingv1.PathType
399 pathType = "Exact"
400
401
402 if path[len(path)-1:] == "*" {
403 pathType = "Prefix"
404 path = path[0 : len(path)-1]
405 }
406
407 httpIngressPath := networkingv1.HTTPIngressPath{
408 Path: path,
409 PathType: &pathType,
410 Backend: buildIngressBackendSvc(service),
411 }
412 return httpIngressPath
413 }
414
415 func buildIngressBackendSvc(service string) networkingv1.IngressBackend {
416 svcname := strings.Split(service, ":")[0]
417 svcport := strings.Split(service, ":")[1]
418
419 ingressBackend := networkingv1.IngressBackend{
420 Service: &networkingv1.IngressServiceBackend{
421 Name: svcname,
422 Port: parseServiceBackendPort(svcport),
423 },
424 }
425 return ingressBackend
426 }
427
428 func parseServiceBackendPort(port string) networkingv1.ServiceBackendPort {
429 var backendPort networkingv1.ServiceBackendPort
430 portIntOrStr := intstr.Parse(port)
431
432 if portIntOrStr.Type == intstr.Int {
433 backendPort.Number = portIntOrStr.IntVal
434 }
435
436 if portIntOrStr.Type == intstr.String {
437 backendPort.Name = portIntOrStr.StrVal
438 }
439 return backendPort
440 }
441
442 func getIndexHost(host string, rules []networkingv1.IngressRule) int {
443 for index, v := range rules {
444 if v.Host == host {
445 return index
446 }
447 }
448 return -1
449 }
450
451 func getIndexSecret(secretname string, tls []networkingv1.IngressTLS) int {
452 for index, v := range tls {
453 if v.SecretName == secretname {
454 return index
455 }
456 }
457 return -1
458 }
459
View as plain text