@@ -4,13 +4,18 @@ import (
4
4
"context"
5
5
"fmt"
6
6
"os"
7
+ "sort"
8
+ "strings"
9
+ "sync"
7
10
8
11
"github.com/fatih/color"
9
12
"github.com/rs/zerolog/log"
13
+ "golang.org/x/sync/errgroup"
10
14
11
15
"github.com/grafana/tanka/pkg/kubernetes"
12
16
"github.com/grafana/tanka/pkg/kubernetes/client"
13
17
"github.com/grafana/tanka/pkg/kubernetes/manifest"
18
+ "github.com/grafana/tanka/pkg/spec/v1alpha1"
14
19
"github.com/grafana/tanka/pkg/term"
15
20
)
16
21
@@ -170,6 +175,8 @@ type DiffOpts struct {
170
175
WithPrune bool
171
176
// Exit with 0 even when differences are found
172
177
ExitZero bool
178
+ // List all available environments and exit
179
+ ListModifiedEnvs bool
173
180
}
174
181
175
182
// Diff parses the environment at the given directory (a `baseDir`) and returns
@@ -178,6 +185,10 @@ type DiffOpts struct {
178
185
// The cluster information is retrieved from the environments `spec.json`.
179
186
// NOTE: This function requires on `diff(1)` and `kubectl(1)`
180
187
func Diff (ctx context.Context , baseDir string , opts DiffOpts ) (* string , error ) {
188
+ if opts .ListModifiedEnvs {
189
+ return ListChangedEnvironments (ctx , baseDir , opts )
190
+ }
191
+
181
192
l , err := Load (ctx , baseDir , opts .Opts )
182
193
if err != nil {
183
194
return nil , err
@@ -195,6 +206,91 @@ func Diff(ctx context.Context, baseDir string, opts DiffOpts) (*string, error) {
195
206
})
196
207
}
197
208
209
+ // ListChangedEnvironments performs a high-level check using kubectl dry-run to identify environments with changes
210
+ func ListChangedEnvironments (ctx context.Context , baseDir string , opts DiffOpts ) (* string , error ) {
211
+ // Find all environments in the directory
212
+ envMetas , err := FindEnvs (ctx , baseDir , FindOpts {
213
+ JsonnetOpts : opts .JsonnetOpts ,
214
+ JsonnetImplementation : opts .JsonnetImplementation ,
215
+ Parallelism : 8 , // magic number for now
216
+ })
217
+ if err != nil {
218
+ return nil , err
219
+ }
220
+
221
+ changed := CheckEnvironmentsForChanges (ctx , envMetas , opts )
222
+ if len (changed ) == 0 {
223
+ return nil , nil
224
+ }
225
+
226
+ sort .Strings (changed )
227
+
228
+ result := strings .Join (changed , "\n " )
229
+ return & result , nil
230
+ }
231
+
232
+ // CheckEnvironmentsForChanges performs a high-level parallel check using kubectl diff --exit-code
233
+ func CheckEnvironmentsForChanges (ctx context.Context , envs []* v1alpha1.Environment , opts DiffOpts ) []string {
234
+ var mu sync.Mutex
235
+ var changed []string
236
+
237
+ g , ctx := errgroup .WithContext (ctx )
238
+ g .SetLimit (4 )
239
+
240
+ for _ , env := range envs {
241
+ envLoop := env
242
+ g .Go (func () error {
243
+ if hasChanges , envName := checkSingleEnvironmentChanges (ctx , envLoop , opts ); hasChanges {
244
+ mu .Lock ()
245
+ changed = append (changed , envName )
246
+ mu .Unlock ()
247
+ }
248
+ return nil
249
+ })
250
+ }
251
+
252
+ if err := g .Wait (); err != nil {
253
+ log .Warn ().Err (err ).Msg ("Failed to check environments for changes" )
254
+ }
255
+ return changed
256
+ }
257
+
258
+ // checkSingleEnvironmentChanges uses kubectl diff to quickly check for changes
259
+ func checkSingleEnvironmentChanges (ctx context.Context , env * v1alpha1.Environment , opts DiffOpts ) (bool , string ) {
260
+ envName := env .Spec .Namespace
261
+ if env .Metadata .Name != "" {
262
+ envName = env .Metadata .Name
263
+ }
264
+
265
+ // Load only this single environment to get its resources
266
+ tempOpts := opts .Opts
267
+ tempOpts .Name = env .Metadata .Name
268
+
269
+ // Use the environment's path for loading
270
+ envPath := env .Metadata .Namespace
271
+ l , err := Load (ctx , envPath , tempOpts )
272
+ if err != nil {
273
+ log .Warn ().Err (err ).Str ("env" , envName ).Msg ("Failed to load environment, assuming no changes" )
274
+ return false , envName
275
+ }
276
+
277
+ kube , err := l .Connect ()
278
+ if err != nil {
279
+ log .Warn ().Err (err ).Str ("env" , envName ).Msg ("Failed to connect, assuming no changes" )
280
+ return false , envName
281
+ }
282
+ defer kube .Close ()
283
+
284
+ // Use a lightweight check via `kubectl diff --exit-code`
285
+ hasChanges , err := kube .HasChanges (l .Resources )
286
+ if err != nil {
287
+ log .Warn ().Err (err ).Str ("env" , envName ).Msg ("Failed to check changes, assuming changes exist" )
288
+ return true , envName
289
+ }
290
+
291
+ return hasChanges , envName
292
+ }
293
+
198
294
// DeleteOpts specify additional properties for the Delete operation
199
295
type DeleteOpts struct {
200
296
ApplyBaseOpts
0 commit comments