-
Notifications
You must be signed in to change notification settings - Fork 20
/
template.go
182 lines (148 loc) · 4.46 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
/*
Package dockerfilegenerator is a Dockerfile generation library. It receives any kind of Dockerfile instructions
and spits out a generated Dockerfile.
*/
package dockerfilegenerator
import (
"fmt"
"gopkg.in/yaml.v3"
"io"
"strconv"
"text/template"
)
// DockerfileTemplate defines the template struct that generates Dockerfile output
type DockerfileTemplate struct {
Data *DockerfileData
}
// NewDockerfileTemplate returns a new NewDockerfileTemplate instance
func NewDockerfileTemplate(data *DockerfileData) *DockerfileTemplate {
return &DockerfileTemplate{Data: data}
}
// Tries to return a *yaml.Node based on the given targetField
func getTargetNode(node *yaml.Node, targetField string) (*yaml.Node, error) {
type part struct {
kind string
val string
intVal int
}
last := func(parts []part) *part {
return &(parts[len(parts)-1])
}
var parts []part
var curPart part
for _, char := range targetField {
if char == '.' {
curPart = part{kind: "map"}
parts = append(parts, curPart)
continue
} else if char == '[' {
curPart = part{kind: "seq"}
parts = append(parts, curPart)
continue
} else if char == ']' {
if len(parts) == 0 || last(parts).kind != "seq" {
return nil, fmt.Errorf("invalid target-val %s", targetField)
}
intVal, err := strconv.Atoi(last(parts).val)
if err != nil {
return nil, fmt.Errorf("invalid target-val %s", targetField)
}
last(parts).intVal = intVal
continue
}
if len(parts) == 0 {
return nil, fmt.Errorf("invalid target-val %s", targetField)
}
if len(parts) > 0 {
last(parts).val += string(char)
}
}
curNode := node.Content[0]
for _, part := range parts {
if part.kind == "map" {
if curNode.Kind != yaml.MappingNode {
return nil, fmt.Errorf("Expected a map with key %s", part.val)
}
for pos, curContent := range curNode.Content {
if curContent.Value == part.val {
curNode = curNode.Content[pos+1]
break
}
}
} else if part.kind == "seq" {
if curNode.Kind != yaml.SequenceNode {
return nil, fmt.Errorf("Expected a seq with key %s", part.val)
}
curNode = curNode.Content[part.intVal]
}
}
return curNode, nil
}
func getStagesDataFromNode(node *yaml.Node) ([]Stage, error) {
var data DockerfileDataYaml
stagesInOrder, err := getStagesOrderFromYamlNode(node)
if err != nil {
return nil, fmt.Errorf("Unmarshal: %v", err)
}
if err := node.Decode(&data); err != nil {
return nil, err
}
var stages []Stage
for _, stageName := range stagesInOrder {
stages = append(stages, data.Stages[stageName])
}
return stages, nil
}
// NewDockerFileDataFromYamlField reads a YAML file and tries to extract Dockerfile data
// from the specified targetField option, examples:
// --target-field ".dev.dockerfileConfig"
// --target-field ".serverConfigs[0].docker.server"
func NewDockerFileDataFromYamlField(filename, targetField string) (*DockerfileData, error) {
node := yaml.Node{}
err := unmarshallYamlFile(filename, &node)
if err != nil {
return nil, fmt.Errorf("Unmarshal: %v", err)
}
targetNode, err := getTargetNode(&node, targetField)
if err != nil {
return nil, fmt.Errorf("Can't decode target val: %v", err)
}
stages, err := getStagesDataFromNode(targetNode)
if err != nil {
return nil, fmt.Errorf("Can't extract stages from node: %v", err)
}
return &DockerfileData{Stages: stages}, nil
}
// NewDockerFileDataFromYamlFile reads a file and returns a *DockerfileData
func NewDockerFileDataFromYamlFile(filename string) (*DockerfileData, error) {
node := yaml.Node{}
err := unmarshallYamlFile(filename, &node)
if err != nil {
return nil, fmt.Errorf("Unmarshal: %v", err)
}
// passing node.Content[0] because the file is expected to store solely the dockerfile config
stages, err := getStagesDataFromNode(node.Content[0])
if err != nil {
return nil, fmt.Errorf("Can't extract stages from node: %v", err)
}
return &DockerfileData{Stages: stages}, nil
}
// Render iterates through the given dockerfile instruction instances and executes the template.
// The output would be a generated Dockerfile.
func (d *DockerfileTemplate) Render(writer io.Writer) error {
templateString := "{{- range .Stages -}}" +
"{{- range $i, $instruction := . }}" +
"{{- if gt $i 0 }}\n{{ end }}" +
"{{ $instruction.Render }}\n" +
"{{- end }}\n\n" +
"{{ end }}"
tmpl, err := template.New("dockerfile.template").Parse(templateString)
if err != nil {
return err
}
err = tmpl.Execute(writer, d.Data)
if err != nil {
return err
}
return nil
}