1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212
|
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
}
|