asteris-llc/converge

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

Summary

Maintainability
A
40 mins
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 volume

import (
    "fmt"
    "sort"
    "strings"

    "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 Volume
type State string

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

    // StateAbsent indicates the volume should be absent
    StateAbsent State = "absent"
)

// Volume is responsible for managing docker volumes
type Volume struct {
    client docker.VolumeClient

    // volume name
    Name string `export:"name"`

    // volume labels
    Labels map[string]string `export:"labels"`

    // driver the volume is configured to use
    Driver string `export:"driver"`

    // driver-specific options
    Options map[string]string `export:"options"`

    // volume state
    State State `export:"state"`

    // reflects whether or not the force option was configured
    Force bool `export:"force"`
}

// Check system for docker volume
func (v *Volume) Check(context.Context, resource.Renderer) (resource.TaskStatus, error) {
    status := resource.NewStatus()
    vol, err := v.client.FindVolume(v.Name)

    if err != nil {
        status.Level = resource.StatusFatal
        return status, err
    }

    status.AddDifference(v.Name, string(volumeState(vol)), string(v.State), "")

    if v.State == StatePresent && vol != nil && v.Force {
        expectedLabels := mapToString(v.Labels)
        actualLabels := mapToString(vol.Labels)
        status.AddDifference("labels", actualLabels, expectedLabels, "")
        status.AddDifference("driver", vol.Driver, v.Driver, "local")
        // we cannot detect difference in Options because the Docker API does not
        // return that information:
        // https://docs.docker.com/engine/reference/api/docker_remote_api_v1.24/#/inspect-a-volume
    }

    status.RaiseLevelForDiffs()

    return status, nil
}

// Apply ensures the volume matches the desired state
func (v *Volume) Apply(context.Context) (resource.TaskStatus, error) {
    status := resource.NewStatus()

    var (
        vol *dc.Volume
        err error
    )

    vol, err = v.client.FindVolume(v.Name)

    if err != nil {
        status.Level = resource.StatusFatal
        return status, err
    }

    if v.State == StatePresent {
        if vol != nil {
            if !v.Force {
                return status, nil
            }

            err = v.client.RemoveVolume(v.Name)
            if err != nil {
                status.Level = resource.StatusFatal
                return status, err
            }
            status.AddMessage(fmt.Sprintf("removed volume %s", v.Name))
        }

        opts := dc.CreateVolumeOptions{
            Name:       v.Name,
            Driver:     v.Driver,
            DriverOpts: v.Options,
            Labels:     v.Labels,
        }

        vol, err = v.client.CreateVolume(opts)
        if err != nil {
            status.Level = resource.StatusFatal
            return status, err
        }
        status.AddMessage(fmt.Sprintf("created volume %s", v.Name))
        status.RaiseLevel(resource.StatusWillChange)
    } else {
        if vol != nil {
            err = v.client.RemoveVolume(v.Name)
            if err != nil {
                status.Level = resource.StatusFatal
                return status, err
            }
            status.AddMessage(fmt.Sprintf("removed volume %s", v.Name))
            status.RaiseLevel(resource.StatusWillChange)
        }
    }

    status.AddDifference(v.Name, string(volumeState(vol)), string(v.State), "")
    return status, nil
}

// SetClient injects a docker api client
func (v *Volume) SetClient(client docker.VolumeClient) {
    v.client = client
}

func volumeState(vol *dc.Volume) State {
    if vol != nil {
        return StatePresent
    }
    return StateAbsent
}

func mapToString(m map[string]string) string {
    if len(m) == 0 {
        return ""
    }
    strs := make([]string, len(m))
    i := 0
    for k, v := range m {
        strs[i] = fmt.Sprintf("%s=%s", k, v)
        i++
    }
    sort.Strings(strs)
    return strings.Join(strs, ", ")
}