code moved (#186)

This commit is contained in:
Chris O'Haver 2019-07-29 13:25:50 -04:00 committed by GitHub
parent 2900b2a671
commit 2f535e77e2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 3 additions and 3493 deletions

View file

@ -1,106 +1,3 @@
# K8s/CoreDNS Corefile Migration Tools
This Go library provides a set of functions to help handle migrations of CoreDNS Corefiles to be compatible
with new versions of CoreDNS. The task of upgrading CoreDNS is the responsibility of a variety of Kubernetes
management tools (e.g. kubeadm and others), and the precise behavior may be different for each one. This
library abstracts some basic helper functions that make this easier to implement.
## Notifications
Several functions in the library return a list of Notices. Each Notice is a warning of a feature deprecation,
an unsupported plugin/option, or a new required plugin/option added to the Corefile. A Notice has a `ToString()`
For display to an end user. e.g.
```
Plugin "foo" is deprecated in <version>. It is replaced by "bar".
Plugin "bar" is removed in <version>. It is replaced by "qux".
Option "foo" in plugin "bar" is added as a default in <version>.
Plugin "baz" is unsupported by this migration tool in <version>.
```
## Functions
### func Deprecated
`Deprecated(fromCoreDNSVersion, toCoreDNSVersion, corefileStr string) ([]Notice, error)`
Deprecated returns a list of deprecation notices affecting the given Corefile. Notices are returned for
any deprecated, removed, or ignored plugins/options present in the Corefile. Notices are also returned for
any new default plugins that would be added in a migration.
### func Migrate
`Migrate(fromCoreDNSVersion, toCoreDNSVersion, corefileStr string, deprecations bool) (string, error)`
Migrate returns a migrated version of the Corefile, or an error if it cannot. The _to_ version
must be >= the _from_ version. It will:
* replace/convert any plugins/options that have replacements (e.g. _proxy_ -> _forward_)
* return an error if replacable plugins/options cannot be converted (e.g. proxy _options_ not available in _forward_)
* remove plugins/options that do not have replacements (e.g. kubernetes `upstream`)
* add in any new default plugins where applicable if they are not already present.
* If deprecations is true, deprecated plugins/options will be migrated as soon as they are deprecated.
* If deprecations is false, deprecated plugins/options will be migrated only once they become removed or ignored.
### func MigrateDown
`MigrateDown(fromCoreDNSVersion, toCoreDNSVersion, corefileStr string) (string, error)`
MigrateDown returns a downward migrated version of the Corefile, or an error if it cannot. The _to_ version
must be <= the _from_ version.
* It will handle the removal of plugins and options that no longer exist in the destination
version when downgrading.
* It will not restore plugins/options that might have been removed or altered during an upward migration.
### func Unsupported
`Unsupported(fromCoreDNSVersion, toCoreDNSVersion, corefileStr string) ([]Notice, error)`
Unsupported returns a list Notices for plugins/options that are unhandled by this migration tool,
but may still be valid in CoreDNS. Currently, only a subset of plugins included by default in CoreDNS are supported
by this tool.
### func Default
`Default(k8sVersion, corefileStr string) bool`
Default returns true if the Corefile is the default for a given version of Kubernetes.
Or, if k8sVersion is empty, Default returns true if the Corefile is the default for any version of Kubernetes.
### func Released
`Released(dockerImageSHA string) bool`
Released returns true if dockerImageSHA matches any released image of CoreDNS.
### func ValidVersions
`ValidVersions() []string`
ValidVersions returns a list of all versions supported by this tool.
## Command Line Converter Example
An example use of this library is provided [here](../corefile-tool/).
## Kubernetes Cluster Managemnt Tool Usage
This is an example flow of how this library could be used by a Kubernetes cluster management tool to perform a
Corefile migration during an upgrade...
0. Check `Released()` to verify that the installed version of CoreDNS is an official release.
1. Check `Default()`, if the Corefile is a default, simply re-deploy the new default over top the old one. No migration needed.
If the Corefile is not a default, continue...
2. Check `Deprecated()`, if anything is deprecated, warn user, but continue install.
3. Check `Unsupported()`, if anything is unsupported, abort and warn user (allow user to override to pass this).
4. Call `Migrate()`, if there is an error, abort and warn user.
5. If there is no error, pause and ask user if they want to continue with the migration. If the starting Corefile was at defaults,
proceed use the migrated Corefile.
# K8s/CoreDNS Corefile Migration
Corefile Migration has moved to https://github.com/coredns/corefile-migration/tree/master/migration

View file

@ -1,33 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddy
import "io"
// allTokens lexes the entire input, but does not parse it.
// It returns all the tokens from the input, unstructured
// and in order.
func allTokens(input io.Reader) ([]Token, error) {
l := new(lexer)
err := l.load(input)
if err != nil {
return nil, err
}
var tokens []Token
for l.next() {
tokens = append(tokens, l.token)
}
return tokens, nil
}

View file

@ -1,260 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddy
import (
"errors"
"fmt"
"io"
"strings"
)
// Dispenser is a type that dispenses tokens, similarly to a lexer,
// except that it can do so with some notion of structure and has
// some really convenient methods.
type Dispenser struct {
filename string
tokens []Token
cursor int
nesting int
}
// NewDispenser returns a Dispenser, ready to use for parsing the given input.
func NewDispenser(filename string, input io.Reader) Dispenser {
tokens, _ := allTokens(input) // ignoring error because nothing to do with it
return Dispenser{
filename: filename,
tokens: tokens,
cursor: -1,
}
}
// NewDispenserTokens returns a Dispenser filled with the given tokens.
func NewDispenserTokens(filename string, tokens []Token) Dispenser {
return Dispenser{
filename: filename,
tokens: tokens,
cursor: -1,
}
}
// Next loads the next token. Returns true if a token
// was loaded; false otherwise. If false, all tokens
// have been consumed.
func (d *Dispenser) Next() bool {
if d.cursor < len(d.tokens)-1 {
d.cursor++
return true
}
return false
}
// NextArg loads the next token if it is on the same
// line. Returns true if a token was loaded; false
// otherwise. If false, all tokens on the line have
// been consumed. It handles imported tokens correctly.
func (d *Dispenser) NextArg() bool {
if d.cursor < 0 {
d.cursor++
return true
}
if d.cursor >= len(d.tokens) {
return false
}
if d.cursor < len(d.tokens)-1 &&
d.tokens[d.cursor].File == d.tokens[d.cursor+1].File &&
d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) == d.tokens[d.cursor+1].Line {
d.cursor++
return true
}
return false
}
// NextLine loads the next token only if it is not on the same
// line as the current token, and returns true if a token was
// loaded; false otherwise. If false, there is not another token
// or it is on the same line. It handles imported tokens correctly.
func (d *Dispenser) NextLine() bool {
if d.cursor < 0 {
d.cursor++
return true
}
if d.cursor >= len(d.tokens) {
return false
}
if d.cursor < len(d.tokens)-1 &&
(d.tokens[d.cursor].File != d.tokens[d.cursor+1].File ||
d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) < d.tokens[d.cursor+1].Line) {
d.cursor++
return true
}
return false
}
// NextBlock can be used as the condition of a for loop
// to load the next token as long as it opens a block or
// is already in a block. It returns true if a token was
// loaded, or false when the block's closing curly brace
// was loaded and thus the block ended. Nested blocks are
// not supported.
func (d *Dispenser) NextBlock() bool {
if d.nesting > 0 {
d.Next()
if d.Val() == "}" {
d.nesting--
return false
}
return true
}
if !d.NextArg() { // block must open on same line
return false
}
if d.Val() != "{" {
d.cursor-- // roll back if not opening brace
return false
}
d.Next()
if d.Val() == "}" {
// Open and then closed right away
return false
}
d.nesting++
return true
}
// Val gets the text of the current token. If there is no token
// loaded, it returns empty string.
func (d *Dispenser) Val() string {
if d.cursor < 0 || d.cursor >= len(d.tokens) {
return ""
}
return d.tokens[d.cursor].Text
}
// Line gets the line number of the current token. If there is no token
// loaded, it returns 0.
func (d *Dispenser) Line() int {
if d.cursor < 0 || d.cursor >= len(d.tokens) {
return 0
}
return d.tokens[d.cursor].Line
}
// File gets the filename of the current token. If there is no token loaded,
// it returns the filename originally given when parsing started.
func (d *Dispenser) File() string {
if d.cursor < 0 || d.cursor >= len(d.tokens) {
return d.filename
}
if tokenFilename := d.tokens[d.cursor].File; tokenFilename != "" {
return tokenFilename
}
return d.filename
}
// Args is a convenience function that loads the next arguments
// (tokens on the same line) into an arbitrary number of strings
// pointed to in targets. If there are fewer tokens available
// than string pointers, the remaining strings will not be changed
// and false will be returned. If there were enough tokens available
// to fill the arguments, then true will be returned.
func (d *Dispenser) Args(targets ...*string) bool {
enough := true
for i := 0; i < len(targets); i++ {
if !d.NextArg() {
enough = false
break
}
*targets[i] = d.Val()
}
return enough
}
// RemainingArgs loads any more arguments (tokens on the same line)
// into a slice and returns them. Open curly brace tokens also indicate
// the end of arguments, and the curly brace is not included in
// the return value nor is it loaded.
func (d *Dispenser) RemainingArgs() []string {
var args []string
for d.NextArg() {
if d.Val() == "{" {
d.cursor--
break
}
args = append(args, d.Val())
}
return args
}
// ArgErr returns an argument error, meaning that another
// argument was expected but not found. In other words,
// a line break or open curly brace was encountered instead of
// an argument.
func (d *Dispenser) ArgErr() error {
if d.Val() == "{" {
return d.Err("Unexpected token '{', expecting argument")
}
return d.Errf("Wrong argument count or unexpected line ending after '%s'", d.Val())
}
// SyntaxErr creates a generic syntax error which explains what was
// found and what was expected.
func (d *Dispenser) SyntaxErr(expected string) error {
msg := fmt.Sprintf("%s:%d - Syntax error: Unexpected token '%s', expecting '%s'", d.File(), d.Line(), d.Val(), expected)
return errors.New(msg)
}
// EOFErr returns an error indicating that the dispenser reached
// the end of the input when searching for the next token.
func (d *Dispenser) EOFErr() error {
return d.Errf("Unexpected EOF")
}
// Err generates a custom parse-time error with a message of msg.
func (d *Dispenser) Err(msg string) error {
msg = fmt.Sprintf("%s:%d - Error during parsing: %s", d.File(), d.Line(), msg)
return errors.New(msg)
}
// Errf is like Err, but for formatted error messages
func (d *Dispenser) Errf(format string, args ...interface{}) error {
return d.Err(fmt.Sprintf(format, args...))
}
// numLineBreaks counts how many line breaks are in the token
// value given by the token index tknIdx. It returns 0 if the
// token does not exist or there are no line breaks.
func (d *Dispenser) numLineBreaks(tknIdx int) int {
if tknIdx < 0 || tknIdx >= len(d.tokens) {
return 0
}
return strings.Count(d.tokens[tknIdx].Text, "\n")
}
// isNewLine determines whether the current token is on a different
// line (higher line number) than the previous token. It handles imported
// tokens correctly. If there isn't a previous token, it returns true.
func (d *Dispenser) isNewLine() bool {
if d.cursor < 1 {
return true
}
if d.cursor > len(d.tokens)-1 {
return false
}
return d.tokens[d.cursor-1].File != d.tokens[d.cursor].File ||
d.tokens[d.cursor-1].Line+d.numLineBreaks(d.cursor-1) < d.tokens[d.cursor].Line
}

View file

@ -1,150 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddy
import (
"bufio"
"io"
"unicode"
)
type (
// lexer is a utility which can get values, token by
// token, from a Reader. A token is a word, and tokens
// are separated by whitespace. A word can be enclosed
// in quotes if it contains whitespace.
lexer struct {
reader *bufio.Reader
token Token
line int
}
// Token represents a single parsable unit.
Token struct {
File string
Line int
Text string
}
)
// load prepares the lexer to scan an input for tokens.
// It discards any leading byte order mark.
func (l *lexer) load(input io.Reader) error {
l.reader = bufio.NewReader(input)
l.line = 1
// discard byte order mark, if present
firstCh, _, err := l.reader.ReadRune()
if err != nil {
return err
}
if firstCh != 0xFEFF {
err := l.reader.UnreadRune()
if err != nil {
return err
}
}
return nil
}
// next loads the next token into the lexer.
// A token is delimited by whitespace, unless
// the token starts with a quotes character (")
// in which case the token goes until the closing
// quotes (the enclosing quotes are not included).
// Inside quoted strings, quotes may be escaped
// with a preceding \ character. No other chars
// may be escaped. The rest of the line is skipped
// if a "#" character is read in. Returns true if
// a token was loaded; false otherwise.
func (l *lexer) next() bool {
var val []rune
var comment, quoted, escaped bool
makeToken := func() bool {
l.token.Text = string(val)
return true
}
for {
ch, _, err := l.reader.ReadRune()
if err != nil {
if len(val) > 0 {
return makeToken()
}
if err == io.EOF {
return false
}
panic(err)
}
if quoted {
if !escaped {
if ch == '\\' {
escaped = true
continue
} else if ch == '"' {
quoted = false
return makeToken()
}
}
if ch == '\n' {
l.line++
}
if escaped {
// only escape quotes
if ch != '"' {
val = append(val, '\\')
}
}
val = append(val, ch)
escaped = false
continue
}
if unicode.IsSpace(ch) {
if ch == '\r' {
continue
}
if ch == '\n' {
l.line++
comment = false
}
if len(val) > 0 {
return makeToken()
}
continue
}
if ch == '#' {
comment = true
}
if comment {
continue
}
if len(val) == 0 {
l.token = Token{Line: l.line}
if ch == '"' {
quoted = true
continue
}
}
val = append(val, ch)
}
}

View file

@ -1,179 +0,0 @@
package corefile
import (
"strings"
"github.com/coredns/deployment/kubernetes/migration/caddy"
)
type Corefile struct {
Servers []*Server
}
type Server struct {
DomPorts []string
Plugins []*Plugin
}
type Plugin struct {
Name string
Args []string
Options []*Option
}
type Option struct {
Name string
Args []string
}
func New(s string) (*Corefile, error) {
c := Corefile{}
cc := caddy.NewDispenser("migration", strings.NewReader(s))
depth := 0
var cSvr *Server
var cPlg *Plugin
for cc.Next() {
if cc.Val() == "{" {
depth += 1
continue
} else if cc.Val() == "}" {
depth -= 1
continue
}
val := cc.Val()
args := cc.RemainingArgs()
switch depth {
case 0:
c.Servers = append(c.Servers,
&Server{
DomPorts: append([]string{val}, args...),
})
cSvr = c.Servers[len(c.Servers)-1]
case 1:
cSvr.Plugins = append(cSvr.Plugins,
&Plugin{
Name: val,
Args: args,
})
cPlg = cSvr.Plugins[len(cSvr.Plugins)-1]
case 2:
cPlg.Options = append(cPlg.Options,
&Option{
Name: val,
Args: args,
})
}
}
return &c, nil
}
func (c *Corefile) ToString() (out string) {
strs := []string{}
for _, s := range c.Servers {
strs = append(strs, s.ToString())
}
return strings.Join(strs, "\n")
}
func (s *Server) ToString() (out string) {
str := strings.Join(s.DomPorts, " ")
strs := []string{}
for _, p := range s.Plugins {
strs = append(strs, strings.Repeat(" ", indent)+p.ToString())
}
if len(strs) > 0 {
str += " {\n" + strings.Join(strs, "\n") + "\n}\n"
}
return str
}
func (p *Plugin) ToString() (out string) {
str := strings.Join(append([]string{p.Name}, p.Args...), " ")
strs := []string{}
for _, o := range p.Options {
strs = append(strs, strings.Repeat(" ", indent*2)+o.ToString())
}
if len(strs) > 0 {
str += " {\n" + strings.Join(strs, "\n") + "\n" + strings.Repeat(" ", indent*1) + "}"
}
return str
}
func (o *Option) ToString() (out string) {
str := strings.Join(append([]string{o.Name}, o.Args...), " ")
return str
}
func (s *Server) FindMatch(def []*Server) (*Server, bool) {
NextServer:
for _, sDef := range def {
for i, dp := range sDef.DomPorts {
if dp == "*" {
continue
}
if dp == "***" {
return sDef, true
}
if i >= len(s.DomPorts) || dp != s.DomPorts[i] {
continue NextServer
}
}
if len(sDef.DomPorts) != len(s.DomPorts) {
continue
}
return sDef, true
}
return nil, false
}
func (p *Plugin) FindMatch(def []*Plugin) (*Plugin, bool) {
NextPlugin:
for _, pDef := range def {
if pDef.Name != p.Name {
continue
}
for i, arg := range pDef.Args {
if arg == "*" {
continue
}
if arg == "***" {
return pDef, true
}
if i >= len(p.Args) || arg != p.Args[i] {
continue NextPlugin
}
}
if len(pDef.Args) != len(p.Args) {
continue
}
return pDef, true
}
return nil, false
}
func (o *Option) FindMatch(def []*Option) (*Option, bool) {
NextPlugin:
for _, oDef := range def {
if oDef.Name != o.Name {
continue
}
for i, arg := range oDef.Args {
if arg == "*" {
continue
}
if arg == "***" {
return oDef, true
}
if i >= len(o.Args) || arg != o.Args[i] {
continue NextPlugin
}
}
if len(oDef.Args) != len(o.Args) {
continue
}
return oDef, true
}
return nil, false
}
const indent = 4

View file

@ -1,121 +0,0 @@
package corefile
import (
"testing"
)
func TestCorefile(t *testing.T) {
startCorefile := `.:53 {
error
health
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
upstream
fallthrough in-addr.arpa ip6.arpa
}
prometheus :9153
proxy . /etc/resolv.conf
cache 30
loop
reload
loadbalance
}
.:5353 {
proxy . /etc/resolv.conf
}
`
c, err := New(startCorefile)
if err != nil {
t.Error(err)
}
got := c.ToString()
if got != startCorefile {
t.Errorf("Corefile did not match expected.\nExpected:\n%v\nGot:\n%v", startCorefile, got)
}
}
func TestServer_FindMatch(t *testing.T) {
tests := []struct {
server *Server
match bool
}{
{server: &Server{DomPorts: []string{".:53"}}, match: true},
{server: &Server{DomPorts: []string{".:54"}}, match: false},
{server: &Server{DomPorts: []string{"abc:53"}}, match: false},
{server: &Server{DomPorts: []string{"abc:53", "blah"}}, match: true},
{server: &Server{DomPorts: []string{"abc:53", "blah", "blah"}}, match: false},
{server: &Server{DomPorts: []string{"xyz:53"}}, match: true},
{server: &Server{DomPorts: []string{"xyz:53", "blah", "blah"}}, match: true},
}
def := []*Server{
{DomPorts: []string{".:53"}},
{DomPorts: []string{"abc:53", "*"}},
{DomPorts: []string{"xyz:53", "***"}},
}
for i, test := range tests {
_, match := test.server.FindMatch(def)
if match != test.match {
t.Errorf("In test #%v, expected match to be %v but got %v.", i, test.match, match)
}
}
}
func TestPlugin_FindMatch(t *testing.T) {
tests := []struct {
plugin *Plugin
match bool
}{
{plugin: &Plugin{Name: "plugin1", Args: []string{}}, match: true},
{plugin: &Plugin{Name: "plugin2", Args: []string{"1", "1.5", "2"}}, match: true},
{plugin: &Plugin{Name: "plugin3", Args: []string{"1", "2", "3", "4"}}, match: true},
{plugin: &Plugin{Name: "plugin1", Args: []string{"a"}}, match: false},
{plugin: &Plugin{Name: "plugin2", Args: []string{"1", "1.5", "b"}}, match: false},
{plugin: &Plugin{Name: "plugin3", Args: []string{"a", "2", "3", "4"}}, match: false},
{plugin: &Plugin{Name: "plugin4", Args: []string{}}, match: false},
}
def := []*Plugin{
{Name: "plugin1", Args: []string{}},
{Name: "plugin2", Args: []string{"1", "*", "2"}},
{Name: "plugin3", Args: []string{"1", "***"}},
}
for i, test := range tests {
_, match := test.plugin.FindMatch(def)
if match != test.match {
t.Errorf("In test #%v, expected match to be %v but got %v.", i, test.match, match)
}
}
}
func TestOption_FindMatch(t *testing.T) {
tests := []struct {
option *Plugin
match bool
}{
{option: &Plugin{Name: "option1", Args: []string{}}, match: true},
{option: &Plugin{Name: "option2", Args: []string{"1", "1.5", "2"}}, match: true},
{option: &Plugin{Name: "option3", Args: []string{"1", "2", "3", "4"}}, match: true},
{option: &Plugin{Name: "option1", Args: []string{"a"}}, match: false},
{option: &Plugin{Name: "option2", Args: []string{"1", "1.5", "b"}}, match: false},
{option: &Plugin{Name: "option3", Args: []string{"a", "2", "3", "4"}}, match: false},
{option: &Plugin{Name: "option4", Args: []string{}}, match: false},
}
def := []*Plugin{
{Name: "option1", Args: []string{}},
{Name: "option2", Args: []string{"1", "*", "2"}},
{Name: "option3", Args: []string{"1", "***"}},
}
for i, test := range tests {
_, match := test.option.FindMatch(def)
if match != test.match {
t.Errorf("In test #%v, expected match to be %v but got %v.", i, test.match, match)
}
}
}

View file

@ -1,3 +0,0 @@
module github.com/coredns/deployment/kubernetes/migration
go 1.12

View file

@ -1,445 +0,0 @@
package migration
// This package provides a set of functions to help handle migrations of CoreDNS Corefiles to be compatible with new
// versions of CoreDNS. The task of upgrading CoreDNS is the responsibility of a variety of Kubernetes management tools
// (e.g. kubeadm and others), and the precise behavior may be different for each one. This library abstracts some basic
// helper functions that make this easier to implement.
import (
"fmt"
"sort"
"github.com/coredns/deployment/kubernetes/migration/corefile"
)
// Deprecated returns a list of deprecation notifications affecting the guven Corefile. Notifications are returned for
// any deprecated, removed, or ignored plugins/directives present in the Corefile. Notifications are also returned for
// any new default plugins that would be added in a migration.
func Deprecated(fromCoreDNSVersion, toCoreDNSVersion, corefileStr string) ([]Notice, error) {
return getStatus(fromCoreDNSVersion, toCoreDNSVersion, corefileStr, all)
}
// Unsupported returns a list notifications of plugins/options that are not handled supported by this migration tool,
// but may still be valid in CoreDNS.
func Unsupported(fromCoreDNSVersion, toCoreDNSVersion, corefileStr string) ([]Notice, error) {
return getStatus(fromCoreDNSVersion, toCoreDNSVersion, corefileStr, unsupported)
}
func getStatus(fromCoreDNSVersion, toCoreDNSVersion, corefileStr, status string) ([]Notice, error) {
if fromCoreDNSVersion == toCoreDNSVersion {
return nil, nil
}
err := validUpMigration(fromCoreDNSVersion, toCoreDNSVersion)
if err != nil {
return nil, err
}
cf, err := corefile.New(corefileStr)
if err != nil {
return nil, err
}
notices := []Notice{}
v := fromCoreDNSVersion
for {
v = Versions[v].nextVersion
for _, s := range cf.Servers {
for _, p := range s.Plugins {
vp, present := Versions[v].plugins[p.Name]
if status == unsupported {
if present {
continue
}
notices = append(notices, Notice{Plugin: p.Name, Severity: status, Version: v})
continue
}
if !present {
continue
}
if vp.status != "" && vp.status != newdefault {
notices = append(notices, Notice{
Plugin: p.Name,
Severity: vp.status,
Version: v,
ReplacedBy: vp.replacedBy,
Additional: vp.additional,
})
continue
}
for _, o := range p.Options {
vo, present := Versions[v].plugins[p.Name].options[o.Name]
if status == unsupported {
if present {
continue
}
notices = append(notices, Notice{
Plugin: p.Name,
Option: o.Name,
Severity: status,
Version: v,
ReplacedBy: vo.replacedBy,
Additional: vo.additional,
})
continue
}
if !present {
continue
}
if vo.status != "" && vo.status != newdefault {
notices = append(notices, Notice{Plugin: p.Name, Option: o.Name, Severity: vo.status, Version: v})
continue
}
}
if status != unsupported {
CheckForNewOptions:
for name, vo := range Versions[v].plugins[p.Name].options {
if vo.status != newdefault {
continue
}
for _, o := range p.Options {
if name == o.Name {
continue CheckForNewOptions
}
}
notices = append(notices, Notice{Plugin: p.Name, Option: name, Severity: newdefault, Version: v})
}
}
}
if status != unsupported {
CheckForNewPlugins:
for name, vp := range Versions[v].plugins {
if vp.status != newdefault {
continue
}
for _, p := range s.Plugins {
if name == p.Name {
continue CheckForNewPlugins
}
}
notices = append(notices, Notice{Plugin: name, Option: "", Severity: newdefault, Version: v})
}
}
}
if v == toCoreDNSVersion {
break
}
}
return notices, nil
}
// Migrate returns the Corefile converted to toCoreDNSVersion, or an error if it cannot. This function only accepts
// a forward migration, where the destination version is => the start version.
// If deprecations is true, deprecated plugins/options will be migrated as soon as they are deprecated.
// If deprecations is false, deprecated plugins/options will be migrated only once they become removed or ignored.
func Migrate(fromCoreDNSVersion, toCoreDNSVersion, corefileStr string, deprecations bool) (string, error) {
if fromCoreDNSVersion == toCoreDNSVersion {
return corefileStr, nil
}
err := validUpMigration(fromCoreDNSVersion, toCoreDNSVersion)
if err != nil {
return "", err
}
cf, err := corefile.New(corefileStr)
if err != nil {
return "", err
}
v := fromCoreDNSVersion
for {
v = Versions[v].nextVersion
newSrvs := []*corefile.Server{}
for _, s := range cf.Servers {
newPlugs := []*corefile.Plugin{}
for _, p := range s.Plugins {
vp, present := Versions[v].plugins[p.Name]
if !present {
newPlugs = append(newPlugs, p)
continue
}
if !deprecations && vp.status == deprecated {
newPlugs = append(newPlugs, p)
continue
}
if vp.action != nil {
p, err := vp.action(p)
if err != nil {
return "", err
}
if p == nil {
// remove plugin, skip options processing
continue
}
}
newOpts := []*corefile.Option{}
for _, o := range p.Options {
vo, present := Versions[v].plugins[p.Name].options[o.Name]
if !present {
newOpts = append(newOpts, o)
continue
}
if !deprecations && vo.status == deprecated {
newOpts = append(newOpts, o)
continue
}
if vo.action == nil {
newOpts = append(newOpts, o)
continue
}
o, err := vo.action(o)
if err != nil {
return "", err
}
if o == nil {
// remove option
continue
}
newOpts = append(newOpts, o)
}
newPlug := &corefile.Plugin{
Name: p.Name,
Args: p.Args,
Options: newOpts,
}
CheckForNewOptions:
for name, vo := range Versions[v].plugins[p.Name].options {
if vo.status != newdefault {
continue
}
for _, o := range p.Options {
if name == o.Name {
continue CheckForNewOptions
}
}
newPlug, err = vo.add(newPlug)
if err != nil {
return "", err
}
}
newPlugs = append(newPlugs, newPlug)
}
newSrv := &corefile.Server{
DomPorts: s.DomPorts,
Plugins: newPlugs,
}
CheckForNewPlugins:
for name, vp := range Versions[v].plugins {
if vp.status != newdefault {
continue
}
for _, p := range s.Plugins {
if name == p.Name {
continue CheckForNewPlugins
}
}
newSrv, err = vp.add(newSrv)
if err != nil {
return "", err
}
}
newSrvs = append(newSrvs, newSrv)
}
cf = &corefile.Corefile{Servers: newSrvs}
// apply any global corefile level post processing
if Versions[v].postProcess != nil {
cf, err = Versions[v].postProcess(cf)
if err != nil {
return "", err
}
}
if v == toCoreDNSVersion {
break
}
}
return cf.ToString(), nil
}
// MigrateDown returns the Corefile converted to toCoreDNSVersion, or an error if it cannot. This function only accepts
// a downward migration, where the destination version is <= the start version.
func MigrateDown(fromCoreDNSVersion, toCoreDNSVersion, corefileStr string) (string, error) {
if fromCoreDNSVersion == toCoreDNSVersion {
return corefileStr, nil
}
err := validDownMigration(fromCoreDNSVersion, toCoreDNSVersion)
if err != nil {
return "", err
}
cf, err := corefile.New(corefileStr)
if err != nil {
return "", err
}
v := fromCoreDNSVersion
for {
newSrvs := []*corefile.Server{}
for _, s := range cf.Servers {
newPlugs := []*corefile.Plugin{}
for _, p := range s.Plugins {
vp, present := Versions[v].plugins[p.Name]
if !present {
newPlugs = append(newPlugs, p)
continue
}
if vp.downAction == nil {
newPlugs = append(newPlugs, p)
continue
}
p, err := vp.downAction(p)
if err != nil {
return "", err
}
if p == nil {
// remove plugin, skip options processing
continue
}
newOpts := []*corefile.Option{}
for _, o := range p.Options {
vo, present := Versions[v].plugins[p.Name].options[o.Name]
if !present {
newOpts = append(newOpts, o)
continue
}
if vo.downAction == nil {
newOpts = append(newOpts, o)
continue
}
o, err := vo.downAction(o)
if err != nil {
return "", err
}
if o == nil {
// remove option
continue
}
newOpts = append(newOpts, o)
}
newPlug := &corefile.Plugin{
Name: p.Name,
Args: p.Args,
Options: newOpts,
}
newPlugs = append(newPlugs, newPlug)
}
newSrv := &corefile.Server{
DomPorts: s.DomPorts,
Plugins: newPlugs,
}
newSrvs = append(newSrvs, newSrv)
}
cf = &corefile.Corefile{Servers: newSrvs}
if v == toCoreDNSVersion {
break
}
v = Versions[v].priorVersion
}
return cf.ToString(), nil
}
// Default returns true if the Corefile is the default for a given version of Kubernetes.
// Or, if k8sVersion is empty, Default returns true if the Corefile is the default for any version of Kubernetes.
func Default(k8sVersion, corefileStr string) bool {
cf, err := corefile.New(corefileStr)
if err != nil {
return false
}
NextVersion:
for _, v := range Versions {
for _, release := range v.k8sReleases {
if k8sVersion != "" && k8sVersion != release {
continue
}
}
defCf, err := corefile.New(v.defaultConf)
if err != nil {
continue
}
// check corefile against k8s release default
if len(cf.Servers) != len(defCf.Servers) {
continue NextVersion
}
for _, s := range cf.Servers {
defS, found := s.FindMatch(defCf.Servers)
if !found {
continue NextVersion
}
if len(s.Plugins) != len(defS.Plugins) {
continue NextVersion
}
for _, p := range s.Plugins {
defP, found := p.FindMatch(defS.Plugins)
if !found {
continue NextVersion
}
if len(p.Options) != len(defP.Options) {
continue NextVersion
}
for _, o := range p.Options {
_, found := o.FindMatch(defP.Options)
if !found {
continue NextVersion
}
}
}
}
return true
}
return false
}
// Released returns true if dockerImageSHA matches any released image of CoreDNS.
func Released(dockerImageSHA string) bool {
for _, v := range Versions {
if v.dockerImageSHA == dockerImageSHA {
return true
}
}
return false
}
// ValidVersions returns a list of all versions defined
func ValidVersions() []string {
var vStrs []string
for vStr := range Versions {
vStrs = append(vStrs, vStr)
}
sort.Strings(vStrs)
return vStrs
}
func validateVersion(fromCoreDNSVersion string) error {
if _, ok := Versions[fromCoreDNSVersion]; !ok {
return fmt.Errorf("start version '%v' not supported", fromCoreDNSVersion)
}
return nil
}
func validUpMigration(fromCoreDNSVersion, toCoreDNSVersion string) error {
err := validateVersion(fromCoreDNSVersion)
if err != nil {
return err
}
for next := Versions[fromCoreDNSVersion].nextVersion; next != ""; next = Versions[next].nextVersion {
if next != toCoreDNSVersion {
continue
}
return nil
}
return fmt.Errorf("cannot migrate up to '%v' from '%v'", toCoreDNSVersion, fromCoreDNSVersion)
}
func validDownMigration(fromCoreDNSVersion, toCoreDNSVersion string) error {
err := validateVersion(fromCoreDNSVersion)
if err != nil {
return err
}
for prior := Versions[fromCoreDNSVersion].priorVersion; prior != ""; prior = Versions[prior].priorVersion {
if prior != toCoreDNSVersion {
continue
}
return nil
}
return fmt.Errorf("cannot migrate down to '%v' from '%v'", toCoreDNSVersion, fromCoreDNSVersion)
}

View file

@ -1,572 +0,0 @@
package migration
import (
"testing"
)
func TestMigrate(t *testing.T) {
testCases := []struct {
name string
fromVersion string
toVersion string
deprecations bool
startCorefile string
expectedCorefile string
}{
{
name: "Remove invalid proxy option",
fromVersion: "1.1.3",
toVersion: "1.2.6",
deprecations: true,
startCorefile: `.:53 {
errors
health
kubernetes cluster.local in-addr.arpa ip6.arpa {
endpoint thing1 thing2
pods insecure
upstream
fallthrough in-addr.arpa ip6.arpa
}
prometheus :9153
proxy example.org 1.2.3.4:53 {
protocol https_google
}
cache 30
loop
reload
loadbalance
}
`,
expectedCorefile: `.:53 {
errors
health
kubernetes cluster.local in-addr.arpa ip6.arpa {
endpoint thing1 thing2
pods insecure
upstream
fallthrough in-addr.arpa ip6.arpa
}
prometheus :9153
proxy example.org 1.2.3.4:53
cache 30
loop
reload
loadbalance
}
`,
},
{
name: "Migrate from proxy to forward and handle Kubernetes deprecations",
fromVersion: "1.3.1",
toVersion: "1.5.0",
deprecations: true,
startCorefile: `.:53 {
errors
health
kubernetes cluster.local in-addr.arpa ip6.arpa {
endpoint thing1 thing2
pods insecure
upstream
fallthrough in-addr.arpa ip6.arpa
}
prometheus :9153
proxy . /etc/resolv.conf
cache 30
loop
reload
loadbalance
}
`,
expectedCorefile: `.:53 {
errors
health
kubernetes cluster.local in-addr.arpa ip6.arpa {
endpoint thing1
pods insecure
fallthrough in-addr.arpa ip6.arpa
}
prometheus :9153
forward . /etc/resolv.conf
cache 30
loop
reload
loadbalance
ready
}
`,
},
{
name: "add missing loop and ready plugins",
fromVersion: "1.1.3",
toVersion: "1.5.0",
deprecations: true,
startCorefile: `.:53 {
errors
health
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
upstream
fallthrough in-addr.arpa ip6.arpa
}
prometheus :9153
proxy . /etc/resolv.conf
cache 30
reload
loadbalance
}
`,
expectedCorefile: `.:53 {
errors
health
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
}
prometheus :9153
forward . /etc/resolv.conf
cache 30
reload
loadbalance
loop
ready
}
`,
},
{
name: "handle multiple proxy plugins",
fromVersion: "1.1.3",
toVersion: "1.5.0",
deprecations: true,
startCorefile: `.:53 {
errors
health
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
upstream
fallthrough in-addr.arpa ip6.arpa
}
prometheus :9153
proxy mystub-1.example.org 1.2.3.4
proxy mystub-2.example.org 5.6.7.8
proxy . /etc/resolv.conf
cache 30
reload
loadbalance
}
`,
expectedCorefile: `.:53 {
errors
health
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
}
prometheus :9153
forward . /etc/resolv.conf
cache 30
reload
loadbalance
loop
ready
}
mystub-1.example.org {
forward . 1.2.3.4
loop
errors
cache 30
}
mystub-2.example.org {
forward . 5.6.7.8
loop
errors
cache 30
}
`,
},
{
name: "no-op same version migration",
fromVersion: "1.3.1",
toVersion: "1.3.1",
deprecations: true,
startCorefile: `.:53 {
errors
health
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
upstream
fallthrough in-addr.arpa ip6.arpa
}
prometheus :9153
proxy . /etc/resolv.conf
cache 30
loop
reload
loadbalance
}
`,
expectedCorefile: `.:53 {
errors
health
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
upstream
fallthrough in-addr.arpa ip6.arpa
}
prometheus :9153
proxy . /etc/resolv.conf
cache 30
loop
reload
loadbalance
}
`,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
result, err := Migrate(testCase.fromVersion, testCase.toVersion, testCase.startCorefile, testCase.deprecations)
if err != nil {
t.Errorf("%v", err)
}
if result != testCase.expectedCorefile {
t.Errorf("expected != result\n%v\n%v", testCase.expectedCorefile, result)
}
})
}
}
func TestMigrateDown(t *testing.T) {
testCases := []struct {
name string
fromVersion string
toVersion string
deprecations bool
startCorefile string
expectedCorefile string
}{
{
name: "from 1.5.0 to 1.1.3",
fromVersion: "1.5.0",
toVersion: "1.1.3",
startCorefile: `.:53 {
errors
health
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
upstream
fallthrough in-addr.arpa ip6.arpa
}
prometheus :9153
forward . /etc/resolv.conf
cache 30
loop
reload
loadbalance
}
`,
expectedCorefile: `.:53 {
errors
health
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
upstream
fallthrough in-addr.arpa ip6.arpa
}
prometheus :9153
forward . /etc/resolv.conf
cache 30
reload
loadbalance
}
`,
},
{
name: "no-op same version migration",
fromVersion: "1.3.1",
toVersion: "1.3.1",
deprecations: true,
startCorefile: `.:53 {
errors
health
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
upstream
fallthrough in-addr.arpa ip6.arpa
}
prometheus :9153
proxy . /etc/resolv.conf
cache 30
loop
reload
loadbalance
}
`,
expectedCorefile: `.:53 {
errors
health
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
upstream
fallthrough in-addr.arpa ip6.arpa
}
prometheus :9153
proxy . /etc/resolv.conf
cache 30
loop
reload
loadbalance
}
`,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
result, err := MigrateDown(testCase.fromVersion, testCase.toVersion, testCase.startCorefile)
if err != nil {
t.Errorf("%v", err)
}
if result != testCase.expectedCorefile {
t.Errorf("expected != result:\n%v\n%v", testCase.expectedCorefile, result)
}
})
}
}
func TestDeprecated(t *testing.T) {
startCorefile := `.:53 {
errors
health
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
upstream
fallthrough in-addr.arpa ip6.arpa
}
prometheus :9153
proxy . /etc/resolv.conf
cache 30
loop
reload
loadbalance
}
`
expected := []Notice{
{Plugin: "kubernetes", Option: "upstream", Severity: deprecated, Version: "1.4.0"},
{Plugin: "proxy", Severity: deprecated, ReplacedBy: "forward", Version: "1.4.0"},
{Option: "upstream", Plugin: "kubernetes", Severity: ignored, Version: "1.5.0"},
{Plugin: "proxy", Severity: removed, ReplacedBy: "forward", Version: "1.5.0"},
{Plugin: "ready", Severity: newdefault, Version: "1.5.0"},
}
result, err := Deprecated("1.2.0", "1.5.0", startCorefile)
if err != nil {
t.Fatal(err)
}
if len(result) != len(expected) {
t.Fatalf("expected to find %v notifications; got %v", len(expected), len(result))
}
for i, dep := range expected {
if result[i].ToString() != dep.ToString() {
t.Errorf("expected to get '%v'; got '%v'", dep.ToString(), result[i].ToString())
}
}
result, err = Deprecated("1.3.1", "1.3.1", startCorefile)
if err != nil {
t.Fatal(err)
}
expected = []Notice{}
if len(result) != len(expected) {
t.Fatalf("expected to find %v notifications in no-op upgrade; got %v", len(expected), len(result))
}
}
func TestUnsupported(t *testing.T) {
startCorefile := `.:53 {
errors
health
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
upstream
fallthrough in-addr.arpa ip6.arpa
}
route53 example.org.:Z1Z2Z3Z4DZ5Z6Z7
prometheus :9153
proxy . /etc/resolv.conf
cache 30
loop
reload
loadbalance
}
`
expected := []Notice{
{Plugin: "route53", Severity: unsupported, Version: "1.4.0"},
{Plugin: "route53", Severity: unsupported, Version: "1.5.0"},
}
result, err := Unsupported("1.3.1", "1.5.0", startCorefile)
if err != nil {
t.Fatal(err)
}
if len(result) != len(expected) {
t.Fatalf("expected to find %v deprecations; got %v", len(expected), len(result))
}
for i, dep := range expected {
if result[i].ToString() != dep.ToString() {
t.Errorf("expected to get '%v'; got '%v'", dep.ToString(), result[i].ToString())
}
}
}
func TestDefault(t *testing.T) {
defaultCorefiles := []string{`.:53 {
errors
health
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
upstream
fallthrough in-addr.arpa ip6.arpa
}
prometheus :9153
forward . /etc/resolv.conf
cache 30
loop
reload
loadbalance
}
`,
`.:53 {
errors
health
kubernetes myzone.org in-addr.arpa ip6.arpa {
pods insecure
upstream
fallthrough in-addr.arpa ip6.arpa
}
prometheus :9153
forward . /etc/resolv.conf
cache 30
loop
reload
loadbalance
}
`}
nonDefaultCorefiles := []string{`.:53 {
errors
health
rewrite name suffix myzone.org cluster.local
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
upstream
fallthrough in-addr.arpa ip6.arpa
}
prometheus :9153
forward . /etc/resolv.conf
cache 30
loop
reload
loadbalance
}
`,
`.:53 {
errors
health
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
upstream
fallthrough in-addr.arpa ip6.arpa
}
prometheus :9153
forward . /etc/resolv.conf
cache 30
loop
reload
loadbalance
}
stubzone.org:53 {
forward . 1.2.3.4
}
`}
for _, d := range defaultCorefiles {
if !Default("", d) {
t.Errorf("expected config to be identified as a default: %v", d)
}
}
for _, d := range nonDefaultCorefiles {
if Default("", d) {
t.Errorf("expected config to NOT be identified as a default: %v", d)
}
}
}
func TestValidUpMigration(t *testing.T) {
testCases := []struct {
from string
to string
shouldErr bool
}{
{"1.3.1", "1.3.1", true},
{"1.3.1", "1.5.0", false},
{"1.5.0", "1.3.1", true},
{"banana", "1.5.0", true},
{"1.3.1", "apple", true},
{"banana", "apple", true},
}
for _, tc := range testCases {
err := validUpMigration(tc.from, tc.to)
if !tc.shouldErr && err != nil {
t.Errorf("expected '%v' to '%v' to be valid versions.", tc.from, tc.to)
}
if tc.shouldErr && err == nil {
t.Errorf("expected '%v' to '%v' to be invalid versions.", tc.from, tc.to)
}
}
}
func TestValidDownMigration(t *testing.T) {
testCases := []struct {
from string
to string
shouldErr bool
}{
{"1.3.1", "1.3.1", true},
{"1.3.1", "1.5.0", true},
{"1.5.0", "1.3.1", false},
{"banana", "1.5.0", true},
{"1.3.1", "apple", true},
{"banana", "apple", true},
}
for _, tc := range testCases {
err := validDownMigration(tc.from, tc.to)
if !tc.shouldErr && err != nil {
t.Errorf("expected '%v' to '%v' to be valid versions.", tc.from, tc.to)
}
if tc.shouldErr && err == nil {
t.Errorf("expected '%v' to '%v' to be invalid versions.", tc.from, tc.to)
}
}
}

View file

@ -1,48 +0,0 @@
package migration
import "fmt"
// Notice is a migration warning
type Notice struct {
Plugin string
Option string
Severity string // 'deprecated', 'removed', or 'unsupported'
ReplacedBy string
Additional string
Version string
}
func (n *Notice) ToString() string {
s := ""
if n.Option == "" {
s += fmt.Sprintf(`Plugin "%v" `, n.Plugin)
} else {
s += fmt.Sprintf(`Option "%v" in plugin "%v" `, n.Option, n.Plugin)
}
if n.Severity == unsupported {
s += "is unsupported by this migration tool in " + n.Version + "."
} else if n.Severity == newdefault {
s += "is added as a default in " + n.Version + "."
} else {
s += "is " + n.Severity + " in " + n.Version + "."
}
if n.ReplacedBy != "" {
s += fmt.Sprintf(` It is replaced by "%v".`, n.ReplacedBy)
}
if n.Additional != "" {
s += " " + n.Additional
}
return s
}
const (
// The following statuses are used to indicate the state of support/deprecation in a given release.
deprecated = "deprecated" // deprecated, but still completely functional
ignored = "ignored" // if included in the corefile, it will be ignored by CoreDNS
removed = "removed" // completely removed from CoreDNS, and would cause CoreDNS to exit if present in the Corefile
newdefault = "newdefault" // added to the default corefile. CoreDNS may not function properly if it is not present in the corefile.
unsupported = "unsupported" // the plugin/option is not supported by the migration tool
// The following statuses are used for selecting/filtering notifications
all = "all" // show all statuses
)

File diff suppressed because it is too large Load diff