asteris-llc/converge

View on GitHub
resource/docker/network/network.go

Summary

Maintainability
A
1 hr
Test Coverage
// Copyright © 2016 Asteris, 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.

// +build !solaris

package network

import (
    "fmt"
    "sort"
    "strconv"
    "strings"

    "github.com/asteris-llc/converge/helpers/transform"
    "github.com/asteris-llc/converge/resource"
    "github.com/asteris-llc/converge/resource/docker"
    dc "github.com/fsouza/go-dockerclient"
    "golang.org/x/net/context"
)

// State type for Network
type State string

const (
    // StatePresent indicates the network should be present
    StatePresent State = "present"

    // StateAbsent indicates the network should be absent
    StateAbsent State = "absent"

    // DefaultDriver is the default network driver
    DefaultDriver = "bridge"

    // DefaultIPAMDriver is the default IPAM driver
    DefaultIPAMDriver = "default"
)

// Network is responsible for managing docker networks
type Network struct {
    client docker.NetworkClient

    // name of the network
    Name string `export:"name"`

    // network drive configured in the hcl
    Driver string `export:"driver"`

    // labels set on the network
    Labels map[string]string `export:"labels"`

    // driver-specific options that have been configured
    Options map[string]interface{} `export:"options"`

    // docker client IPAM options
    IPAM dc.IPAMOptions `export:"ipam"`

    // restricted to internal networking
    Internal bool `export:"internal"`

    // uses ipv6
    IPv6 bool `export:"ipv6"`

    // the configured state
    State State `export:"state"`

    // true if 'force' was specified in the hcl
    Force bool `export:"force"`
}

// Check system for docker network
func (n *Network) Check(context.Context, resource.Renderer) (resource.TaskStatus, error) {
    status := resource.NewStatus()
    nw, err := n.client.FindNetwork(n.Name)

    if err != nil {
        status.RaiseLevel(resource.StatusFatal)
        return status, err
    }

    status.AddDifference(n.Name, string(networkState(nw)), string(n.State), "")

    if n.State == StatePresent {
        if nw != nil && n.Force {
            n.diffNetwork(nw, status)
        }

        invalidGWs, err := n.findConflicting(nw, status)
        if err != nil {
            status.RaiseLevel(resource.StatusFatal)
            return status, err
        }

        if len(invalidGWs) > 0 {
            status.RaiseLevel(resource.StatusCantChange)
            return status, fmt.Errorf("%s: gateway(s) are already in use", strings.Join(invalidGWs, ", "))

        }
    }

    status.RaiseLevelForDiffs()

    return status, nil
}

// Apply ensures the network matches the desired state
func (n *Network) Apply(context.Context) (resource.TaskStatus, error) {
    status := resource.NewStatus()

    var (
        nw  *dc.Network
        err error
    )

    nw, err = n.client.FindNetwork(n.Name)
    if err != nil {
        status.RaiseLevel(resource.StatusFatal)
        return status, err
    }

    if n.State == StatePresent {
        if nw != nil {
            if !n.Force {
                return status, nil
            }

            err = n.client.RemoveNetwork(n.Name)
            if err != nil {
                status.RaiseLevel(resource.StatusFatal)
                return status, err
            }
            status.AddMessage(fmt.Sprintf("removed network %s", n.Name))
        }

        opts := dc.CreateNetworkOptions{
            Name:       n.Name,
            Driver:     n.Driver,
            Labels:     n.Labels,
            Options:    n.Options,
            IPAM:       n.IPAM,
            Internal:   n.Internal,
            EnableIPv6: n.IPv6,
        }

        nw, err = n.client.CreateNetwork(opts)
        if err != nil {
            status.RaiseLevel(resource.StatusFatal)
            return status, err
        }
        status.AddMessage(fmt.Sprintf("created network %s", n.Name))
        status.RaiseLevel(resource.StatusWillChange)
    } else {
        if nw != nil {
            err = n.client.RemoveNetwork(n.Name)
            if err != nil {
                status.RaiseLevel(resource.StatusFatal)
                return status, err
            }
            status.AddMessage(fmt.Sprintf("removed network %s", n.Name))
            status.RaiseLevel(resource.StatusWillChange)
        }
    }

    status.AddDifference(n.Name, string(networkState(nw)), string(n.State), "")
    return status, nil
}

// SetClient injects a docker api client
func (n *Network) SetClient(client docker.NetworkClient) {
    n.client = client
}

func (n *Network) diffNetwork(nw *dc.Network, status *resource.Status) {
    status.AddDifference("labels", mapCompareStr(nw.Labels), mapCompareStr(n.Labels), "")
    status.AddDifference("driver", nw.Driver, n.Driver, DefaultDriver)
    status.AddDifference("options", mapCompareStr(nw.Options), mapCompareStr(toStrMap(n.Options)), "")
    status.AddDifference("internal", strconv.FormatBool(nw.Internal), strconv.FormatBool(n.Internal), "false")
    status.AddDifference("ipv6", strconv.FormatBool(nw.EnableIPv6), strconv.FormatBool(n.IPv6), "false")
    status.AddDifference("ipam_driver", nw.IPAM.Driver, n.IPAM.Driver, DefaultIPAMDriver)

    // we cannot reliably detect a diff of the ipam config if the desired ipam
    // config is the default ([]) but the actual ipam config has a single
    // customized entry
    if len(n.IPAM.Config) > 0 || len(nw.IPAM.Config) > 1 {
        actualIPAMConfigs := IPAMConfigs(nw.IPAM.Config)
        sort.Sort(actualIPAMConfigs)
        expectedIPAMConfigs := IPAMConfigs(n.IPAM.Config)
        sort.Sort(expectedIPAMConfigs)
        status.AddDifference("ipam_config", actualIPAMConfigs.String(), expectedIPAMConfigs.String(), "")
    }
}

// findConflicting can validate whether the network can be created on the docker
// host. use sparingly as behavior can vary across different docker network
// plugins. in most cases, we can rely on the docker api to return errors during
// apply.  It returns a list of networks that would conflict with the network
// and prevent it from being created.
func (n *Network) findConflicting(nw *dc.Network, status *resource.Status) ([]string, error) {
    if len(n.IPAM.Config) > 0 {
        inUse, err := n.gatewaysInUse(nw, status)
        return inUse, err
    }
    return nil, nil
}

func (n *Network) gatewaysInUse(nw *dc.Network, status *resource.Status) ([]string, error) {
    var gateways []string
    var usedGWs []string
    networks, err := n.client.ListNetworks()
    if err != nil {
        return nil, err
    }

    for _, network := range networks {
        if nw == nil || network.ID != nw.ID {
            for _, ipamConfig := range network.IPAM.Config {
                if ipamConfig.Gateway != "" {
                    gateways = append(gateways, ipamConfig.Gateway)
                }
            }
        }
    }

    for _, ipamConfig := range n.IPAM.Config {
        for _, gateway := range gateways {
            if strings.EqualFold(ipamConfig.Gateway, gateway) {
                usedGWs = append(usedGWs, gateway)
                status.AddMessage(fmt.Sprintf("gateway %s already in use", gateway))
            }
        }
    }

    return usedGWs, nil
}

func networkState(nw *dc.Network) State {
    if nw != nil {
        return StatePresent
    }
    return StateAbsent
}

func toStrMap(m map[string]interface{}) map[string]string {
    strmap := make(map[string]string)
    for k, v := range m {
        strmap[k] = v.(string)
    }
    return strmap
}

func mapCompareStr(m map[string]string) string {
    pairs := transform.StringsMapToStringSlice(
        m,
        func(k, v string) string {
            return fmt.Sprintf("%s=%s", k, v)
        },
    )
    sort.Strings(pairs)
    return strings.Join(pairs, ", ")
}

// IPAMConfigs is a slice of dc.IPAMConfig
type IPAMConfigs []dc.IPAMConfig

// Len implements the sort interface for IPAMConfigs
func (ic IPAMConfigs) Len() int { return len(ic) }

// Swap implements the sort interface for IPAMConfigs
func (ic IPAMConfigs) Swap(i, j int) { ic[i], ic[j] = ic[j], ic[i] }

// Less implements the sort interface for IPAMConfigs
func (ic IPAMConfigs) Less(i, j int) bool { return ic[i].Subnet < ic[j].Subnet }

// IPAMConfigString returns a string representation of the IPAMConfigs slice
func (ic IPAMConfigs) String() string {
    var configStrs []string
    for _, c := range ic {
        configStrs = append(configStrs, ipamConfigString(c))
    }
    return strings.Join(configStrs, "\n")
}

func ipamConfigString(c dc.IPAMConfig) string {
    var parts []string

    if c.Subnet != "" {
        parts = append(parts, fmt.Sprintf("subnet: %s", c.Subnet))
    }

    if c.Gateway != "" {
        parts = append(parts, fmt.Sprintf("gateway: %s", c.Gateway))
    }

    if c.IPRange != "" {
        parts = append(parts, fmt.Sprintf("ip_range: %s", c.IPRange))
    }

    if len(c.AuxAddress) > 0 {
        parts = append(parts, fmt.Sprintf("aux_addresses: [%s]", mapCompareStr(c.AuxAddress)))
    }

    return strings.Join(parts, ", ")
}