-
Notifications
You must be signed in to change notification settings - Fork 2
/
main.go
228 lines (197 loc) · 6.49 KB
/
main.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
package main
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path"
"github.com/benthosdev/benthos/v4/public/bloblang"
_ "github.com/benthosdev/benthos/v4/public/components/io"
sdk "github.com/cludden/concourse-go-sdk"
"github.com/cludden/concourse-go-sdk/pkg/archive"
"github.com/hashicorp/go-multierror"
"gopkg.in/yaml.v2"
)
func main() {
sdk.Main[Source, Version, GetParams, PutParams](&Resource{})
}
// =============================================================================
type (
// GetParams describes the available parameters for a get step
GetParams struct {
Files map[string]string `json:"files"`
}
// PutParams describes the available parameters for a put step
PutParams struct {
Mapping string `json:"mapping"`
}
// Source describes resource configuration
Source struct {
Archive *archive.Config `json:"archive"`
InitialMapping string `json:"initial_mapping"`
}
// Version holds arbitrary key value data that can be passed across
// jobs within a pipeline
Version struct {
Data map[string]interface{}
}
)
func (v *Version) MarshalJSON() ([]byte, error) {
return json.Marshal(v.Data)
}
func (v *Version) UnmarshalJSON(b []byte) error {
v.Data = make(map[string]interface{})
return json.Unmarshal(b, &v.Data)
}
// =============================================================================
// Resource implements a keyval concourse resource
type Resource struct {
sdk.BaseResource[Source, Version, GetParams, PutParams]
}
// Archive initializes a new archive value if configured
func (r *Resource) Archive(ctx context.Context, s *Source) (sdk.Archive, error) {
if s != nil && s.Archive != nil {
return archive.New(ctx, *s.Archive)
}
return nil, nil
}
// Check is a required resource method, but is a no-op for this resource
func (r *Resource) Check(ctx context.Context, s *Source, v *Version) (versions []Version, err error) {
if v != nil {
versions = append(versions, *v)
}
if len(versions) == 0 && s != nil && s.InitialMapping != "" {
init, _, err := r.newVersion(ctx, s.InitialMapping)
if err != nil {
return nil, err
}
versions = append(versions, init)
}
return
}
// In writes an incoming version the filesystem, allowing downstream steams to utilize
// arbitary data from an earlier put step
func (r *Resource) In(ctx context.Context, s *Source, v *Version, dir string, p *GetParams) ([]sdk.Metadata, error) {
if err := writeJSON(dir, "version.json", v); err != nil {
return nil, fmt.Errorf("error writing version.json: %v", err)
}
doc, meta := metadata()
if err := writeJSON(dir, "metadata.json", doc); err != nil {
return nil, fmt.Errorf("error writing metadata.json: %v", err)
}
if p != nil && len(p.Files) > 0 {
for k, v := range v.Data {
doc[k] = v
}
for f, m := range p.Files {
e, err := bloblang.Parse(m)
if err != nil {
return nil, fmt.Errorf("error parsing '%s' file mapping: %v", f, err)
}
raw, err := e.Query(doc)
if err != nil {
return nil, fmt.Errorf("error executing '%s' file mapping: %v", f, err)
}
var b []byte
switch v := raw.(type) {
case string:
b = []byte(v)
case []byte:
b = v
default:
switch path.Ext(f) {
case ".json":
b, err = json.Marshal(raw)
if err != nil {
return nil, fmt.Errorf("error serializing '%s' file mapping result (%T) as json: %v", f, raw, err)
}
case ".yaml", ".yml":
b, err = yaml.Marshal(raw)
if err != nil {
return nil, fmt.Errorf("error serializing '%s' file mapping result (%T) as yaml: %v", f, raw, err)
}
default:
return nil, fmt.Errorf("unclear how to serialize result (%T) returned by '%s' file mapping: try adding a supported file extension (.json, .yml)", raw, f)
}
}
if err := ioutil.WriteFile(path.Join(dir, f), b, 0777); err != nil {
return nil, fmt.Errorf("error writing '%s' file: %v", f, err)
}
}
}
return meta, nil
}
// Out generates a new version that contains arbitray key value pairs, where both keys
// and values are string data
func (r *Resource) Out(ctx context.Context, s *Source, dir string, p *PutParams) (Version, []sdk.Metadata, error) {
m := "root = this"
if p != nil && p.Mapping != "" {
m = p.Mapping
}
return r.newVersion(ctx, m)
}
// =============================================================================
// metadata returns build metadata as a map[string]interface{} to be used as the input
// document to bloblang mappings, in addition to a []concourse.Metadata to be returned
// by In and Out methods
func metadata() (doc map[string]interface{}, meta []sdk.Metadata) {
doc = map[string]interface{}{
"build_id": os.Getenv("BUILD_ID"),
"build_name": os.Getenv("BUILD_NAME"),
"build_job": os.Getenv("BUILD_JOB_NAME"),
"build_pipeline": os.Getenv("BUILD_PIPELINE_NAME"),
"build_team": os.Getenv("BUILD_TEAM_NAME"),
"build_url": fmt.Sprintf("%s/builds/%s", os.Getenv("ATC_EXTERNAL_URL"), os.Getenv("BUILD_ID")),
}
if entry := os.Getenv("BUILD_CREATED_BY"); entry != "" {
doc["build_created_by"] = entry
}
if entry := os.Getenv("BUILD_PIPELINE_INSTANCE_VARS"); entry != "" {
doc["build_instance_vars"] = entry
}
for k, v := range doc {
meta = append(meta, sdk.Metadata{
Name: k,
Value: v.(string),
})
}
return
}
// newVersion initializes a new Version value using the specified mapping
func (r *Resource) newVersion(ctx context.Context, mapping string) (Version, []sdk.Metadata, error) {
e, err := bloblang.Parse(mapping)
if err != nil {
return Version{}, nil, fmt.Errorf("error parsing 'mapping': %v", err)
}
doc, meta := metadata()
raw, err := e.Query(doc)
if err != nil {
return Version{}, nil, fmt.Errorf("error executing version mapping: %v", err)
}
data, ok := raw.(map[string]interface{})
if !ok {
return Version{}, nil, fmt.Errorf("version mapping returned invalid result, expected map, got: %T", raw)
}
errs := multierror.Append(nil)
for k, v := range data {
if _, ok := v.(string); !ok {
err = multierror.Append(err, fmt.Errorf("invalid version key '%s', expected string value, got: %T", k, v))
}
}
if errs.Len() > 0 {
return Version{}, nil, fmt.Errorf("version mapping returned invalid result: %s", errs.Error())
}
return Version{Data: data}, meta, nil
}
// writeJSON creates a formatted json file at the given directory + path
func writeJSON(dir, filename string, data interface{}) error {
f, err := os.Create(path.Join(dir, filename))
if err != nil {
return err
}
defer f.Close()
enc := json.NewEncoder(f)
enc.SetIndent("", " ")
return enc.Encode(data)
}