Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix ambiguous networks #1831

Merged
merged 9 commits into from
Sep 14, 2020
101 changes: 94 additions & 7 deletions pkg/cluster/internal/providers/docker/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,18 @@ limitations under the License.
package docker

import (
"bytes"
"crypto/sha1"
"encoding/binary"
"errors"
"encoding/json"
"io"
"net"
"regexp"
"sort"
"strings"
"time"

"sigs.k8s.io/kind/pkg/errors"
"sigs.k8s.io/kind/pkg/exec"
)

Expand All @@ -42,13 +47,15 @@ const fixedNetworkName = "kind"

// ensureNetwork checks if docker network by name exists, if not it creates it
func ensureNetwork(name string) error {
// TODO: the network might already exist and not have ipv6 ... :|
// discussion: https://github.com/kubernetes-sigs/kind/pull/1508#discussion_r414594198
exists, err := checkIfNetworkExists(name)
// check if network exists already and remove any duplicate networks
exists, err := removeDuplicateNetworks(name)
BenTheElder marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return err
}

// network already exists, we're good
// TODO: the network might already exist and not have ipv6 ... :|
// discussion: https://github.com/kubernetes-sigs/kind/pull/1508#discussion_r414594198
if exists {
return nil
}
Expand All @@ -57,7 +64,7 @@ func ensureNetwork(name string) error {
// obtained from the ULA fc00::/8 range
// Make N attempts with "probing" in case we happen to collide
subnet := generateULASubnetFromName(name, 0)
err = createNetwork(name, subnet)
err = createNetworkNoDuplicates(name, subnet)
if err == nil {
// Success!
return nil
Expand All @@ -69,7 +76,7 @@ func ensureNetwork(name string) error {
// If it is, make more attempts below
if isIPv6UnavailableError(err) {
// only one attempt, IPAM is automatic in ipv4 only
return createNetwork(name, "")
return createNetworkNoDuplicates(name, "")
BenTheElder marked this conversation as resolved.
Show resolved Hide resolved
} else if !isPoolOverlapError(err) {
// unknown error ...
return err
Expand All @@ -79,7 +86,7 @@ func ensureNetwork(name string) error {
const maxAttempts = 5
for attempt := int32(1); attempt < maxAttempts; attempt++ {
subnet := generateULASubnetFromName(name, attempt)
err = createNetwork(name, subnet)
err = createNetworkNoDuplicates(name, subnet)
if err == nil {
// success!
return nil
Expand All @@ -91,6 +98,27 @@ func ensureNetwork(name string) error {
return errors.New("exhausted attempts trying to find a non-overlapping subnet")
}

func createNetworkNoDuplicates(name, ipv6Subnet string) error {
if err := createNetwork(name, ipv6Subnet); err != nil && !isNetworkAlreadyExistsError(err) {
return err
}
_, err := removeDuplicateNetworks(name)
return err
}

func removeDuplicateNetworks(name string) (bool, error) {
networks, err := sortedNetworksWithName(name)
BenTheElder marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return false, err
}
if len(networks) > 1 {
if err := deleteNetworks(networks[1:]...); err != nil {
return false, err
}
}
return len(networks) > 0, nil
}

func createNetwork(name, ipv6Subnet string) error {
if ipv6Subnet == "" {
return exec.Command("docker", "network", "create", "-d=bridge",
Expand All @@ -102,6 +130,55 @@ func createNetwork(name, ipv6Subnet string) error {
"--ipv6", "--subnet", ipv6Subnet, name).Run()
}

func sortedNetworksWithName(name string) ([]string, error) {
// list all networks by this name
out, err := exec.Output(exec.Command(
"docker", "network", "ls",
"--filter=name=^"+regexp.QuoteMeta(name)+"$",
"--format={{json .}}",
))
if err != nil {
return nil, err
}

// parse
type networkLSEntry struct {
CreatedAt goDefaultTime `json:"CreatedAt"`
ID string `json:"ID"`
}

networks := []networkLSEntry{}
decoder := json.NewDecoder(bytes.NewReader(out))
for {
var network networkLSEntry
err := decoder.Decode(&network)
if err == io.EOF {
break
} else if err != nil {
return nil, errors.Wrap(err, "failed to decode networks list")
}
networks = append(networks, network)
}

// deterministically sort networks
// NOTE: THIS PART IS IMPORTANT!
// TODO(fixme): we should be sorting on active usage first!
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this TODO is the one thing keeping this WIP.
I will come back and rework this later. the overall code will be more or less the same otherwise.

// unfortunately this is only available in docker network inspect
sort.Slice(networks, func(i, j int) bool {
if time.Time(networks[i].CreatedAt).Before(time.Time(networks[j].CreatedAt)) {
return true
}
return networks[i].ID < networks[j].ID
})

// return network IDs
ids := make([]string, 0, len(networks))
for i := range networks {
ids = append(ids, networks[i].ID)
}
return ids, nil
}

func checkIfNetworkExists(name string) (bool, error) {
out, err := exec.Output(exec.Command(
"docker", "network", "ls",
Expand All @@ -121,6 +198,16 @@ func isPoolOverlapError(err error) bool {
return rerr != nil && strings.HasPrefix(string(rerr.Output), "Error response from daemon: Pool overlaps with other one on this address space")
}

func isNetworkAlreadyExistsError(err error) bool {
rerr := exec.RunErrorForError(err)
return rerr != nil && strings.HasPrefix(string(rerr.Output), "Error response from daemon: network with name") && strings.HasSuffix(string(rerr.Output), "already exists")
}

func deleteNetworks(networks ...string) error {
println("DELETING NETWORKS")
return exec.Command("docker", append([]string{"network", "rm"}, networks...)...).Run()
}

// generateULASubnetFromName generate an IPv6 subnet based on the
// name and Nth probing attempt
func generateULASubnetFromName(name string, attempt int32) string {
Expand Down
42 changes: 42 additions & 0 deletions pkg/cluster/internal/providers/docker/parse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
Copyright 2020 The Kubernetes Authors.

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 docker

import (
"time"
)

/*
Docker CLI outputs time.Time objects with the default string format
This is going to be a huge pain if go actually makes good on their threat
that this format is not stable

see: https://golang.org/pkg/time/#Time.String
*/

const goDefaultTimeFormat = "2006-01-02 15:04:05.999999999 -0700 MST"

type goDefaultTime time.Time

func (g *goDefaultTime) UnmarshalJSON(p []byte) error {
t, err := time.Parse(`"`+goDefaultTimeFormat+`"`, string(p))
if err != nil {
return err
}
*g = goDefaultTime(t)
return nil
}