terraform-provider-docker/docker/resource_docker_service_funcs.go
Manuel Vogel dc824c1030
Feat/swarm 4 new resources (#40)
Adds docker swarm features to the provider for the Docker Engine 17.09.1 and API Version 1.32. 

The spec is close to the API. By default, the swarm services are fire and forget. A converging config implements the features of the docker cli to ensure a service and all its replicas are up and running. Furthermore, service can have configs, secrets, networks, mounts and be added to a network.
2018-05-16 18:00:04 +02:00

1379 lines
44 KiB
Go

package docker
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"strconv"
"strings"
"time"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/swarm"
dc "github.com/fsouza/go-dockerclient"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
)
type convergeConfig struct {
timeout time.Duration
timeoutRaw string
delay time.Duration
}
/////////////////
// TF CRUD funcs
/////////////////
func resourceDockerServiceExists(d *schema.ResourceData, meta interface{}) (bool, error) {
client := meta.(*ProviderConfig).DockerClient
if client == nil {
return false, nil
}
apiService, err := fetchDockerService(d.Id(), d.Get("name").(string), client)
if err != nil {
return false, err
}
if apiService == nil {
return false, nil
}
return true, nil
}
func resourceDockerServiceCreate(d *schema.ResourceData, meta interface{}) error {
var err error
client := meta.(*ProviderConfig).DockerClient
serviceSpec, err := createServiceSpec(d)
if err != nil {
return err
}
createOpts := dc.CreateServiceOptions{
ServiceSpec: serviceSpec,
}
if v, ok := d.GetOk("auth"); ok {
createOpts.Auth = authToServiceAuth(v.(map[string]interface{}))
} else {
createOpts.Auth = fromRegistryAuth(d.Get("task_spec.0.container_spec.0.image").(string), meta.(*ProviderConfig).AuthConfigs.Configs)
}
service, err := client.CreateService(createOpts)
if err != nil {
return err
}
if v, ok := d.GetOk("converge_config"); ok {
convergeConfig := createConvergeConfig(v.([]interface{}))
log.Printf("[INFO] Waiting for Service '%s' to be created with timeout: %v", service.ID, convergeConfig.timeoutRaw)
timeout, _ := time.ParseDuration(convergeConfig.timeoutRaw)
stateConf := &resource.StateChangeConf{
Pending: serviceCreatePendingStates,
Target: []string{"running", "complete"},
Refresh: resourceDockerServiceCreateRefreshFunc(service.ID, meta),
Timeout: timeout,
MinTimeout: 5 * time.Second,
Delay: convergeConfig.delay,
}
// Wait, catching any errors
_, err := stateConf.WaitForState()
if err != nil {
// the service will be deleted in case it cannot be converged
if deleteErr := deleteService(service.ID, d, client); deleteErr != nil {
return deleteErr
}
if strings.Contains(err.Error(), "timeout while waiting for state") {
return &DidNotConvergeError{ServiceID: service.ID, Timeout: convergeConfig.timeout}
}
return err
}
}
return resourceDockerServiceRead(d, meta)
}
func resourceDockerServiceRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ProviderConfig).DockerClient
apiService, err := fetchDockerService(d.Id(), d.Get("name").(string), client)
if err != nil {
return err
}
if apiService == nil {
d.SetId("")
return nil
}
service, err := client.InspectService(apiService.ID)
if err != nil {
return fmt.Errorf("Error inspecting service %s: %s", apiService.ID, err)
}
jsonObj, _ := json.Marshal(service)
log.Printf("[DEBUG] Docker service inspect: %s", jsonObj)
d.SetId(service.ID)
d.Set("name", service.Spec.Name)
d.Set("labels", service.Spec.Labels)
if err = d.Set("task_spec", flattenTaskSpec(service.Spec.TaskTemplate)); err != nil {
log.Printf("[WARN] failed to set task spec from API: %s", err)
}
if err = d.Set("mode", flattenServiceMode(service.Spec.Mode)); err != nil {
log.Printf("[WARN] failed to set mode from API: %s", err)
}
if err := d.Set("update_config", flattenServiceUpdateOrRollbackConfig(service.Spec.UpdateConfig)); err != nil {
log.Printf("[WARN] failed to set update_config from API: %s", err)
}
if err = d.Set("rollback_config", flattenServiceUpdateOrRollbackConfig(service.Spec.RollbackConfig)); err != nil {
log.Printf("[WARN] failed to set rollback_config from API: %s", err)
}
if err = d.Set("endpoint_spec", flattenServiceEndpointSpec(service.Endpoint.Spec)); err != nil {
log.Printf("[WARN] failed to set endpoint spec from API: %s", err)
}
return nil
}
func resourceDockerServiceUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ProviderConfig).DockerClient
service, err := client.InspectService(d.Id())
if err != nil {
return err
}
serviceSpec, err := createServiceSpec(d)
if err != nil {
return err
}
updateOpts := dc.UpdateServiceOptions{
ServiceSpec: serviceSpec,
Version: service.Version.Index,
}
if v, ok := d.GetOk("auth"); ok {
updateOpts.Auth = authToServiceAuth(v.(map[string]interface{}))
} else {
updateOpts.Auth = fromRegistryAuth(d.Get("task_spec.0.container_spec.0.image").(string), meta.(*ProviderConfig).AuthConfigs.Configs)
}
if err = client.UpdateService(d.Id(), updateOpts); err != nil {
return err
}
if v, ok := d.GetOk("converge_config"); ok {
convergeConfig := createConvergeConfig(v.([]interface{}))
log.Printf("[INFO] Waiting for Service '%s' to be updated with timeout: %v", service.ID, convergeConfig.timeoutRaw)
timeout, _ := time.ParseDuration(convergeConfig.timeoutRaw)
stateConf := &resource.StateChangeConf{
Pending: serviceUpdatePendingStates,
Target: []string{"completed"},
Refresh: resourceDockerServiceUpdateRefreshFunc(service.ID, meta),
Timeout: timeout,
MinTimeout: 5 * time.Second,
Delay: 7 * time.Second,
}
// Wait, catching any errors
state, err := stateConf.WaitForState()
log.Printf("[INFO] ###### State awaited: %v with error: %v", state, err)
if err != nil {
if strings.Contains(err.Error(), "timeout while waiting for state") {
log.Printf("######## did not converge error...")
return &DidNotConvergeError{ServiceID: service.ID, Timeout: convergeConfig.timeout}
}
log.Printf("######## OTHER converge error...")
return err
}
}
return resourceDockerServiceRead(d, meta)
}
func resourceDockerServiceDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ProviderConfig).DockerClient
if err := deleteService(d.Id(), d, client); err != nil {
return err
}
d.SetId("")
return nil
}
/////////////////
// Helpers
/////////////////
// fetchDockerService fetches a service by its name or id
func fetchDockerService(ID string, name string, client *dc.Client) (*swarm.Service, error) {
apiServices, err := client.ListServices(dc.ListServicesOptions{})
if err != nil {
return nil, fmt.Errorf("Error fetching service information from Docker: %s", err)
}
for _, apiService := range apiServices {
if apiService.ID == ID || apiService.Spec.Name == name {
return &apiService, nil
}
}
return nil, nil
}
// deleteService deletes the service with the given id
func deleteService(serviceID string, d *schema.ResourceData, client *dc.Client) error {
// get containerIDs of the running service because they do not exist after the service is deleted
serviceContainerIds := make([]string, 0)
if _, ok := d.GetOk("task_spec.0.container_spec.0.stop_grace_period"); ok {
filter := make(map[string][]string)
filter["service"] = []string{d.Get("name").(string)}
tasks, err := client.ListTasks(dc.ListTasksOptions{
Filters: filter,
})
if err != nil {
return err
}
for _, t := range tasks {
task, _ := client.InspectTask(t.ID)
log.Printf("[INFO] Found container ['%s'] for destroying: '%s'", task.Status.State, task.Status.ContainerStatus.ContainerID)
if strings.TrimSpace(task.Status.ContainerStatus.ContainerID) != "" && task.Status.State != swarm.TaskStateShutdown {
serviceContainerIds = append(serviceContainerIds, task.Status.ContainerStatus.ContainerID)
}
}
}
// delete the service
log.Printf("[INFO] Deleting service: '%s'", serviceID)
removeOpts := dc.RemoveServiceOptions{
ID: serviceID,
}
if err := client.RemoveService(removeOpts); err != nil {
if _, ok := err.(*dc.NoSuchService); ok {
log.Printf("[WARN] Service (%s) not found, removing from state", serviceID)
d.SetId("")
return nil
}
return fmt.Errorf("Error deleting service %s: %s", serviceID, err)
}
// destroy each container after a grace period if specified
if v, ok := d.GetOk("task_spec.0.container_spec.0.stop_grace_period"); ok {
for _, containerID := range serviceContainerIds {
destroyGraceSeconds, _ := time.ParseDuration(v.(string))
log.Printf("[INFO] Waiting for container: '%s' to exit: max %v", containerID, destroyGraceSeconds)
ctx, cancel := context.WithTimeout(context.Background(), destroyGraceSeconds)
defer cancel()
exitCode, _ := client.WaitContainerWithContext(containerID, ctx)
log.Printf("[INFO] Container exited with code [%v]: '%s'", exitCode, containerID)
removeOpts := dc.RemoveContainerOptions{
ID: containerID,
RemoveVolumes: true,
Force: true,
}
log.Printf("[INFO] Removing container: '%s'", containerID)
if err := client.RemoveContainer(removeOpts); err != nil {
if !(strings.Contains(err.Error(), "No such container") || strings.Contains(err.Error(), "is already in progress")) {
return fmt.Errorf("Error deleting container %s: %s", containerID, err)
}
}
}
}
return nil
}
//////// Convergers
// DidNotConvergeError is the error returned when a the service does not converge in
// the defined time
type DidNotConvergeError struct {
ServiceID string
Timeout time.Duration
Err error
}
// Error the custom error if a service does not converge
func (err *DidNotConvergeError) Error() string {
if err.Err != nil {
return err.Err.Error()
}
return "Service with ID (" + err.ServiceID + ") did not converge after " + err.Timeout.String()
}
// resourceDockerServiceCreateRefreshFunc refreshes the state of a service when it is created and needs to converge
func resourceDockerServiceCreateRefreshFunc(
serviceID string, meta interface{}) resource.StateRefreshFunc {
return func() (interface{}, string, error) {
client := meta.(*ProviderConfig).DockerClient
ctx := context.Background()
var updater progressUpdater
if updater == nil {
updater = &replicatedConsoleLogUpdater{}
}
filter := make(map[string][]string)
filter["service"] = []string{serviceID}
filter["desired-state"] = []string{"running"}
getUpToDateTasks := func() ([]swarm.Task, error) {
return client.ListTasks(dc.ListTasksOptions{
Filters: filter,
Context: ctx,
})
}
var service *swarm.Service
service, err := client.InspectService(serviceID)
if err != nil {
return nil, "", err
}
tasks, err := getUpToDateTasks()
if err != nil {
return nil, "", err
}
activeNodes, err := getActiveNodes(ctx, client)
if err != nil {
return nil, "", err
}
serviceCreateStatus, err := updater.update(service, tasks, activeNodes, false)
if err != nil {
return nil, "", err
}
if serviceCreateStatus {
return service.ID, "running", nil
}
return service.ID, "creating", nil
}
}
// resourceDockerServiceUpdateRefreshFunc refreshes the state of a service when it is updated and needs to converge
func resourceDockerServiceUpdateRefreshFunc(
serviceID string, meta interface{}) resource.StateRefreshFunc {
return func() (interface{}, string, error) {
client := meta.(*ProviderConfig).DockerClient
ctx := context.Background()
var (
updater progressUpdater
rollback bool
)
if updater == nil {
updater = &replicatedConsoleLogUpdater{}
}
rollback = false
filter := make(map[string][]string)
filter["service"] = []string{serviceID}
filter["desired-state"] = []string{"running"}
getUpToDateTasks := func() ([]swarm.Task, error) {
return client.ListTasks(dc.ListTasksOptions{
Filters: filter,
Context: ctx,
})
}
var service *swarm.Service
service, err := client.InspectService(serviceID)
if err != nil {
return nil, "", err
}
if service.UpdateStatus != nil {
log.Printf("######## update status: %v", service.UpdateStatus.State)
switch service.UpdateStatus.State {
case swarm.UpdateStateUpdating:
rollback = false
case swarm.UpdateStateCompleted:
return service.ID, "completed", nil
case swarm.UpdateStateRollbackStarted:
rollback = true
case swarm.UpdateStateRollbackCompleted:
return nil, "", fmt.Errorf("service rollback completed: %s", service.UpdateStatus.Message)
case swarm.UpdateStatePaused:
return nil, "", fmt.Errorf("service update paused: %s", service.UpdateStatus.Message)
case swarm.UpdateStateRollbackPaused:
return nil, "", fmt.Errorf("service rollback paused: %s", service.UpdateStatus.Message)
}
}
tasks, err := getUpToDateTasks()
if err != nil {
return nil, "", err
}
activeNodes, err := getActiveNodes(ctx, client)
if err != nil {
return nil, "", err
}
isUpdateCompleted, err := updater.update(service, tasks, activeNodes, rollback)
if err != nil {
return nil, "", err
}
if isUpdateCompleted {
if rollback {
return nil, "", fmt.Errorf("service rollback completed: %s", service.UpdateStatus.Message)
}
return service.ID, "completed", nil
}
return service.ID, "updating", nil
}
}
// getActiveNodes gets the actives nodes withon a swarm
func getActiveNodes(ctx context.Context, client *dc.Client) (map[string]struct{}, error) {
nodes, err := client.ListNodes(dc.ListNodesOptions{Context: ctx})
if err != nil {
return nil, err
}
activeNodes := make(map[string]struct{})
for _, n := range nodes {
if n.Status.State != swarm.NodeStateDown {
activeNodes[n.ID] = struct{}{}
}
}
return activeNodes, nil
}
// progressUpdater interface for progressive task updates
type progressUpdater interface {
update(service *swarm.Service, tasks []swarm.Task, activeNodes map[string]struct{}, rollback bool) (bool, error)
}
// replicatedConsoleLogUpdater console log updater for replicated services
type replicatedConsoleLogUpdater struct {
// used for mapping slots to a contiguous space
// this also causes progress bars to appear in order
slotMap map[int]int
initialized bool
done bool
}
// update is the concrete implementation of updating replicated services
func (u *replicatedConsoleLogUpdater) update(service *swarm.Service, tasks []swarm.Task, activeNodes map[string]struct{}, rollback bool) (bool, error) {
if service.Spec.Mode.Replicated == nil || service.Spec.Mode.Replicated.Replicas == nil {
return false, fmt.Errorf("no replica count")
}
replicas := *service.Spec.Mode.Replicated.Replicas
if !u.initialized {
u.slotMap = make(map[int]int)
u.initialized = true
}
// get the task for each slot. there can be multiple slots on one node
tasksBySlot := u.tasksBySlot(tasks, activeNodes)
// if a converged state is reached, check if is still converged.
if u.done {
for _, task := range tasksBySlot {
if task.Status.State != swarm.TaskStateRunning {
u.done = false
break
}
}
}
running := uint64(0)
// map the slots to keep track of their state individually
for _, task := range tasksBySlot {
mappedSlot := u.slotMap[task.Slot]
if mappedSlot == 0 {
mappedSlot = len(u.slotMap) + 1
u.slotMap[task.Slot] = mappedSlot
}
// if a task is in the desired state count it as running
if !terminalState(task.DesiredState) && task.Status.State == swarm.TaskStateRunning {
running++
}
}
// check if all tasks the same amount of tasks is running than replicas defined
if !u.done {
log.Printf("[INFO] ... progress: [%v/%v] - rollback: %v", running, replicas, rollback)
if running == replicas {
log.Printf("[INFO] DONE: all %v replicas running", running)
u.done = true
}
}
return running == replicas, nil
}
// tasksBySlot maps the tasks to slots on active nodes. There can be multiple slots on active nodes.
// A task is analogous to a “slot” where (on a node) the scheduler places a container.
func (u *replicatedConsoleLogUpdater) tasksBySlot(tasks []swarm.Task, activeNodes map[string]struct{}) map[int]swarm.Task {
// if there are multiple tasks with the same slot number, favor the one
// with the *lowest* desired state. This can happen in restart
// scenarios.
tasksBySlot := make(map[int]swarm.Task)
for _, task := range tasks {
if numberedStates[task.DesiredState] == 0 || numberedStates[task.Status.State] == 0 {
continue
}
if existingTask, ok := tasksBySlot[task.Slot]; ok {
if numberedStates[existingTask.DesiredState] < numberedStates[task.DesiredState] {
continue
}
// if the desired states match, observed state breaks
// ties. This can happen with the "start first" service
// update mode.
if numberedStates[existingTask.DesiredState] == numberedStates[task.DesiredState] &&
numberedStates[existingTask.Status.State] <= numberedStates[task.Status.State] {
continue
}
}
// if the task is on a node and this node is active, then map this task to a slot
if task.NodeID != "" {
if _, nodeActive := activeNodes[task.NodeID]; !nodeActive {
continue
}
}
tasksBySlot[task.Slot] = task
}
return tasksBySlot
}
// terminalState determines if the given state is a terminal state
// meaninig 'higher' than running (see numberedStates)
func terminalState(state swarm.TaskState) bool {
return numberedStates[state] > numberedStates[swarm.TaskStateRunning]
}
//////// Mappers
// createServiceSpec creates the service spec: https://docs.docker.com/engine/api/v1.32/#operation/ServiceCreate
func createServiceSpec(d *schema.ResourceData) (swarm.ServiceSpec, error) {
serviceSpec := swarm.ServiceSpec{
Annotations: swarm.Annotations{
Name: d.Get("name").(string),
},
}
labels, err := createServiceLabels(d)
if err != nil {
return serviceSpec, err
}
serviceSpec.Labels = labels
taskTemplate, err := createServiceTaskSpec(d)
if err != nil {
return serviceSpec, err
}
serviceSpec.TaskTemplate = taskTemplate
mode, err := createServiceMode(d)
if err != nil {
return serviceSpec, err
}
serviceSpec.Mode = mode
updateConfig, err := createServiceUpdateConfig(d)
if err != nil {
return serviceSpec, err
}
serviceSpec.UpdateConfig = updateConfig
rollbackConfig, err := createServiceRollbackConfig(d)
if err != nil {
return serviceSpec, err
}
serviceSpec.RollbackConfig = rollbackConfig
endpointSpec, err := createServiceEndpointSpec(d)
if err != nil {
return serviceSpec, err
}
serviceSpec.EndpointSpec = endpointSpec
return serviceSpec, nil
}
// createServiceLabels creates the labels for the service
func createServiceLabels(d *schema.ResourceData) (map[string]string, error) {
if v, ok := d.GetOk("labels"); ok {
return mapTypeMapValsToString(v.(map[string]interface{})), nil
}
return nil, nil
}
// == start taskSpec
// createServiceTaskSpec creates the task template for the service
func createServiceTaskSpec(d *schema.ResourceData) (swarm.TaskSpec, error) {
taskSpec := swarm.TaskSpec{}
if v, ok := d.GetOk("task_spec"); ok {
if len(v.([]interface{})) > 0 {
for _, rawTaskSpec := range v.([]interface{}) {
rawTaskSpec := rawTaskSpec.(map[string]interface{})
if rawContainerSpec, ok := rawTaskSpec["container_spec"]; ok {
containerSpec, err := createContainerSpec(rawContainerSpec)
if err != nil {
return taskSpec, err
}
taskSpec.ContainerSpec = containerSpec
}
if rawResourcesSpec, ok := rawTaskSpec["resources"]; ok {
resources, err := createResources(rawResourcesSpec)
if err != nil {
return taskSpec, err
}
taskSpec.Resources = resources
}
if rawRestartPolicySpec, ok := rawTaskSpec["restart_policy"]; ok {
restartPolicy, err := createRestartPolicy(rawRestartPolicySpec)
if err != nil {
return taskSpec, err
}
taskSpec.RestartPolicy = restartPolicy
}
if rawPlacementSpec, ok := rawTaskSpec["placement"]; ok {
placement, err := createPlacement(rawPlacementSpec)
if err != nil {
return taskSpec, err
}
taskSpec.Placement = placement
}
if rawForceUpdate, ok := rawTaskSpec["force_update"]; ok {
taskSpec.ForceUpdate = uint64(rawForceUpdate.(int))
}
if rawRuntimeSpec, ok := rawTaskSpec["runtime"]; ok {
taskSpec.Runtime = swarm.RuntimeType(rawRuntimeSpec.(string))
}
if rawNetworksSpec, ok := rawTaskSpec["networks"]; ok {
networks, err := createServiceNetworks(rawNetworksSpec)
if err != nil {
return taskSpec, err
}
taskSpec.Networks = networks
}
if rawLogDriverSpec, ok := rawTaskSpec["log_driver"]; ok {
logDriver, err := createLogDriver(rawLogDriverSpec)
if err != nil {
return taskSpec, err
}
taskSpec.LogDriver = logDriver
}
}
}
}
return taskSpec, nil
}
// createContainerSpec creates the container spec
func createContainerSpec(v interface{}) (*swarm.ContainerSpec, error) {
containerSpec := swarm.ContainerSpec{}
if len(v.([]interface{})) > 0 {
for _, rawContainerSpec := range v.([]interface{}) {
rawContainerSpec := rawContainerSpec.(map[string]interface{})
if value, ok := rawContainerSpec["image"]; ok {
containerSpec.Image = value.(string)
}
if value, ok := rawContainerSpec["labels"]; ok {
containerSpec.Labels = mapTypeMapValsToString(value.(map[string]interface{}))
}
if value, ok := rawContainerSpec["command"]; ok {
containerSpec.Command = stringListToStringSlice(value.([]interface{}))
}
if value, ok := rawContainerSpec["args"]; ok {
containerSpec.Args = stringListToStringSlice(value.([]interface{}))
}
if value, ok := rawContainerSpec["hostname"]; ok {
containerSpec.Hostname = value.(string)
}
if value, ok := rawContainerSpec["env"]; ok {
containerSpec.Env = mapTypeMapValsToStringSlice(value.(map[string]interface{}))
}
if value, ok := rawContainerSpec["dir"]; ok {
containerSpec.Dir = value.(string)
}
if value, ok := rawContainerSpec["user"]; ok {
containerSpec.User = value.(string)
}
if value, ok := rawContainerSpec["groups"]; ok {
containerSpec.Groups = stringListToStringSlice(value.([]interface{}))
}
if value, ok := rawContainerSpec["privileges"]; ok {
if len(value.([]interface{})) > 0 {
containerSpec.Privileges = &swarm.Privileges{}
for _, rawPrivilegesSpec := range value.([]interface{}) {
rawPrivilegesSpec := rawPrivilegesSpec.(map[string]interface{})
if value, ok := rawPrivilegesSpec["credential_spec"]; ok {
if len(value.([]interface{})) > 0 {
containerSpec.Privileges.CredentialSpec = &swarm.CredentialSpec{}
for _, rawCredentialSpec := range value.([]interface{}) {
rawCredentialSpec := rawCredentialSpec.(map[string]interface{})
if value, ok := rawCredentialSpec["file"]; ok {
containerSpec.Privileges.CredentialSpec.File = value.(string)
}
if value, ok := rawCredentialSpec["registry"]; ok {
containerSpec.Privileges.CredentialSpec.File = value.(string)
}
}
}
}
if value, ok := rawPrivilegesSpec["se_linux_context"]; ok {
if len(value.([]interface{})) > 0 {
containerSpec.Privileges.SELinuxContext = &swarm.SELinuxContext{}
for _, rawSELinuxContext := range value.([]interface{}) {
rawSELinuxContext := rawSELinuxContext.(map[string]interface{})
if value, ok := rawSELinuxContext["disable"]; ok {
containerSpec.Privileges.SELinuxContext.Disable = value.(bool)
}
if value, ok := rawSELinuxContext["user"]; ok {
containerSpec.Privileges.SELinuxContext.User = value.(string)
}
if value, ok := rawSELinuxContext["role"]; ok {
containerSpec.Privileges.SELinuxContext.Role = value.(string)
}
if value, ok := rawSELinuxContext["type"]; ok {
containerSpec.Privileges.SELinuxContext.Type = value.(string)
}
if value, ok := rawSELinuxContext["level"]; ok {
containerSpec.Privileges.SELinuxContext.Level = value.(string)
}
}
}
}
}
}
}
if value, ok := rawContainerSpec["read_only"]; ok {
containerSpec.ReadOnly = value.(bool)
}
if value, ok := rawContainerSpec["mounts"]; ok {
mounts := []mount.Mount{}
for _, rawMount := range value.(*schema.Set).List() {
rawMount := rawMount.(map[string]interface{})
mountType := mount.Type(rawMount["type"].(string))
mountInstance := mount.Mount{
Type: mountType,
Target: rawMount["target"].(string),
Source: rawMount["source"].(string),
}
if value, ok := rawMount["read_only"]; ok {
mountInstance.ReadOnly = value.(bool)
}
if mountType == mount.TypeBind {
if value, ok := rawMount["bind_options"]; ok {
if len(value.([]interface{})) > 0 {
mountInstance.BindOptions = &mount.BindOptions{}
for _, rawBindOptions := range value.([]interface{}) {
rawBindOptions := rawBindOptions.(map[string]interface{})
if value, ok := rawBindOptions["propagation"]; ok {
mountInstance.BindOptions.Propagation = mount.Propagation(value.(string))
}
}
}
}
} else if mountType == mount.TypeVolume {
if value, ok := rawMount["volume_options"]; ok {
if len(value.([]interface{})) > 0 {
mountInstance.VolumeOptions = &mount.VolumeOptions{}
for _, rawVolumeOptions := range value.([]interface{}) {
rawVolumeOptions := rawVolumeOptions.(map[string]interface{})
if value, ok := rawVolumeOptions["no_copy"]; ok {
mountInstance.VolumeOptions.NoCopy = value.(bool)
}
if value, ok := rawVolumeOptions["labels"]; ok {
mountInstance.VolumeOptions.Labels = mapTypeMapValsToString(value.(map[string]interface{}))
}
// because it is not possible to nest maps
if value, ok := rawVolumeOptions["driver_name"]; ok {
if mountInstance.VolumeOptions.DriverConfig == nil {
mountInstance.VolumeOptions.DriverConfig = &mount.Driver{}
}
mountInstance.VolumeOptions.DriverConfig.Name = value.(string)
}
if value, ok := rawVolumeOptions["driver_options"]; ok {
if mountInstance.VolumeOptions.DriverConfig == nil {
mountInstance.VolumeOptions.DriverConfig = &mount.Driver{}
}
mountInstance.VolumeOptions.DriverConfig.Options = mapTypeMapValsToString(value.(map[string]interface{}))
}
}
}
}
} else if mountType == mount.TypeTmpfs {
if value, ok := rawMount["tmpfs_options"]; ok {
if len(value.([]interface{})) > 0 {
mountInstance.TmpfsOptions = &mount.TmpfsOptions{}
for _, rawTmpfsOptions := range value.([]interface{}) {
rawTmpfsOptions := rawTmpfsOptions.(map[string]interface{})
if value, ok := rawTmpfsOptions["size_bytes"]; ok {
mountInstance.TmpfsOptions.SizeBytes = value.(int64)
}
if value, ok := rawTmpfsOptions["mode"]; ok {
mountInstance.TmpfsOptions.Mode = os.FileMode(value.(int))
}
}
}
}
}
mounts = append(mounts, mountInstance)
}
containerSpec.Mounts = mounts
}
if value, ok := rawContainerSpec["stop_signal"]; ok {
containerSpec.StopSignal = value.(string)
}
if value, ok := rawContainerSpec["stop_grace_period"]; ok {
parsed, _ := time.ParseDuration(value.(string))
containerSpec.StopGracePeriod = &parsed
}
if value, ok := rawContainerSpec["healthcheck"]; ok {
containerSpec.Healthcheck = &container.HealthConfig{}
if len(value.([]interface{})) > 0 {
for _, rawHealthCheck := range value.([]interface{}) {
rawHealthCheck := rawHealthCheck.(map[string]interface{})
if testCommand, ok := rawHealthCheck["test"]; ok {
containerSpec.Healthcheck.Test = stringListToStringSlice(testCommand.([]interface{}))
}
if rawInterval, ok := rawHealthCheck["interval"]; ok {
containerSpec.Healthcheck.Interval, _ = time.ParseDuration(rawInterval.(string))
}
if rawTimeout, ok := rawHealthCheck["timeout"]; ok {
containerSpec.Healthcheck.Timeout, _ = time.ParseDuration(rawTimeout.(string))
}
if rawStartPeriod, ok := rawHealthCheck["start_period"]; ok {
containerSpec.Healthcheck.StartPeriod, _ = time.ParseDuration(rawStartPeriod.(string))
}
if rawRetries, ok := rawHealthCheck["retries"]; ok {
containerSpec.Healthcheck.Retries, _ = rawRetries.(int)
}
}
}
}
if value, ok := rawContainerSpec["hosts"]; ok {
containerSpec.Hosts = extraHostsSetToDockerExtraHosts(value.(*schema.Set))
}
if value, ok := rawContainerSpec["dns_config"]; ok {
containerSpec.DNSConfig = &swarm.DNSConfig{}
if len(v.([]interface{})) > 0 {
for _, rawDNSConfig := range value.([]interface{}) {
if rawDNSConfig != nil {
rawDNSConfig := rawDNSConfig.(map[string]interface{})
if nameservers, ok := rawDNSConfig["nameservers"]; ok {
containerSpec.DNSConfig.Nameservers = stringListToStringSlice(nameservers.([]interface{}))
}
if search, ok := rawDNSConfig["search"]; ok {
containerSpec.DNSConfig.Search = stringListToStringSlice(search.([]interface{}))
}
if options, ok := rawDNSConfig["options"]; ok {
containerSpec.DNSConfig.Options = stringListToStringSlice(options.([]interface{}))
}
}
}
}
}
if value, ok := rawContainerSpec["secrets"]; ok {
secrets := []*swarm.SecretReference{}
for _, rawSecret := range value.(*schema.Set).List() {
rawSecret := rawSecret.(map[string]interface{})
secret := swarm.SecretReference{
SecretID: rawSecret["secret_id"].(string),
File: &swarm.SecretReferenceFileTarget{
Name: rawSecret["file_name"].(string),
UID: "0",
GID: "0",
Mode: os.FileMode(0444),
},
}
if value, ok := rawSecret["secret_name"]; ok {
secret.SecretName = value.(string)
}
secrets = append(secrets, &secret)
}
containerSpec.Secrets = secrets
}
if value, ok := rawContainerSpec["configs"]; ok {
configs := []*swarm.ConfigReference{}
for _, rawConfig := range value.(*schema.Set).List() {
rawConfig := rawConfig.(map[string]interface{})
config := swarm.ConfigReference{
ConfigID: rawConfig["config_id"].(string),
File: &swarm.ConfigReferenceFileTarget{
Name: rawConfig["file_name"].(string),
UID: "0",
GID: "0",
Mode: os.FileMode(0444),
},
}
if value, ok := rawConfig["config_name"]; ok {
config.ConfigName = value.(string)
}
configs = append(configs, &config)
}
containerSpec.Configs = configs
}
}
}
return &containerSpec, nil
}
// createResources creates the resource requirements for the service
func createResources(v interface{}) (*swarm.ResourceRequirements, error) {
resources := swarm.ResourceRequirements{}
if len(v.([]interface{})) > 0 {
for _, rawResourcesSpec := range v.([]interface{}) {
if rawResourcesSpec != nil {
rawResourcesSpec := rawResourcesSpec.(map[string]interface{})
if value, ok := rawResourcesSpec["limits"]; ok {
if len(value.([]interface{})) > 0 {
resources.Limits = &swarm.Resources{}
for _, rawLimitsSpec := range value.([]interface{}) {
rawLimitsSpec := rawLimitsSpec.(map[string]interface{})
if value, ok := rawLimitsSpec["nano_cpus"]; ok {
resources.Limits.NanoCPUs = int64(value.(int))
}
if value, ok := rawLimitsSpec["memory_bytes"]; ok {
resources.Limits.MemoryBytes = int64(value.(int))
}
if value, ok := rawLimitsSpec["generic_resources"]; ok {
resources.Limits.GenericResources, _ = createGenericResources(value)
}
}
}
}
if value, ok := rawResourcesSpec["reservation"]; ok {
if len(value.([]interface{})) > 0 {
resources.Reservations = &swarm.Resources{}
for _, rawReservationSpec := range value.([]interface{}) {
rawReservationSpec := rawReservationSpec.(map[string]interface{})
if value, ok := rawReservationSpec["nano_cpus"]; ok {
resources.Reservations.NanoCPUs = int64(value.(int))
}
if value, ok := rawReservationSpec["memory_bytes"]; ok {
resources.Reservations.MemoryBytes = int64(value.(int))
}
if value, ok := rawReservationSpec["generic_resources"]; ok {
resources.Reservations.GenericResources, _ = createGenericResources(value)
}
}
}
}
}
}
}
return &resources, nil
}
// createGenericResources creates generic resources for a container
func createGenericResources(value interface{}) ([]swarm.GenericResource, error) {
genericResources := make([]swarm.GenericResource, 0)
if len(value.([]interface{})) > 0 {
for _, rawGenericResource := range value.([]interface{}) {
rawGenericResource := rawGenericResource.(map[string]interface{})
if rawNamedResources, ok := rawGenericResource["named_resources_spec"]; ok {
for _, rawNamedResource := range rawNamedResources.(*schema.Set).List() {
namedGenericResource := &swarm.NamedGenericResource{}
splitted := strings.Split(rawNamedResource.(string), "=")
namedGenericResource.Kind = splitted[0]
namedGenericResource.Value = splitted[1]
genericResource := swarm.GenericResource{}
genericResource.NamedResourceSpec = namedGenericResource
genericResources = append(genericResources, genericResource)
}
}
if rawDiscreteResources, ok := rawGenericResource["discrete_resources_spec"]; ok {
for _, rawDiscreteResource := range rawDiscreteResources.(*schema.Set).List() {
discreteGenericResource := &swarm.DiscreteGenericResource{}
splitted := strings.Split(rawDiscreteResource.(string), "=")
discreteGenericResource.Kind = splitted[0]
discreteGenericResource.Value, _ = strconv.ParseInt(splitted[1], 10, 64)
genericResource := swarm.GenericResource{}
genericResource.DiscreteResourceSpec = discreteGenericResource
genericResources = append(genericResources, genericResource)
}
}
}
}
return genericResources, nil
}
// createRestartPolicy creates the restart poliyc of the service
func createRestartPolicy(v interface{}) (*swarm.RestartPolicy, error) {
restartPolicy := swarm.RestartPolicy{}
rawRestartPolicy := v.(map[string]interface{})
if v, ok := rawRestartPolicy["condition"]; ok {
restartPolicy.Condition = swarm.RestartPolicyCondition(v.(string))
}
if v, ok := rawRestartPolicy["delay"]; ok {
parsed, _ := time.ParseDuration(v.(string))
restartPolicy.Delay = &parsed
}
if v, ok := rawRestartPolicy["max_attempts"]; ok {
parsed, _ := strconv.ParseInt(v.(string), 10, 64)
mapped := uint64(parsed)
restartPolicy.MaxAttempts = &mapped
}
if v, ok := rawRestartPolicy["window"]; ok {
parsed, _ := time.ParseDuration(v.(string))
restartPolicy.Window = &parsed
}
return &restartPolicy, nil
}
// createPlacement creates the placement strategy for the service
func createPlacement(v interface{}) (*swarm.Placement, error) {
placement := swarm.Placement{}
if len(v.([]interface{})) > 0 {
for _, rawPlacement := range v.([]interface{}) {
if rawPlacement != nil {
rawPlacement := rawPlacement.(map[string]interface{})
if v, ok := rawPlacement["constraints"]; ok {
placement.Constraints = stringSetToStringSlice(v.(*schema.Set))
}
if v, ok := rawPlacement["prefs"]; ok {
placement.Preferences = stringSetToPlacementPrefs(v.(*schema.Set))
}
if v, ok := rawPlacement["platforms"]; ok {
placement.Platforms = mapSetToPlacementPlatforms(v.(*schema.Set))
}
}
}
}
return &placement, nil
}
// createServiceNetworks creates the networks the service will be attachted to
func createServiceNetworks(v interface{}) ([]swarm.NetworkAttachmentConfig, error) {
networks := []swarm.NetworkAttachmentConfig{}
if len(v.(*schema.Set).List()) > 0 {
for _, rawNetwork := range v.(*schema.Set).List() {
network := swarm.NetworkAttachmentConfig{
Target: rawNetwork.(string),
}
networks = append(networks, network)
}
}
return networks, nil
}
// createLogDriver creates the log driver for the service
func createLogDriver(v interface{}) (*swarm.Driver, error) {
logDriver := swarm.Driver{}
if len(v.([]interface{})) > 0 {
for _, rawLogging := range v.([]interface{}) {
rawLogging := rawLogging.(map[string]interface{})
if rawName, ok := rawLogging["name"]; ok {
logDriver.Name = rawName.(string)
}
if rawOptions, ok := rawLogging["options"]; ok {
logDriver.Options = mapTypeMapValsToString(rawOptions.(map[string]interface{}))
}
return &logDriver, nil
}
}
return nil, nil
}
// == end taskSpec
// createServiceMode creates the mode the service will run in
func createServiceMode(d *schema.ResourceData) (swarm.ServiceMode, error) {
serviceMode := swarm.ServiceMode{}
if v, ok := d.GetOk("mode"); ok {
// because its a list
if len(v.([]interface{})) > 0 {
for _, rawMode := range v.([]interface{}) {
// with a map
rawMode := rawMode.(map[string]interface{})
if rawReplicatedMode, replModeOk := rawMode["replicated"]; replModeOk {
// with a list
if len(rawReplicatedMode.([]interface{})) > 0 {
for _, rawReplicatedModeInt := range rawReplicatedMode.([]interface{}) {
// which is a map
rawReplicatedModeMap := rawReplicatedModeInt.(map[string]interface{})
log.Printf("[INFO] Setting service mode to 'replicated'")
serviceMode.Replicated = &swarm.ReplicatedService{}
if testReplicas, testReplicasOk := rawReplicatedModeMap["replicas"]; testReplicasOk {
log.Printf("[INFO] Setting %v replicas", testReplicas)
replicas := uint64(testReplicas.(int))
serviceMode.Replicated.Replicas = &replicas
}
}
}
}
if rawGlobalMode, globalModeOk := rawMode["global"]; globalModeOk && rawGlobalMode.(bool) {
log.Printf("[INFO] Setting service mode to 'global' is %v", rawGlobalMode)
serviceMode.Global = &swarm.GlobalService{}
}
}
}
}
return serviceMode, nil
}
// createServiceUpdateConfig creates the service update config
func createServiceUpdateConfig(d *schema.ResourceData) (*swarm.UpdateConfig, error) {
if v, ok := d.GetOk("update_config"); ok {
return createUpdateOrRollbackConfig(v.([]interface{}))
}
return nil, nil
}
// createServiceRollbackConfig create the service rollback config
func createServiceRollbackConfig(d *schema.ResourceData) (*swarm.UpdateConfig, error) {
if v, ok := d.GetOk("rollback_config"); ok {
return createUpdateOrRollbackConfig(v.([]interface{}))
}
return nil, nil
}
// == start endpointSpec
// createServiceEndpointSpec creates the spec for the endpoint
func createServiceEndpointSpec(d *schema.ResourceData) (*swarm.EndpointSpec, error) {
endpointSpec := swarm.EndpointSpec{}
if v, ok := d.GetOk("endpoint_spec"); ok {
if len(v.([]interface{})) > 0 {
for _, rawEndpointSpec := range v.([]interface{}) {
if rawEndpointSpec != nil {
rawEndpointSpec := rawEndpointSpec.(map[string]interface{})
if value, ok := rawEndpointSpec["mode"]; ok {
endpointSpec.Mode = swarm.ResolutionMode(value.(string))
}
if value, ok := rawEndpointSpec["ports"]; ok {
endpointSpec.Ports = portSetToServicePorts(value)
}
}
}
}
}
return &endpointSpec, nil
}
// portSetToServicePorts maps a set of ports to portConfig
func portSetToServicePorts(v interface{}) []swarm.PortConfig {
retPortConfigs := []swarm.PortConfig{}
if len(v.(*schema.Set).List()) > 0 {
for _, portInt := range v.(*schema.Set).List() {
portConfig := swarm.PortConfig{}
rawPort := portInt.(map[string]interface{})
if value, ok := rawPort["name"]; ok {
portConfig.Name = value.(string)
}
if value, ok := rawPort["protocol"]; ok {
portConfig.Protocol = swarm.PortConfigProtocol(value.(string))
}
if value, ok := rawPort["target_port"]; ok {
portConfig.TargetPort = uint32(value.(int))
}
if externalPort, ok := rawPort["published_port"]; ok {
portConfig.PublishedPort = uint32(externalPort.(int))
} else {
// If the external port is not specified we use the internal port for it
portConfig.PublishedPort = portConfig.TargetPort
}
if value, ok := rawPort["publish_mode"]; ok {
portConfig.PublishMode = swarm.PortConfigPublishMode(value.(string))
}
retPortConfigs = append(retPortConfigs, portConfig)
}
}
return retPortConfigs
}
// == end endpointSpec
// createUpdateOrRollbackConfig create the configuration for and update or rollback
func createUpdateOrRollbackConfig(config []interface{}) (*swarm.UpdateConfig, error) {
updateConfig := swarm.UpdateConfig{}
if len(config) > 0 {
sc := config[0].(map[string]interface{})
if v, ok := sc["parallelism"]; ok {
updateConfig.Parallelism = uint64(v.(int))
}
if v, ok := sc["delay"]; ok {
updateConfig.Delay, _ = time.ParseDuration(v.(string))
}
if v, ok := sc["failure_action"]; ok {
updateConfig.FailureAction = v.(string)
}
if v, ok := sc["monitor"]; ok {
updateConfig.Monitor, _ = time.ParseDuration(v.(string))
}
if v, ok := sc["max_failure_ratio"]; ok {
value, _ := strconv.ParseFloat(v.(string), 64)
updateConfig.MaxFailureRatio = float32(value)
}
if v, ok := sc["order"]; ok {
updateConfig.Order = v.(string)
}
}
return &updateConfig, nil
}
// createConvergeConfig creates the configuration for converging
func createConvergeConfig(config []interface{}) *convergeConfig {
plainConvergeConfig := &convergeConfig{}
if len(config) > 0 {
for _, rawConvergeConfig := range config {
rawConvergeConfig := rawConvergeConfig.(map[string]interface{})
if delay, ok := rawConvergeConfig["delay"]; ok {
plainConvergeConfig.delay, _ = time.ParseDuration(delay.(string))
}
if timeout, ok := rawConvergeConfig["timeout"]; ok {
plainConvergeConfig.timeoutRaw, _ = timeout.(string)
plainConvergeConfig.timeout, _ = time.ParseDuration(timeout.(string))
}
}
}
return plainConvergeConfig
}
// authToServiceAuth maps the auth to AuthConfiguration
func authToServiceAuth(auth map[string]interface{}) dc.AuthConfiguration {
if auth["username"] != nil && len(auth["username"].(string)) > 0 && auth["password"] != nil && len(auth["password"].(string)) > 0 {
return dc.AuthConfiguration{
Username: auth["username"].(string),
Password: auth["password"].(string),
ServerAddress: auth["server_address"].(string),
}
}
return dc.AuthConfiguration{}
}
// fromRegistryAuth extract the desired AuthConfiguration for the given image
func fromRegistryAuth(image string, configs map[string]dc.AuthConfiguration) dc.AuthConfiguration {
// Remove normalized prefixes to simlify substring
image = strings.Replace(strings.Replace(image, "http://", "", 1), "https://", "", 1)
// Get the registry with optional port
lastBin := strings.Index(image, "/")
// No auth given and image name has no slash like 'alpine:3.1'
if lastBin != -1 {
serverAddress := image[0:lastBin]
if fromRegistryAuth, ok := configs[normalizeRegistryAddress(serverAddress)]; ok {
return fromRegistryAuth
}
}
return dc.AuthConfiguration{}
}
// stringSetToPlacementPrefs maps a string set to PlacementPreference
func stringSetToPlacementPrefs(stringSet *schema.Set) []swarm.PlacementPreference {
ret := []swarm.PlacementPreference{}
if stringSet == nil {
return ret
}
for _, envVal := range stringSet.List() {
ret = append(ret, swarm.PlacementPreference{
Spread: &swarm.SpreadOver{
SpreadDescriptor: envVal.(string),
},
})
}
return ret
}
// mapSetToPlacementPlatforms maps a string set to Platform
func mapSetToPlacementPlatforms(stringSet *schema.Set) []swarm.Platform {
ret := []swarm.Platform{}
if stringSet == nil {
return ret
}
for _, rawPlatform := range stringSet.List() {
rawPlatform := rawPlatform.(map[string]interface{})
ret = append(ret, swarm.Platform{
Architecture: rawPlatform["architecture"].(string),
OS: rawPlatform["os"].(string),
})
}
return ret
}
//////// States
// numberedStates are ascending sorted states for docker tasks
// meaning they appear internally in this order in the statemachine
var (
numberedStates = map[swarm.TaskState]int64{
swarm.TaskStateNew: 1,
swarm.TaskStateAllocated: 2,
swarm.TaskStatePending: 3,
swarm.TaskStateAssigned: 4,
swarm.TaskStateAccepted: 5,
swarm.TaskStatePreparing: 6,
swarm.TaskStateReady: 7,
swarm.TaskStateStarting: 8,
swarm.TaskStateRunning: 9,
// The following states are not actually shown in progress
// output, but are used internally for ordering.
swarm.TaskStateComplete: 10,
swarm.TaskStateShutdown: 11,
swarm.TaskStateFailed: 12,
swarm.TaskStateRejected: 13,
}
longestState int
)
// serviceCreatePendingStates are the pending states for the creation of a service
var serviceCreatePendingStates = []string{
"new",
"allocated",
"pending",
"assigned",
"accepted",
"preparing",
"ready",
"starting",
"creating",
"paused",
}
// serviceUpdatePendingStates are the pending states for the update of a service
var serviceUpdatePendingStates = []string{
"creating",
"updating",
}