docker/swarmkit

View on GitHub
template/context.go

Summary

Maintainability
A
1 hr
Test Coverage
package template

import (
    "bytes"
    "fmt"
    "strings"
    "text/template"

    "github.com/moby/swarmkit/v2/agent/configs"
    "github.com/moby/swarmkit/v2/agent/exec"
    "github.com/moby/swarmkit/v2/agent/secrets"
    "github.com/moby/swarmkit/v2/api"
    "github.com/moby/swarmkit/v2/api/naming"
    "github.com/pkg/errors"
)

// Platform holds information about the underlying platform of the node
type Platform struct {
    Architecture string
    OS           string
}

// Context defines the strict set of values that can be injected into a
// template expression in SwarmKit data structure.
// NOTE: Be very careful adding any fields to this structure with types
// that have methods defined on them. The template would be able to
// invoke those methods.
type Context struct {
    Service struct {
        ID     string
        Name   string
        Labels map[string]string
    }

    Node struct {
        ID       string
        Hostname string
        Platform Platform
    }

    Task struct {
        ID   string
        Name string
        Slot string

        // NOTE(stevvooe): Why no labels here? Tasks don't actually have labels
        // (from a user perspective). The labels are part of the container! If
        // one wants to use labels for templating, use service labels!
    }
}

// NewContext returns a new template context from the data available in the
// task and the node where it is scheduled to run.
// The provided context can then be used to populate runtime values in a
// ContainerSpec.
func NewContext(n *api.NodeDescription, t *api.Task) (ctx Context) {
    ctx.Service.ID = t.ServiceID
    ctx.Service.Name = t.ServiceAnnotations.Name
    ctx.Service.Labels = t.ServiceAnnotations.Labels

    ctx.Node.ID = t.NodeID

    // Add node information to context only if we have them available
    if n != nil {
        ctx.Node.Hostname = n.Hostname
        ctx.Node.Platform = Platform{
            Architecture: n.Platform.Architecture,
            OS:           n.Platform.OS,
        }
    }
    ctx.Task.ID = t.ID
    ctx.Task.Name = naming.Task(t)

    if t.Slot != 0 {
        ctx.Task.Slot = fmt.Sprint(t.Slot)
    } else {
        // fall back to node id for slot when there is no slot
        ctx.Task.Slot = t.NodeID
    }

    return
}

// Expand treats the string s as a template and populates it with values from
// the context.
func (ctx *Context) Expand(s string) (string, error) {
    tmpl, err := newTemplate(s, nil)
    if err != nil {
        return s, err
    }

    var buf bytes.Buffer
    if err := tmpl.Execute(&buf, ctx); err != nil {
        return s, err
    }

    return buf.String(), nil
}

// PayloadContext provides a context for expanding a config or secret payload.
// NOTE: Be very careful adding any fields to this structure with types
// that have methods defined on them. The template would be able to
// invoke those methods.
type PayloadContext struct {
    Context

    t                 *api.Task
    restrictedSecrets exec.SecretGetter
    restrictedConfigs exec.ConfigGetter
    sensitive         bool
}

func (ctx *PayloadContext) secretGetter(target string) (string, error) {
    if ctx.restrictedSecrets == nil {
        return "", errors.New("secrets unavailable")
    }

    container := ctx.t.Spec.GetContainer()
    if container == nil {
        return "", errors.New("task is not a container")
    }

    for _, secretRef := range container.Secrets {
        file := secretRef.GetFile()
        if file != nil && file.Name == target {
            secret, err := ctx.restrictedSecrets.Get(secretRef.SecretID)
            if err != nil {
                return "", err
            }
            ctx.sensitive = true
            return string(secret.Spec.Data), nil
        }
    }

    return "", errors.Errorf("secret target %s not found", target)
}

func (ctx *PayloadContext) configGetter(target string) (string, error) {
    if ctx.restrictedConfigs == nil {
        return "", errors.New("configs unavailable")
    }

    container := ctx.t.Spec.GetContainer()
    if container == nil {
        return "", errors.New("task is not a container")
    }

    for _, configRef := range container.Configs {
        file := configRef.GetFile()
        if file != nil && file.Name == target {
            config, err := ctx.restrictedConfigs.Get(configRef.ConfigID)
            if err != nil {
                return "", err
            }
            return string(config.Spec.Data), nil
        }
    }

    return "", errors.Errorf("config target %s not found", target)
}

func (ctx *PayloadContext) envGetter(variable string) (string, error) {
    container := ctx.t.Spec.GetContainer()
    if container == nil {
        return "", errors.New("task is not a container")
    }

    for _, env := range container.Env {
        parts := strings.SplitN(env, "=", 2)

        if len(parts) > 1 && parts[0] == variable {
            return parts[1], nil
        }
    }
    return "", nil
}

// NewPayloadContextFromTask returns a new template context from the data
// available in the task and the node where it is scheduled to run.
// This context also provides access to the configs
// and secrets that the task has access to. The provided context can then
// be used to populate runtime values in a templated config or secret.
func NewPayloadContextFromTask(node *api.NodeDescription, t *api.Task, dependencies exec.DependencyGetter) (ctx PayloadContext) {
    return PayloadContext{
        Context:           NewContext(node, t),
        t:                 t,
        restrictedSecrets: secrets.Restrict(dependencies.Secrets(), t),
        restrictedConfigs: configs.Restrict(dependencies.Configs(), t),
    }
}

// Expand treats the string s as a template and populates it with values from
// the context.
func (ctx *PayloadContext) Expand(s string) (string, error) {
    funcMap := template.FuncMap{
        "secret": ctx.secretGetter,
        "config": ctx.configGetter,
        "env":    ctx.envGetter,
    }

    tmpl, err := newTemplate(s, funcMap)
    if err != nil {
        return s, err
    }

    var buf bytes.Buffer
    if err := tmpl.Execute(&buf, ctx); err != nil {
        return s, err
    }

    return buf.String(), nil
}