2015-08-20 23:50:28 +00:00
|
|
|
package driver
|
|
|
|
|
|
|
|
import (
|
2017-05-13 00:07:54 +00:00
|
|
|
"bufio"
|
|
|
|
"bytes"
|
2017-04-12 20:26:55 +00:00
|
|
|
"context"
|
2017-04-29 22:43:23 +00:00
|
|
|
"crypto/md5"
|
2017-01-09 21:47:06 +00:00
|
|
|
"errors"
|
2015-08-20 23:50:28 +00:00
|
|
|
"fmt"
|
2017-04-29 22:43:23 +00:00
|
|
|
"io"
|
2015-08-20 23:50:28 +00:00
|
|
|
"log"
|
2016-10-07 19:37:52 +00:00
|
|
|
"os"
|
2017-05-13 00:07:54 +00:00
|
|
|
"path/filepath"
|
2016-12-20 20:24:24 +00:00
|
|
|
"strings"
|
2015-08-20 23:50:28 +00:00
|
|
|
|
2015-09-21 21:13:17 +00:00
|
|
|
"github.com/hashicorp/nomad/client/allocdir"
|
2015-09-08 19:43:02 +00:00
|
|
|
"github.com/hashicorp/nomad/client/config"
|
2016-01-11 17:58:26 +00:00
|
|
|
"github.com/hashicorp/nomad/client/driver/env"
|
2015-08-20 23:50:28 +00:00
|
|
|
"github.com/hashicorp/nomad/client/fingerprint"
|
2015-08-23 23:49:48 +00:00
|
|
|
"github.com/hashicorp/nomad/nomad/structs"
|
2015-11-14 06:07:13 +00:00
|
|
|
|
2016-06-12 03:15:50 +00:00
|
|
|
dstructs "github.com/hashicorp/nomad/client/driver/structs"
|
|
|
|
cstructs "github.com/hashicorp/nomad/client/structs"
|
2015-08-20 23:50:28 +00:00
|
|
|
)
|
|
|
|
|
2017-01-09 21:47:06 +00:00
|
|
|
var (
|
|
|
|
// BuiltinDrivers contains the built in registered drivers
|
|
|
|
// which are available for allocation handling
|
|
|
|
BuiltinDrivers = map[string]Factory{
|
|
|
|
"docker": NewDockerDriver,
|
|
|
|
"exec": NewExecDriver,
|
|
|
|
"raw_exec": NewRawExecDriver,
|
|
|
|
"java": NewJavaDriver,
|
|
|
|
"qemu": NewQemuDriver,
|
|
|
|
"rkt": NewRktDriver,
|
|
|
|
}
|
|
|
|
|
|
|
|
// DriverStatsNotImplemented is the error to be returned if a driver doesn't
|
|
|
|
// implement stats.
|
|
|
|
DriverStatsNotImplemented = errors.New("stats not implemented for driver")
|
|
|
|
)
|
2015-08-20 23:50:28 +00:00
|
|
|
|
|
|
|
// NewDriver is used to instantiate and return a new driver
|
|
|
|
// given the name and a logger
|
2015-09-10 01:06:23 +00:00
|
|
|
func NewDriver(name string, ctx *DriverContext) (Driver, error) {
|
2015-08-20 23:50:28 +00:00
|
|
|
// Lookup the factory function
|
|
|
|
factory, ok := BuiltinDrivers[name]
|
|
|
|
if !ok {
|
|
|
|
return nil, fmt.Errorf("unknown driver '%s'", name)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Instantiate the driver
|
2015-09-10 01:06:23 +00:00
|
|
|
f := factory(ctx)
|
2015-08-20 23:50:28 +00:00
|
|
|
return f, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Factory is used to instantiate a new Driver
|
2015-09-10 01:06:23 +00:00
|
|
|
type Factory func(*DriverContext) Driver
|
2015-08-20 23:50:28 +00:00
|
|
|
|
2017-01-10 21:24:45 +00:00
|
|
|
// CreatedResources is a map of resources (eg downloaded images) created by a driver
|
|
|
|
// that must be cleaned up.
|
|
|
|
type CreatedResources struct {
|
|
|
|
Resources map[string][]string
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewCreatedResources() *CreatedResources {
|
|
|
|
return &CreatedResources{Resources: make(map[string][]string)}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add a new resource if it doesn't already exist.
|
|
|
|
func (r *CreatedResources) Add(k, v string) {
|
|
|
|
if r.Resources == nil {
|
|
|
|
r.Resources = map[string][]string{k: []string{v}}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
existing, ok := r.Resources[k]
|
|
|
|
if !ok {
|
|
|
|
// Key doesn't exist, create it
|
|
|
|
r.Resources[k] = []string{v}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
for _, item := range existing {
|
|
|
|
if item == v {
|
|
|
|
// resource exists, return
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Resource type exists but value did not, append it
|
|
|
|
r.Resources[k] = append(existing, v)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-01-18 00:41:59 +00:00
|
|
|
// Remove a resource. Return true if removed, otherwise false.
|
|
|
|
//
|
|
|
|
// Removes the entire key if the needle is the last value in the list.
|
|
|
|
func (r *CreatedResources) Remove(k, needle string) bool {
|
|
|
|
haystack := r.Resources[k]
|
|
|
|
for i, item := range haystack {
|
|
|
|
if item == needle {
|
|
|
|
r.Resources[k] = append(haystack[:i], haystack[i+1:]...)
|
|
|
|
if len(r.Resources[k]) == 0 {
|
|
|
|
delete(r.Resources, k)
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2017-01-14 00:46:08 +00:00
|
|
|
// Copy returns a new deep copy of CreatedResrouces.
|
|
|
|
func (r *CreatedResources) Copy() *CreatedResources {
|
2017-01-31 17:19:59 +00:00
|
|
|
if r == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2017-01-14 00:46:08 +00:00
|
|
|
newr := CreatedResources{
|
|
|
|
Resources: make(map[string][]string, len(r.Resources)),
|
|
|
|
}
|
|
|
|
for k, v := range r.Resources {
|
|
|
|
newv := make([]string, len(v))
|
|
|
|
copy(newv, v)
|
|
|
|
newr.Resources[k] = newv
|
|
|
|
}
|
|
|
|
return &newr
|
|
|
|
}
|
|
|
|
|
2017-01-10 21:24:45 +00:00
|
|
|
// Merge another CreatedResources into this one. If the other CreatedResources
|
|
|
|
// is nil this method is a noop.
|
|
|
|
func (r *CreatedResources) Merge(o *CreatedResources) {
|
|
|
|
if o == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
for k, v := range o.Resources {
|
|
|
|
// New key
|
|
|
|
if len(r.Resources[k]) == 0 {
|
|
|
|
r.Resources[k] = v
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// Existing key
|
|
|
|
OUTER:
|
|
|
|
for _, item := range v {
|
|
|
|
for _, existing := range r.Resources[k] {
|
|
|
|
if item == existing {
|
|
|
|
// Found it, move on
|
|
|
|
continue OUTER
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// New item, append it
|
|
|
|
r.Resources[k] = append(r.Resources[k], item)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-04-29 22:43:23 +00:00
|
|
|
func (r *CreatedResources) Hash() []byte {
|
|
|
|
h := md5.New()
|
|
|
|
|
|
|
|
for k, values := range r.Resources {
|
|
|
|
io.WriteString(h, k)
|
|
|
|
io.WriteString(h, "values")
|
|
|
|
for i, v := range values {
|
|
|
|
io.WriteString(h, fmt.Sprintf("%d-%v", i, v))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return h.Sum(nil)
|
|
|
|
}
|
|
|
|
|
2015-08-20 23:50:28 +00:00
|
|
|
// Driver is used for execution of tasks. This allows Nomad
|
|
|
|
// to support many pluggable implementations of task drivers.
|
|
|
|
// Examples could include LXC, Docker, Qemu, etc.
|
|
|
|
type Driver interface {
|
|
|
|
// Drivers must support the fingerprint interface for detection
|
|
|
|
fingerprint.Fingerprint
|
2015-08-23 23:49:48 +00:00
|
|
|
|
2016-11-30 00:39:36 +00:00
|
|
|
// Prestart prepares the task environment and performs expensive
|
|
|
|
// intialization steps like downloading images.
|
2017-01-10 21:24:45 +00:00
|
|
|
//
|
|
|
|
// CreatedResources may be non-nil even when an error occurs.
|
|
|
|
Prestart(*ExecContext, *structs.Task) (*CreatedResources, error)
|
2016-11-30 00:39:36 +00:00
|
|
|
|
2015-08-23 23:49:48 +00:00
|
|
|
// Start is used to being task execution
|
|
|
|
Start(ctx *ExecContext, task *structs.Task) (DriverHandle, error)
|
|
|
|
|
|
|
|
// Open is used to re-open a handle to a task
|
|
|
|
Open(ctx *ExecContext, handleID string) (DriverHandle, error)
|
2016-04-08 20:19:43 +00:00
|
|
|
|
2017-01-10 21:24:45 +00:00
|
|
|
// Cleanup is called to remove resources which were created for a task
|
2017-01-31 18:51:32 +00:00
|
|
|
// and no longer needed. Cleanup is not called if CreatedResources is
|
|
|
|
// nil.
|
2017-01-10 21:24:45 +00:00
|
|
|
//
|
2017-01-14 00:46:08 +00:00
|
|
|
// If Cleanup returns a recoverable error it may be retried. On retry
|
|
|
|
// it will be passed the same CreatedResources, so all successfully
|
|
|
|
// cleaned up resources should be removed.
|
|
|
|
Cleanup(*ExecContext, *CreatedResources) error
|
2017-01-10 21:24:45 +00:00
|
|
|
|
2016-04-08 20:19:43 +00:00
|
|
|
// Drivers must validate their configuration
|
|
|
|
Validate(map[string]interface{}) error
|
2016-10-19 22:06:23 +00:00
|
|
|
|
|
|
|
// Abilities returns the abilities of the driver
|
|
|
|
Abilities() DriverAbilities
|
2016-12-03 01:04:07 +00:00
|
|
|
|
|
|
|
// FSIsolation returns the method of filesystem isolation used
|
|
|
|
FSIsolation() cstructs.FSIsolation
|
2016-10-19 22:06:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// DriverAbilities marks the abilities the driver has.
|
|
|
|
type DriverAbilities struct {
|
|
|
|
// SendSignals marks the driver as being able to send signals
|
|
|
|
SendSignals bool
|
2017-04-13 16:52:16 +00:00
|
|
|
|
|
|
|
// Exec marks the driver as being able to execute arbitrary commands
|
|
|
|
// such as health checks. Used by the ScriptExecutor interface.
|
|
|
|
Exec bool
|
2015-08-23 23:49:48 +00:00
|
|
|
}
|
|
|
|
|
2016-11-30 00:39:36 +00:00
|
|
|
// LogEventFn is a callback which allows Drivers to emit task events.
|
|
|
|
type LogEventFn func(message string, args ...interface{})
|
|
|
|
|
2015-09-10 01:06:23 +00:00
|
|
|
// DriverContext is a means to inject dependencies such as loggers, configs, and
|
|
|
|
// node attributes into a Driver without having to change the Driver interface
|
|
|
|
// each time we do it. Used in conjection with Factory, above.
|
|
|
|
type DriverContext struct {
|
2015-09-21 21:13:17 +00:00
|
|
|
taskName string
|
2017-03-26 00:05:53 +00:00
|
|
|
allocID string
|
2015-09-21 21:13:17 +00:00
|
|
|
config *config.Config
|
|
|
|
logger *log.Logger
|
|
|
|
node *structs.Node
|
2016-01-11 17:58:26 +00:00
|
|
|
taskEnv *env.TaskEnvironment
|
2016-11-30 00:39:36 +00:00
|
|
|
|
|
|
|
emitEvent LogEventFn
|
2015-09-10 01:06:23 +00:00
|
|
|
}
|
|
|
|
|
2016-04-09 22:38:42 +00:00
|
|
|
// NewEmptyDriverContext returns a DriverContext with all fields set to their
|
|
|
|
// zero value.
|
|
|
|
func NewEmptyDriverContext() *DriverContext {
|
2016-11-30 00:39:36 +00:00
|
|
|
return &DriverContext{}
|
2016-04-09 22:38:42 +00:00
|
|
|
}
|
|
|
|
|
2015-09-10 01:06:23 +00:00
|
|
|
// NewDriverContext initializes a new DriverContext with the specified fields.
|
|
|
|
// This enables other packages to create DriverContexts but keeps the fields
|
|
|
|
// private to the driver. If we want to change this later we can gorename all of
|
|
|
|
// the fields in DriverContext.
|
2017-03-26 00:05:53 +00:00
|
|
|
func NewDriverContext(taskName, allocID string, config *config.Config, node *structs.Node,
|
2016-11-30 00:39:36 +00:00
|
|
|
logger *log.Logger, taskEnv *env.TaskEnvironment, eventEmitter LogEventFn) *DriverContext {
|
2015-09-10 01:06:23 +00:00
|
|
|
return &DriverContext{
|
2016-11-30 00:39:36 +00:00
|
|
|
taskName: taskName,
|
2017-03-26 00:05:53 +00:00
|
|
|
allocID: allocID,
|
2016-11-30 00:39:36 +00:00
|
|
|
config: config,
|
|
|
|
node: node,
|
|
|
|
logger: logger,
|
|
|
|
taskEnv: taskEnv,
|
|
|
|
emitEvent: eventEmitter,
|
2015-09-10 01:06:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-08-23 23:49:48 +00:00
|
|
|
// DriverHandle is an opaque handle into a driver used for task
|
|
|
|
// manipulation
|
|
|
|
type DriverHandle interface {
|
|
|
|
// Returns an opaque handle that can be used to re-open the handle
|
|
|
|
ID() string
|
|
|
|
|
|
|
|
// WaitCh is used to return a channel used wait for task completion
|
2016-06-12 03:15:50 +00:00
|
|
|
WaitCh() chan *dstructs.WaitResult
|
2015-08-23 23:49:48 +00:00
|
|
|
|
2016-02-04 03:43:44 +00:00
|
|
|
// Update is used to update the task if possible and update task related
|
|
|
|
// configurations.
|
2015-08-29 22:46:10 +00:00
|
|
|
Update(task *structs.Task) error
|
|
|
|
|
2015-08-23 23:49:48 +00:00
|
|
|
// Kill is used to stop the task
|
|
|
|
Kill() error
|
2016-04-28 23:06:01 +00:00
|
|
|
|
2016-05-19 14:41:11 +00:00
|
|
|
// Stats returns aggregated stats of the driver
|
2016-04-28 23:06:01 +00:00
|
|
|
Stats() (*cstructs.TaskResourceUsage, error)
|
2016-10-07 19:37:52 +00:00
|
|
|
|
|
|
|
// Signal is used to send a signal to the task
|
|
|
|
Signal(s os.Signal) error
|
2017-04-13 16:52:16 +00:00
|
|
|
|
|
|
|
// ScriptExecutor is an interface used to execute commands such as
|
|
|
|
// health check scripts in the a DriverHandle's context.
|
|
|
|
ScriptExecutor
|
2015-08-20 23:50:28 +00:00
|
|
|
}
|
2015-08-23 22:36:06 +00:00
|
|
|
|
2017-04-13 16:52:16 +00:00
|
|
|
// ScriptExecutor is an interface that supports Exec()ing commands in the
|
|
|
|
// driver's context. Split out of DriverHandle to ease testing.
|
2017-04-12 20:26:55 +00:00
|
|
|
type ScriptExecutor interface {
|
|
|
|
Exec(ctx context.Context, cmd string, args []string) ([]byte, int, error)
|
|
|
|
}
|
|
|
|
|
2016-12-03 01:04:07 +00:00
|
|
|
// ExecContext is a task's execution context
|
2015-08-23 22:36:06 +00:00
|
|
|
type ExecContext struct {
|
2016-12-03 01:04:07 +00:00
|
|
|
// TaskDir contains information about the task directory structure.
|
|
|
|
TaskDir *allocdir.TaskDir
|
2015-08-23 22:36:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewExecContext is used to create a new execution context
|
2017-03-26 00:05:53 +00:00
|
|
|
func NewExecContext(td *allocdir.TaskDir) *ExecContext {
|
2017-01-09 19:21:51 +00:00
|
|
|
return &ExecContext{
|
2017-01-12 19:50:49 +00:00
|
|
|
TaskDir: td,
|
2017-01-09 19:21:51 +00:00
|
|
|
}
|
2015-08-23 22:36:06 +00:00
|
|
|
}
|
2015-09-23 05:33:29 +00:00
|
|
|
|
2016-03-02 00:08:21 +00:00
|
|
|
// GetTaskEnv converts the alloc dir, the node, task and alloc into a
|
2015-09-27 00:56:14 +00:00
|
|
|
// TaskEnvironment.
|
2016-12-03 01:04:07 +00:00
|
|
|
func GetTaskEnv(taskDir *allocdir.TaskDir, node *structs.Node,
|
2016-12-20 20:24:24 +00:00
|
|
|
task *structs.Task, alloc *structs.Allocation, conf *config.Config,
|
|
|
|
vaultToken string) (*env.TaskEnvironment, error) {
|
2016-03-02 00:08:21 +00:00
|
|
|
|
2016-01-11 17:58:26 +00:00
|
|
|
env := env.NewTaskEnvironment(node).
|
2016-12-16 18:45:09 +00:00
|
|
|
SetTaskMeta(alloc.Job.CombinedTaskMeta(alloc.TaskGroup, task.Name)).
|
2016-10-11 17:57:12 +00:00
|
|
|
SetJobName(alloc.Job.Name).
|
2017-03-30 21:15:06 +00:00
|
|
|
SetDatacenterName(node.Datacenter).
|
2017-04-13 18:47:07 +00:00
|
|
|
SetRegionName(conf.Region).
|
2016-03-02 00:08:21 +00:00
|
|
|
SetEnvvars(task.Env).
|
|
|
|
SetTaskName(task.Name)
|
2016-01-11 17:58:26 +00:00
|
|
|
|
2017-05-13 00:07:54 +00:00
|
|
|
// Set env vars from env files
|
|
|
|
for _, tmpl := range task.Templates {
|
|
|
|
if !tmpl.Envvars {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
f, err := os.Open(filepath.Join(taskDir.Dir, tmpl.DestPath))
|
|
|
|
if err != nil {
|
|
|
|
//FIXME GetTaskEnv may be called before env files are written
|
|
|
|
log.Printf("[DEBUG] driver: XXX FIXME Templates not rendered yet, skipping")
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
vars, err := parseEnvFile(f)
|
|
|
|
if err != nil {
|
|
|
|
//TODO soft or hard fail?!
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
env.AppendEnvvars(vars)
|
|
|
|
}
|
|
|
|
|
2017-01-06 20:19:32 +00:00
|
|
|
// Vary paths by filesystem isolation used
|
|
|
|
drv, err := NewDriver(task.Driver, NewEmptyDriverContext())
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
switch drv.FSIsolation() {
|
|
|
|
case cstructs.FSIsolationNone:
|
|
|
|
// Use host paths
|
2016-12-03 01:04:07 +00:00
|
|
|
env.SetAllocDir(taskDir.SharedAllocDir)
|
|
|
|
env.SetTaskLocalDir(taskDir.LocalDir)
|
|
|
|
env.SetSecretsDir(taskDir.SecretsDir)
|
2017-01-06 20:19:32 +00:00
|
|
|
default:
|
|
|
|
// filesystem isolation; use container paths
|
|
|
|
env.SetAllocDir(allocdir.SharedAllocContainerPath)
|
|
|
|
env.SetTaskLocalDir(allocdir.TaskLocalContainerPath)
|
|
|
|
env.SetSecretsDir(allocdir.TaskSecretsContainerPath)
|
2015-09-27 00:56:14 +00:00
|
|
|
}
|
2015-09-23 06:11:55 +00:00
|
|
|
|
|
|
|
if task.Resources != nil {
|
2016-03-02 00:08:21 +00:00
|
|
|
env.SetMemLimit(task.Resources.MemoryMB).
|
|
|
|
SetCpuLimit(task.Resources.CPU).
|
|
|
|
SetNetworks(task.Resources.Networks)
|
|
|
|
}
|
|
|
|
|
|
|
|
if alloc != nil {
|
2016-03-10 02:09:51 +00:00
|
|
|
env.SetAlloc(alloc)
|
2015-09-23 06:11:55 +00:00
|
|
|
}
|
2015-09-23 05:33:29 +00:00
|
|
|
|
2016-09-20 20:22:29 +00:00
|
|
|
if task.Vault != nil {
|
|
|
|
env.SetVaultToken(vaultToken, task.Vault.Env)
|
|
|
|
}
|
2016-09-14 20:30:01 +00:00
|
|
|
|
2017-01-18 18:27:03 +00:00
|
|
|
// Set the host environment variables for non-image based drivers
|
|
|
|
if drv.FSIsolation() != cstructs.FSIsolationImage {
|
|
|
|
filter := strings.Split(conf.ReadDefault("env.blacklist", config.DefaultEnvBlacklist), ",")
|
|
|
|
env.AppendHostEnvvars(filter)
|
|
|
|
}
|
2016-12-20 20:24:24 +00:00
|
|
|
|
2016-01-11 17:58:26 +00:00
|
|
|
return env.Build(), nil
|
2015-09-23 05:33:29 +00:00
|
|
|
}
|
2015-11-20 05:29:37 +00:00
|
|
|
|
2017-05-13 00:07:54 +00:00
|
|
|
// parseEnvFile and return a map of the environment variables suitable for
|
|
|
|
// TaskEnvironment.AppendEnvvars or an error.
|
|
|
|
//
|
|
|
|
// See nomad/structs#Template.Envvars comment for format.
|
|
|
|
func parseEnvFile(r io.Reader) (map[string]string, error) {
|
|
|
|
vars := make(map[string]string, 50)
|
|
|
|
lines := 0
|
|
|
|
scanner := bufio.NewScanner(r)
|
|
|
|
for scanner.Scan() {
|
|
|
|
lines++
|
|
|
|
buf := scanner.Bytes()
|
|
|
|
if len(buf) == 0 {
|
|
|
|
// Skip empty lines
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if buf[0] == '#' {
|
|
|
|
// Skip lines starting with a #
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
n := bytes.IndexByte(buf, '=')
|
|
|
|
if n == -1 {
|
|
|
|
return nil, fmt.Errorf("error on line %d: no '=' sign: %q", lines, string(buf))
|
|
|
|
}
|
|
|
|
if len(buf) > n {
|
|
|
|
vars[string(buf[0:n])] = string(buf[n+1 : len(buf)])
|
|
|
|
} else {
|
|
|
|
vars[string(buf[0:n])] = ""
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return vars, nil
|
|
|
|
}
|
|
|
|
|
2015-11-20 05:29:37 +00:00
|
|
|
func mapMergeStrInt(maps ...map[string]int) map[string]int {
|
|
|
|
out := map[string]int{}
|
|
|
|
for _, in := range maps {
|
|
|
|
for key, val := range in {
|
|
|
|
out[key] = val
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return out
|
|
|
|
}
|
|
|
|
|
|
|
|
func mapMergeStrStr(maps ...map[string]string) map[string]string {
|
|
|
|
out := map[string]string{}
|
|
|
|
for _, in := range maps {
|
|
|
|
for key, val := range in {
|
|
|
|
out[key] = val
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return out
|
|
|
|
}
|