-
Notifications
You must be signed in to change notification settings - Fork 42
/
template.go
237 lines (206 loc) · 8.74 KB
/
template.go
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
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
// Copyright 2024 Humanitec
//
// 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.
package templateprov
import (
"bytes"
"context"
"fmt"
"log/slog"
"strings"
"text/template"
"github.com/Masterminds/sprig/v3"
"github.com/compose-spec/compose-go/v2/loader"
compose "github.com/compose-spec/compose-go/v2/types"
"github.com/mitchellh/mapstructure"
"gopkg.in/yaml.v3"
"github.com/score-spec/score-compose/internal/project"
"github.com/score-spec/score-compose/internal/provisioners"
"github.com/score-spec/score-compose/internal/util"
)
// Provisioner is the decoded template provisioner.
// A template provisioner provisions a resource by evaluating a series of Go text/templates that have access to some
// input parameters, previous state, and utility functions. Each parameter is expected to return a JSON object.
type Provisioner struct {
ProvisionerUri string `yaml:"uri"`
ResType string `yaml:"type"`
ResClass *string `yaml:"class,omitempty"`
ResId *string `yaml:"id,omitempty"`
// The InitTemplate is always evaluated first, it is used as temporary or working set data that may be needed in the
// later templates. It has access to the resource inputs and previous state.
InitTemplate string `yaml:"init,omitempty"`
// StateTemplate generates the new state of the resource based on the init and previous state.
StateTemplate string `yaml:"state,omitempty"`
// SharedStateTemplate generates modifications to the shared state, based on the init and current state.
SharedStateTemplate string `yaml:"shared,omitempty"`
// OutputsTemplate generates the outputs of the resource, based on the init and current state.
OutputsTemplate string `yaml:"outputs,omitempty"`
// RelativeDirectoriesTemplate generates a set of directories to create (true) or delete (false). These may then
// be used in mounting requests for volumes or service mounts.
RelativeDirectoriesTemplate string `yaml:"directories,omitempty"`
// RelativeFilesTemplate generates a set of file contents to write (non nil) or delete (nil) from the mounts
// directory. These will then be used during service bind mounting.
RelativeFilesTemplate string `yaml:"files,omitempty"`
// ComposeNetworksTemplate generates a set of networks to add to the compose project. These will replace any with
// the same name already.
ComposeNetworksTemplate string `yaml:"networks,omitempty"`
// ComposeVolumesTemplate generates a set of volumes to add to the compose project. These will replace any with
// the same name already.
ComposeVolumesTemplate string `yaml:"volumes,omitempty"`
// ComposeServicesTemplate generates a set of services to add to the compose project. These will replace any with
// the same name already.
ComposeServicesTemplate string `yaml:"services,omitempty"`
// InfoLogsTemplate allows the provisioner to return informational messages for the user which may help connecting or
// testing the provisioned resource
InfoLogsTemplate string `yaml:"info_logs,omitempty"`
}
func Parse(raw map[string]interface{}) (*Provisioner, error) {
p := new(Provisioner)
intermediate, _ := yaml.Marshal(raw)
dec := yaml.NewDecoder(bytes.NewReader(intermediate))
dec.KnownFields(true)
if err := dec.Decode(&p); err != nil {
return nil, err
}
if p.ProvisionerUri == "" {
return nil, fmt.Errorf("uri not set")
} else if p.ResType == "" {
return nil, fmt.Errorf("type not set")
}
return p, nil
}
func (p *Provisioner) Uri() string {
return p.ProvisionerUri
}
func (p *Provisioner) Match(resUid project.ResourceUid) bool {
if resUid.Type() != p.ResType {
return false
} else if p.ResClass != nil && resUid.Class() != *p.ResClass {
return false
} else if p.ResId != nil && resUid.Id() != *p.ResId {
return false
}
return true
}
func renderTemplateAndDecode(raw string, data interface{}, out interface{}, withComposeExtensions bool) error {
raw = strings.TrimSpace(raw)
if raw == "" {
return nil
}
prepared, err := template.New("").Funcs(sprig.FuncMap()).Parse(raw)
if err != nil {
return fmt.Errorf("failed to parse template: %w", err)
}
buff := new(bytes.Buffer)
if err := prepared.Execute(buff, data); err != nil {
return fmt.Errorf("failed to execute template: %w", err)
}
buffContents := buff.String()
if strings.TrimSpace(buff.String()) == "" {
return nil
}
var intermediate interface{}
if err := yaml.Unmarshal([]byte(buffContents), &intermediate); err != nil {
slog.Debug(fmt.Sprintf("template output was '%s' from template '%s'", buffContents, raw))
return fmt.Errorf("failed to decode output: %w", err)
}
if withComposeExtensions {
err = loader.Transform(intermediate, &out)
} else {
err = mapstructure.Decode(intermediate, &out)
}
if err != nil {
return fmt.Errorf("failed to decode output: %w", err)
}
return nil
}
// Data is the structure sent to each template during rendering.
type Data struct {
Uid string
Type string
Class string
Id string
Params map[string]interface{}
Metadata map[string]interface{}
Init map[string]interface{}
State map[string]interface{}
Shared map[string]interface{}
WorkloadServices map[string]provisioners.NetworkService
ComposeProjectName string
MountsDirectory string
}
func (p *Provisioner) Provision(ctx context.Context, input *provisioners.Input) (*provisioners.ProvisionOutput, error) {
out := &provisioners.ProvisionOutput{}
// The data payload that gets passed into each template
data := Data{
Uid: input.ResourceUid,
Type: input.ResourceType,
Class: input.ResourceClass,
Id: input.ResourceId,
Params: input.ResourceParams,
Metadata: input.ResourceMetadata,
State: input.ResourceState,
Shared: input.SharedState,
WorkloadServices: input.WorkloadServices,
ComposeProjectName: input.ComposeProjectName,
MountsDirectory: input.MountDirectoryPath,
}
init := make(map[string]interface{})
if err := renderTemplateAndDecode(p.InitTemplate, &data, &init, false); err != nil {
return nil, fmt.Errorf("init template failed: %w", err)
}
data.Init = init
out.ResourceState = make(map[string]interface{})
if err := renderTemplateAndDecode(p.StateTemplate, &data, &out.ResourceState, false); err != nil {
return nil, fmt.Errorf("state template failed: %w", err)
}
data.State = out.ResourceState
out.SharedState = make(map[string]interface{})
if err := renderTemplateAndDecode(p.SharedStateTemplate, &data, &out.SharedState, false); err != nil {
return nil, fmt.Errorf("shared template failed: %w", err)
}
data.Shared = util.PatchMap(data.Shared, out.SharedState)
out.ResourceOutputs = make(map[string]interface{})
if err := renderTemplateAndDecode(p.OutputsTemplate, &data, &out.ResourceOutputs, false); err != nil {
return nil, fmt.Errorf("outputs template failed: %w", err)
}
out.RelativeDirectories = make(map[string]bool)
if err := renderTemplateAndDecode(p.RelativeDirectoriesTemplate, &data, &out.RelativeDirectories, false); err != nil {
return nil, fmt.Errorf("directories template failed: %w", err)
}
out.RelativeFileContents = make(map[string]*string)
if err := renderTemplateAndDecode(p.RelativeFilesTemplate, &data, &out.RelativeFileContents, false); err != nil {
return nil, fmt.Errorf("files template failed: %w", err)
}
out.ComposeNetworks = make(map[string]compose.NetworkConfig)
if err := renderTemplateAndDecode(p.ComposeNetworksTemplate, &data, &out.ComposeNetworks, true); err != nil {
return nil, fmt.Errorf("networks template failed: %w", err)
}
out.ComposeServices = make(map[string]compose.ServiceConfig)
if err := renderTemplateAndDecode(p.ComposeServicesTemplate, &data, &out.ComposeServices, true); err != nil {
return nil, fmt.Errorf("services template failed: %w", err)
}
out.ComposeVolumes = make(map[string]compose.VolumeConfig)
if err := renderTemplateAndDecode(p.ComposeVolumesTemplate, &data, &out.ComposeVolumes, true); err != nil {
return nil, fmt.Errorf("volumes template failed: %w", err)
}
var infoLogs []string
if err := renderTemplateAndDecode(p.InfoLogsTemplate, &data, &infoLogs, false); err != nil {
return nil, fmt.Errorf("info logs template failed: %w", err)
}
for _, log := range infoLogs {
slog.Info(fmt.Sprintf("%s: %s", input.ResourceUid, log))
}
return out, nil
}
var _ provisioners.Provisioner = (*Provisioner)(nil)