1package migration
2
3import (
4	"errors"
5
6	"github.com/coredns/corefile-migration/migration/corefile"
7)
8
9type plugin struct {
10	status         string
11	replacedBy     string
12	additional     string
13	namedOptions   map[string]option
14	patternOptions map[string]option
15	action         pluginActionFn // action affecting this plugin only
16	add            serverActionFn // action to add a new plugin to the server block
17	downAction     pluginActionFn // downgrade action affecting this plugin only
18}
19
20type option struct {
21	name       string
22	status     string
23	replacedBy string
24	additional string
25	action     optionActionFn // action affecting this option only
26	add        pluginActionFn // action to add the option to the plugin
27	downAction optionActionFn // downgrade action affecting this option only
28}
29
30type corefileAction func(*corefile.Corefile) (*corefile.Corefile, error)
31type serverActionFn func(*corefile.Server) (*corefile.Server, error)
32type pluginActionFn func(*corefile.Plugin) (*corefile.Plugin, error)
33type optionActionFn func(*corefile.Option) (*corefile.Option, error)
34
35// plugins holds a map of plugin names and their migration rules per "version".  "Version" here is meaningless outside
36// of the context of this code. Each change in options or migration actions for a plugin requires a new "version"
37// containing those new/removed options and migration actions. Plugins in CoreDNS are not versioned.
38var plugins = map[string]map[string]plugin{
39	"kubernetes": {
40		"v1": plugin{
41			namedOptions: map[string]option{
42				"resyncperiod":       {},
43				"endpoint":           {},
44				"tls":                {},
45				"namespaces":         {},
46				"labels":             {},
47				"pods":               {},
48				"endpoint_pod_names": {},
49				"upstream":           {},
50				"ttl":                {},
51				"noendpoints":        {},
52				"transfer":           {},
53				"fallthrough":        {},
54				"ignore":             {},
55			},
56		},
57		"v2": plugin{
58			namedOptions: map[string]option{
59				"resyncperiod":       {},
60				"endpoint":           {},
61				"tls":                {},
62				"namespaces":         {},
63				"labels":             {},
64				"pods":               {},
65				"endpoint_pod_names": {},
66				"upstream":           {},
67				"ttl":                {},
68				"noendpoints":        {},
69				"transfer":           {},
70				"fallthrough":        {},
71				"ignore":             {},
72				"kubeconfig":         {}, // new option
73			},
74		},
75		"v3": plugin{
76			namedOptions: map[string]option{
77				"resyncperiod": {},
78				"endpoint": { // new deprecation
79					status: SevDeprecated,
80					action: useFirstArgumentOnly,
81				},
82				"tls":                {},
83				"kubeconfig":         {},
84				"namespaces":         {},
85				"labels":             {},
86				"pods":               {},
87				"endpoint_pod_names": {},
88				"upstream":           {},
89				"ttl":                {},
90				"noendpoints":        {},
91				"transfer":           {},
92				"fallthrough":        {},
93				"ignore":             {},
94			},
95		},
96		"v4": plugin{
97			namedOptions: map[string]option{
98				"resyncperiod": {},
99				"endpoint": {
100					status: SevIgnored,
101					action: useFirstArgumentOnly,
102				},
103				"tls":                {},
104				"kubeconfig":         {},
105				"namespaces":         {},
106				"labels":             {},
107				"pods":               {},
108				"endpoint_pod_names": {},
109				"upstream": { // new deprecation
110					status: SevDeprecated,
111					action: removeOption,
112				},
113				"ttl":         {},
114				"noendpoints": {},
115				"transfer":    {},
116				"fallthrough": {},
117				"ignore":      {},
118			},
119		},
120		"v5": plugin{
121			namedOptions: map[string]option{
122				"resyncperiod": { // new deprecation
123					status: SevDeprecated,
124					action: removeOption,
125				},
126				"endpoint": {
127					status: SevIgnored,
128					action: useFirstArgumentOnly,
129				},
130				"tls":                {},
131				"kubeconfig":         {},
132				"namespaces":         {},
133				"labels":             {},
134				"pods":               {},
135				"endpoint_pod_names": {},
136				"upstream": {
137					status: SevIgnored,
138					action: removeOption,
139				},
140				"ttl":         {},
141				"noendpoints": {},
142				"transfer":    {},
143				"fallthrough": {},
144				"ignore":      {},
145			},
146		},
147		"v6": plugin{
148			namedOptions: map[string]option{
149				"resyncperiod": { // now ignored
150					status: SevIgnored,
151					action: removeOption,
152				},
153				"endpoint": {
154					status: SevIgnored,
155					action: useFirstArgumentOnly,
156				},
157				"tls":                {},
158				"kubeconfig":         {},
159				"namespaces":         {},
160				"labels":             {},
161				"pods":               {},
162				"endpoint_pod_names": {},
163				"upstream": {
164					status: SevIgnored,
165					action: removeOption,
166				},
167				"ttl":         {},
168				"noendpoints": {},
169				"transfer":    {},
170				"fallthrough": {},
171				"ignore":      {},
172			},
173		},
174		"v7": plugin{
175			namedOptions: map[string]option{
176				"resyncperiod": { // new removal
177					status: SevRemoved,
178					action: removeOption,
179				},
180				"endpoint": {
181					status: SevIgnored,
182					action: useFirstArgumentOnly,
183				},
184				"tls":                {},
185				"kubeconfig":         {},
186				"namespaces":         {},
187				"labels":             {},
188				"pods":               {},
189				"endpoint_pod_names": {},
190				"upstream": { // new removal
191					status: SevRemoved,
192					action: removeOption,
193				},
194				"ttl":         {},
195				"noendpoints": {},
196				"transfer":    {},
197				"fallthrough": {},
198				"ignore":      {},
199			},
200		},
201		"v8 remove transfer option": plugin{
202			namedOptions: map[string]option{
203				"endpoint": {
204					status: SevIgnored,
205					action: useFirstArgumentOnly,
206				},
207				"tls":                {},
208				"kubeconfig":         {},
209				"namespaces":         {},
210				"labels":             {},
211				"pods":               {},
212				"endpoint_pod_names": {},
213				"ttl":                {},
214				"noendpoints":        {},
215				"transfer": {
216					status: SevRemoved,
217					action: removeOption,
218				},
219				"fallthrough": {},
220				"ignore":      {},
221			},
222		},
223		"v8": plugin{
224			namedOptions: map[string]option{
225				"endpoint": {
226					status: SevIgnored,
227					action: useFirstArgumentOnly,
228				},
229				"tls":                {},
230				"kubeconfig":         {},
231				"namespaces":         {},
232				"labels":             {},
233				"pods":               {},
234				"endpoint_pod_names": {},
235				"ttl":                {},
236				"noendpoints":        {},
237				"fallthrough":        {},
238				"ignore":             {},
239			},
240		},
241	},
242
243	"errors": {
244		"v1": plugin{},
245		"v2": plugin{
246			namedOptions: map[string]option{
247				"consolidate": {},
248			},
249		},
250	},
251
252	"health": {
253		"v1": plugin{
254			namedOptions: map[string]option{
255				"lameduck": {},
256			},
257		},
258		"v1 add lameduck": plugin{
259			namedOptions: map[string]option{
260				"lameduck": {
261					status: SevNewDefault,
262					add: func(c *corefile.Plugin) (*corefile.Plugin, error) {
263						return addOptionToPlugin(c, &corefile.Option{Name: "lameduck 5s"})
264					},
265					downAction: removeOption,
266				},
267			},
268		},
269	},
270
271	"hosts": {
272		"v1": plugin{
273			namedOptions: map[string]option{
274				"ttl":         {},
275				"no_reverse":  {},
276				"reload":      {},
277				"fallthrough": {},
278			},
279			patternOptions: map[string]option{
280				`\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}`:              {}, // close enough
281				`[0-9A-Fa-f]{1,4}:[:0-9A-Fa-f]+:[0-9A-Fa-f]{1,4}`: {}, // less close, but still close enough
282			},
283		},
284	},
285
286	"rewrite": {
287		"v1": plugin{
288			namedOptions: map[string]option{
289				"type":        {},
290				"class":       {},
291				"name":        {},
292				"answer name": {},
293				"edns0":       {},
294			},
295		},
296		"v2": plugin{
297			namedOptions: map[string]option{
298				"type":        {},
299				"class":       {},
300				"name":        {},
301				"answer name": {},
302				"edns0":       {},
303				"ttl":         {}, // new option
304			},
305		},
306	},
307
308	"log": {
309		"v1": plugin{
310			namedOptions: map[string]option{
311				"class": {},
312			},
313		},
314	},
315
316	"cache": {
317		"v1": plugin{
318			namedOptions: map[string]option{
319				"success":  {},
320				"denial":   {},
321				"prefetch": {},
322			},
323		},
324		"v2": plugin{
325			namedOptions: map[string]option{
326				"success":     {},
327				"denial":      {},
328				"prefetch":    {},
329				"serve_stale": {}, // new option
330			},
331		},
332	},
333
334	"forward": {
335		"v1": plugin{
336			namedOptions: map[string]option{
337				"except":         {},
338				"force_tcp":      {},
339				"expire":         {},
340				"max_fails":      {},
341				"tls":            {},
342				"tls_servername": {},
343				"policy":         {},
344				"health_check":   {},
345			},
346		},
347		"v2": plugin{
348			namedOptions: map[string]option{
349				"except":         {},
350				"force_tcp":      {},
351				"prefer_udp":     {},
352				"expire":         {},
353				"max_fails":      {},
354				"tls":            {},
355				"tls_servername": {},
356				"policy":         {},
357				"health_check":   {},
358			},
359		},
360		"v3": plugin{
361			namedOptions: map[string]option{
362				"except":         {},
363				"force_tcp":      {},
364				"prefer_udp":     {},
365				"expire":         {},
366				"max_fails":      {},
367				"tls":            {},
368				"tls_servername": {},
369				"policy":         {},
370				"health_check":   {},
371				"max_concurrent": {},
372			},
373		},
374		"v3 add max_concurrent": plugin{
375			namedOptions: map[string]option{
376				"except":         {},
377				"force_tcp":      {},
378				"prefer_udp":     {},
379				"expire":         {},
380				"max_fails":      {},
381				"tls":            {},
382				"tls_servername": {},
383				"policy":         {},
384				"health_check":   {},
385				"max_concurrent": { // new option
386					status: SevNewDefault,
387					add: func(c *corefile.Plugin) (*corefile.Plugin, error) {
388						return addOptionToPlugin(c, &corefile.Option{Name: "max_concurrent 1000"})
389					},
390					downAction: removeOption,
391				},
392			},
393		},
394	},
395
396	"k8s_external": {
397		"v1": plugin{
398			namedOptions: map[string]option{
399				"apex": {},
400				"ttl":  {},
401			},
402		},
403	},
404
405	"proxy": {
406		"v1": plugin{
407			namedOptions: map[string]option{
408				"policy":       {},
409				"fail_timeout": {},
410				"max_fails":    {},
411				"health_check": {},
412				"except":       {},
413				"spray":        {},
414				"protocol": { // https_google option ignored
415					status: SevIgnored,
416					action: proxyRemoveHttpsGoogleProtocol,
417				},
418			},
419		},
420		"v2": plugin{
421			namedOptions: map[string]option{
422				"policy":       {},
423				"fail_timeout": {},
424				"max_fails":    {},
425				"health_check": {},
426				"except":       {},
427				"spray":        {},
428				"protocol": { // https_google option removed
429					status: SevRemoved,
430					action: proxyRemoveHttpsGoogleProtocol,
431				},
432			},
433		},
434		"deprecation": plugin{ // proxy -> forward deprecation migration
435			status:       SevDeprecated,
436			replacedBy:   "forward",
437			action:       proxyToForwardPluginAction,
438			namedOptions: proxyToForwardOptionsMigrations,
439		},
440		"removal": plugin{ // proxy -> forward forced migration
441			status:       SevRemoved,
442			replacedBy:   "forward",
443			action:       proxyToForwardPluginAction,
444			namedOptions: proxyToForwardOptionsMigrations,
445		},
446	},
447
448	"transfer": {
449		"v1": plugin{
450			namedOptions: map[string]option{
451				"to": {},
452			},
453		},
454	},
455}
456
457func removePlugin(*corefile.Plugin) (*corefile.Plugin, error) { return nil, nil }
458func removeOption(*corefile.Option) (*corefile.Option, error) { return nil, nil }
459
460func renamePlugin(p *corefile.Plugin, to string) (*corefile.Plugin, error) {
461	p.Name = to
462	return p, nil
463}
464
465func addToServerBlockWithPlugins(sb *corefile.Server, newPlugin *corefile.Plugin, with []string) (*corefile.Server, error) {
466	if len(with) == 0 {
467		// add to all blocks
468		sb.Plugins = append(sb.Plugins, newPlugin)
469		return sb, nil
470	}
471	for _, p := range sb.Plugins {
472		for _, w := range with {
473			if w == p.Name {
474				// add to this block
475				sb.Plugins = append(sb.Plugins, newPlugin)
476				return sb, nil
477			}
478		}
479	}
480	return sb, nil
481}
482
483func copyKubernetesTransferOptToPlugin(cf *corefile.Corefile) (*corefile.Corefile, error) {
484	for _, s := range cf.Servers {
485		var (
486			to   []string
487			zone string
488		)
489		for _, p := range s.Plugins {
490			if p.Name != "kubernetes" {
491				continue
492			}
493			zone = p.Args[0]
494			for _, o := range p.Options {
495				if o.Name != "transfer" {
496					continue
497				}
498				to = o.Args
499			}
500		}
501		if len(to) < 2 {
502			continue
503		}
504		s.Plugins = append(s.Plugins, &corefile.Plugin{
505			Name:    "transfer",
506			Args:    []string{zone},
507			Options: []*corefile.Option{{Name: "to", Args: to[1:]}},
508		})
509	}
510	return cf, nil
511}
512
513func addToKubernetesServerBlocks(sb *corefile.Server, newPlugin *corefile.Plugin) (*corefile.Server, error) {
514	return addToServerBlockWithPlugins(sb, newPlugin, []string{"kubernetes"})
515}
516
517func addToForwardingServerBlocks(sb *corefile.Server, newPlugin *corefile.Plugin) (*corefile.Server, error) {
518	return addToServerBlockWithPlugins(sb, newPlugin, []string{"forward", "proxy"})
519}
520
521func addToAllServerBlocks(sb *corefile.Server, newPlugin *corefile.Plugin) (*corefile.Server, error) {
522	return addToServerBlockWithPlugins(sb, newPlugin, []string{})
523}
524
525func addOptionToPlugin(pl *corefile.Plugin, newOption *corefile.Option) (*corefile.Plugin, error) {
526	pl.Options = append(pl.Options, newOption)
527	return pl, nil
528}
529
530var proxyToForwardOptionsMigrations = map[string]option{
531	"policy": {
532		action: func(o *corefile.Option) (*corefile.Option, error) {
533			if len(o.Args) == 1 && o.Args[0] == "least_conn" {
534				o.Name = "force_tcp"
535				o.Args = nil
536			}
537			return o, nil
538		},
539	},
540	"except":       {},
541	"fail_timeout": {action: removeOption},
542	"max_fails":    {action: removeOption},
543	"health_check": {action: removeOption},
544	"spray":        {action: removeOption},
545	"protocol": {
546		action: func(o *corefile.Option) (*corefile.Option, error) {
547			if len(o.Args) >= 2 && o.Args[0] == "force_tcp" {
548				o.Name = "force_tcp"
549				o.Args = nil
550				return o, nil
551			}
552			return nil, nil
553		},
554	},
555}
556
557var proxyToForwardPluginAction = func(p *corefile.Plugin) (*corefile.Plugin, error) {
558	return renamePlugin(p, "forward")
559}
560
561var useFirstArgumentOnly = func(o *corefile.Option) (*corefile.Option, error) {
562	if len(o.Args) < 1 {
563		return o, nil
564	}
565	o.Args = o.Args[:1]
566	return o, nil
567}
568
569var proxyRemoveHttpsGoogleProtocol = func(o *corefile.Option) (*corefile.Option, error) {
570	if len(o.Args) > 0 && o.Args[0] == "https_google" {
571		return nil, nil
572	}
573	return o, nil
574}
575
576func breakForwardStubDomainsIntoServerBlocks(cf *corefile.Corefile) (*corefile.Corefile, error) {
577	for _, sb := range cf.Servers {
578		for j, fwd := range sb.Plugins {
579			if fwd.Name != "forward" {
580				continue
581			}
582			if len(fwd.Args) == 0 {
583				return nil, errors.New("found invalid forward plugin declaration")
584			}
585			if fwd.Args[0] == "." {
586				// dont move the default upstream
587				continue
588			}
589			if len(sb.DomPorts) != 1 {
590				return cf, errors.New("unhandled migration of multi-domain/port server block")
591			}
592			if sb.DomPorts[0] != "." && sb.DomPorts[0] != ".:53" {
593				return cf, errors.New("unhandled migration of non-default domain/port server block")
594			}
595
596			newSb := &corefile.Server{}                // create a new server block
597			newSb.DomPorts = []string{fwd.Args[0]}     // copy the forward zone to the server block domain
598			fwd.Args[0] = "."                          // the plugin's zone changes to "." for brevity
599			newSb.Plugins = append(newSb.Plugins, fwd) // add the plugin to its new server block
600
601			// Add appropriate addtl plugins to new server block
602			newSb.Plugins = append(newSb.Plugins, &corefile.Plugin{Name: "loop"})
603			newSb.Plugins = append(newSb.Plugins, &corefile.Plugin{Name: "errors"})
604			newSb.Plugins = append(newSb.Plugins, &corefile.Plugin{Name: "cache", Args: []string{"30"}})
605
606			//add new server block to corefile
607			cf.Servers = append(cf.Servers, newSb)
608
609			//remove the forward plugin from the original server block
610			sb.Plugins = append(sb.Plugins[:j], sb.Plugins[j+1:]...)
611		}
612	}
613	return cf, nil
614}
615