@@ -10,11 +10,14 @@ import (
10
10
"os"
11
11
"path/filepath"
12
12
"strings"
13
+ "sync"
13
14
"text/template"
14
15
15
16
"github.com/Masterminds/sprig/v3"
16
17
"github.com/pkg/errors"
17
18
"github.com/rs/zerolog/log"
19
+ "go.opentelemetry.io/otel/attribute"
20
+ "golang.org/x/sync/errgroup"
18
21
"k8s.io/apimachinery/pkg/labels"
19
22
20
23
"github.com/grafana/tanka/internal/telemetry"
@@ -69,9 +72,6 @@ func ExportEnvironments(ctx context.Context, envs []*v1alpha1.Environment, to st
69
72
70
73
span .SetAttributes (telemetry .AttrNumEnvs (len (envs )))
71
74
72
- // Keep track of which file maps to which environment
73
- fileToEnv := map [string ]string {}
74
-
75
75
// dir must be empty
76
76
empty , err := dirEmpty (to )
77
77
if err != nil {
@@ -93,79 +93,168 @@ func ExportEnvironments(ctx context.Context, envs []*v1alpha1.Environment, to st
93
93
return fmt .Errorf ("deleting previously exported manifests from deleted environments: %w" , err )
94
94
}
95
95
96
+ parallelism := opts .Parallelism
97
+
98
+ if parallelism <= 0 {
99
+ parallelism = defaultParallelism
100
+ }
101
+
102
+ if parallelism > len (envs ) {
103
+ parallelism = len (envs )
104
+ }
105
+
96
106
// get all environments for paths
97
107
loadedEnvs , err := parallelLoadEnvironments (ctx , envs , parallelOpts {
98
108
Opts : opts .Opts ,
99
109
Selector : opts .Selector ,
100
- Parallelism : opts . Parallelism ,
110
+ Parallelism : parallelism ,
101
111
})
102
112
if err != nil {
103
113
return err
104
114
}
105
115
106
- {
107
- ctx , span := tracer .Start (ctx , "generateManifests" )
108
- defer span .End ()
116
+ fileToEnv , err := manifestEnvironments (ctx , loadedEnvs , parallelism , to , opts )
117
+ if err != nil {
118
+ return err
119
+ }
109
120
110
- // FINDING: Generating the export files takes some time. Perhaps we should parallelize this.
121
+ return exportManifestFile (to , fileToEnv , nil )
122
+ }
123
+
124
+ func manifestEnvironments (ctx context.Context , loadedEnvs []* v1alpha1.Environment , parallelism int , to string , opts * ExportEnvOpts ) (map [string ]string , error ) {
125
+ ctx , span := tracer .Start (ctx , "tanka.manifestEnvironments" )
126
+ defer span .End ()
127
+ span .SetAttributes (telemetry .AttrNumEnvs (len (loadedEnvs )))
128
+ fileToEnv := map [string ]string {}
129
+ fileToEnvLock := sync.Mutex {}
130
+
131
+ // Similar to the parallel loader, we're going to split all that up
132
+ // into multiple routines that handle manifest loading and writing
133
+ grp , ctx := errgroup .WithContext (ctx )
134
+
135
+ // Create a work channel that holds enough for all workers * 2 so that a
136
+ // worker doesn't have to wait unnecessarily:
137
+ envsToManifest := make (chan * v1alpha1.Environment , parallelism * 2 )
138
+
139
+ for range parallelism {
140
+ grp .Go (func () error {
141
+ ctx , span := tracer .Start (ctx , "tanka.manifestsGenerateWorker" )
142
+ defer span .End ()
143
+
144
+ for {
145
+ select {
146
+ case <- ctx .Done ():
147
+ telemetry .FailSpanWithError (span , ctx .Err ())
148
+ return ctx .Err ()
149
+ case work , ok := <- envsToManifest :
150
+ if ! ok {
151
+ // Channel is empty and closed
152
+ return nil
153
+ }
154
+ localFileToEnv , err := manifestSingleEnv (ctx , work , to , opts )
155
+ if err != nil {
156
+ telemetry .FailSpanWithError (span , err )
157
+ return err
158
+ }
159
+
160
+ fileToEnvLock .Lock ()
161
+ for name , namespace := range localFileToEnv {
162
+ fileToEnv [name ] = namespace
163
+ }
164
+ fileToEnvLock .Unlock ()
165
+ }
166
+ }
167
+ })
168
+ }
169
+
170
+ grp .Go (func () error {
111
171
for _ , env := range loadedEnvs {
112
- // get the manifests
113
- loaded , err := LoadManifests (ctx , env , opts .Opts .Filters )
114
- if err != nil {
115
- return err
172
+ select {
173
+ case <- ctx .Done ():
174
+ close (envsToManifest )
175
+ return ctx .Err ()
176
+ case envsToManifest <- env :
116
177
}
178
+ }
179
+ close (envsToManifest )
180
+ return nil
181
+ })
117
182
118
- env := loaded .Env
119
- res := loaded .Resources
183
+ if err := grp .Wait (); err != nil {
184
+ telemetry .FailSpanWithError (span , err )
185
+ return nil , err
186
+ }
120
187
121
- // create raw manifest version of env for templating
122
- env .Data = nil
123
- raw , err := json .Marshal (env )
124
- if err != nil {
125
- return err
126
- }
127
- var menv manifest.Manifest
128
- if err := json .Unmarshal (raw , & menv ); err != nil {
129
- return err
130
- }
188
+ return fileToEnv , nil
189
+ }
131
190
132
- // create template
133
- manifestTemplate , err := createTemplate (opts .Format , menv )
134
- if err != nil {
135
- return fmt .Errorf ("parsing format: %s" , err )
136
- }
191
+ func manifestSingleEnv (ctx context.Context , work * v1alpha1.Environment , to string , opts * ExportEnvOpts ) (map [string ]string , error ) {
192
+ ctx , span := tracer .Start (ctx , "tanka.manifestSingleEnv" )
193
+ defer span .End ()
194
+ span .SetAttributes (telemetry .AttrEnv (work )... )
137
195
138
- // write each to a file
139
- for _ , m := range res {
140
- // apply template
141
- name , err := applyTemplate (manifestTemplate , m )
142
- if err != nil {
143
- return fmt .Errorf ("executing name template: %w" , err )
144
- }
196
+ fileToEnv := make (map [string ]string )
145
197
146
- // Create all subfolders in path
147
- relpath := name + "." + opts .Extension
148
- path := filepath .Join (to , relpath )
198
+ loaded , err := LoadManifests (ctx , work , opts .Opts .Filters )
199
+ if err != nil {
200
+ return nil , err
201
+ }
149
202
150
- fileToEnv [relpath ] = env .Metadata .Namespace
203
+ env := loaded .Env
204
+ res := loaded .Resources
151
205
152
- // Abort if already exists
153
- if exists , err := fileExists (path ); err != nil {
154
- return err
155
- } else if exists {
156
- return fmt .Errorf ("file '%s' already exists. Aborting" , path )
157
- }
206
+ span .SetAttributes (attribute .Int ("tanka.env.num_resources" , len (res )))
158
207
159
- // Write manifest
160
- data := m .String ()
161
- if err := writeExportFile (path , []byte (data )); err != nil {
162
- return err
163
- }
164
- }
165
- }
208
+ // If there is no thing to process then we can skip the rest.
209
+ if len (res ) == 0 {
210
+ return fileToEnv , nil
166
211
}
167
212
168
- return exportManifestFile (to , fileToEnv , nil )
213
+ // create raw manifest version of env for templating
214
+ env .Data = nil
215
+ raw , err := json .Marshal (env )
216
+ if err != nil {
217
+ return nil , err
218
+ }
219
+ var menv manifest.Manifest
220
+ if err := json .Unmarshal (raw , & menv ); err != nil {
221
+ return nil , err
222
+ }
223
+
224
+ // create template
225
+ manifestTemplate , err := createTemplate (ctx , opts .Format , menv )
226
+ if err != nil {
227
+ return nil , fmt .Errorf ("parsing format: %s" , err )
228
+ }
229
+
230
+ // write each to a file
231
+ for _ , m := range res {
232
+ // apply template
233
+ name , err := applyTemplate (ctx , manifestTemplate , m )
234
+ if err != nil {
235
+ return nil , fmt .Errorf ("executing name template: %w" , err )
236
+ }
237
+
238
+ // Create all subfolders in path
239
+ relpath := name + "." + opts .Extension
240
+ path := filepath .Join (to , relpath )
241
+
242
+ fileToEnv [relpath ] = env .Metadata .Namespace
243
+
244
+ // Abort if already exists
245
+ if exists , err := fileExists (path ); err != nil {
246
+ return nil , err
247
+ } else if exists {
248
+ return nil , fmt .Errorf ("file '%s' already exists. Aborting" , path )
249
+ }
250
+
251
+ // Write manifest
252
+ data := m .String ()
253
+ if err := writeExportFile (path , []byte (data )); err != nil {
254
+ return nil , err
255
+ }
256
+ }
257
+ return fileToEnv , nil
169
258
}
170
259
171
260
func fileExists (name string ) (bool , error ) {
@@ -284,7 +373,7 @@ func writeExportFile(path string, data []byte) error {
284
373
return os .WriteFile (path , data , 0644 )
285
374
}
286
375
287
- func createTemplate (format string , env manifest.Manifest ) (* template.Template , error ) {
376
+ func createTemplate (_ context. Context , format string , env manifest.Manifest ) (* template.Template , error ) {
288
377
// Replace all os.path separators in string with BelRune for creating subfolders
289
378
replaceFormat := replaceTmplText (format , string (os .PathSeparator ), BelRune )
290
379
@@ -318,7 +407,7 @@ func replaceTmplText(s, oldString, newString string) string {
318
407
return strings .Join (parts , "" )
319
408
}
320
409
321
- func applyTemplate (template * template.Template , m manifest.Manifest ) (path string , err error ) {
410
+ func applyTemplate (_ context. Context , template * template.Template , m manifest.Manifest ) (path string , err error ) {
322
411
buf := bytes.Buffer {}
323
412
if err := template .Execute (& buf , m ); err != nil {
324
413
return "" , err
0 commit comments