99 "os"
1010 "os/exec"
1111 "strings"
12+ "time"
1213
1314 "github.com/target/impeller/constants"
1415 "github.com/target/impeller/types"
@@ -19,6 +20,14 @@ import (
1920
2021const (
2122 kubectlBin = "kubectl"
23+
24+ maxRetriesDeployment = 30
25+ maxRetriesDaemonSet = 30
26+ maxRetriesStatefulSet = 120
27+ retryDelayDeployment = 10 * time .Second
28+ retryDelayDaemonSet = 10 * time .Second
29+ retryDelayStatefulSet = 10 * time .Second
30+ statefulSetLogInterval = 3
2231)
2332
2433var (
@@ -154,14 +163,31 @@ func (p *Plugin) updateHelmRepos() error {
154163
155164func (p * Plugin ) installAddon (release * types.Release ) error {
156165 log .Println ("Installing addon:" , release .Name , "@" , release .Version )
166+ var err error
157167 switch release .DeploymentMethod {
158168 case "kubectl" :
159- return p .installAddonViaKubectl (release )
169+ err = p .installAddonViaKubectl (release )
160170 case "helm" :
161171 fallthrough
162172 default :
163- return p .installAddonViaHelm (release )
173+ err = p .installAddonViaHelm (release )
174+ }
175+
176+ if err != nil {
177+ return err
164178 }
179+
180+ // Wait for resources to be ready
181+ if err := p .waitForResources (release ); err != nil {
182+ return err
183+ }
184+
185+ // Apply additional kubectl files after resources are ready
186+ if err := p .applyKubectlFiles (release ); err != nil {
187+ return err
188+ }
189+
190+ return nil
165191}
166192
167193// installAddonViaHelm installs addons via helm upgrade --install RELEASE CHART
@@ -181,6 +207,11 @@ func (p *Plugin) installAddonViaHelm(release *types.Release) error {
181207 } else if p .ClusterConfig .Helm .DefaultHistory > 0 {
182208 cb .Add (commandbuilder.Arg {Type : commandbuilder .ArgTypeLongParam , Name : "history-max" , Value : fmt .Sprint (p .ClusterConfig .Helm .DefaultHistory )})
183209 }
210+ // Force recreate resources if immutable fields change
211+ if release .Force {
212+ log .Println ("Force flag enabled: will recreate resources with immutable field changes" )
213+ cb .Add (commandbuilder.Arg {Type : commandbuilder .ArgTypeRaw , Value : "--force" })
214+ }
184215 }
185216 cb .Add (commandbuilder.Arg {Type : commandbuilder .ArgTypeRaw , Value : release .Name })
186217 cb .Add (commandbuilder.Arg {Type : commandbuilder .ArgTypeRaw , Value : release .ChartPath })
@@ -194,6 +225,10 @@ func (p *Plugin) installAddonViaHelm(release *types.Release) error {
194225 // Add namespaces to command
195226 if release .Namespace != "" {
196227 cb .Add (commandbuilder.Arg {Type : commandbuilder .ArgTypeLongParam , Name : "namespace" , Value : release .Namespace })
228+ // Create namespace if it doesn't exist (won't fail if it already exists like kube-system)
229+ if ! p .Diffrun {
230+ cb .Add (commandbuilder.Arg {Type : commandbuilder .ArgTypeRaw , Value : "--create-namespace" })
231+ }
197232 }
198233
199234 if p .ClusterConfig .Helm .LogLevel != 0 {
@@ -271,6 +306,243 @@ func (p *Plugin) installAddonViaKubectl(release *types.Release) error {
271306 return nil
272307}
273308
309+ // waitForResources waits for deployments, daemonsets, and statefulsets to be ready
310+ func (p * Plugin ) waitForResources (release * types.Release ) error {
311+ // Skip waiting if dry-run or diff-run
312+ if p .Dryrun || p .Diffrun {
313+ return nil
314+ }
315+
316+ // Wait for Deployments
317+ for _ , deployment := range release .WaitforDeployment {
318+ log .Printf ("Waiting for Deployment: %s" , deployment )
319+ if err := p .waitForResource ("deployment" , deployment , release .Namespace ); err != nil {
320+ return fmt .Errorf ("error waiting for deployment \" %s\" : %v" , deployment , err )
321+ }
322+ }
323+
324+ // Wait for DaemonSets
325+ for _ , daemonset := range release .WaitforDaemonSet {
326+ log .Printf ("Waiting for DaemonSet: %s" , daemonset )
327+ if err := p .waitForResource ("daemonset" , daemonset , release .Namespace ); err != nil {
328+ return fmt .Errorf ("error waiting for daemonset \" %s\" : %v" , daemonset , err )
329+ }
330+ }
331+
332+ // Wait for StatefulSets
333+ for _ , statefulset := range release .WaitforStatefulSet {
334+ log .Printf ("Waiting for StatefulSet: %s" , statefulset )
335+ if err := p .waitForResource ("statefulset" , statefulset , release .Namespace ); err != nil {
336+ return fmt .Errorf ("error waiting for statefulset \" %s\" : %v" , statefulset , err )
337+ }
338+ }
339+
340+ return nil
341+ }
342+
343+ // waitForResource checks if a resource is ready using polling (read-only operation)
344+ func (p * Plugin ) waitForResource (resourceType , resourceName , namespace string ) error {
345+ switch resourceType {
346+ case "deployment" :
347+ return p .waitForDeployment (resourceName , namespace )
348+ case "daemonset" :
349+ return p .waitForDaemonSet (resourceName , namespace )
350+ case "statefulset" :
351+ return p .waitForStatefulSet (resourceName , namespace )
352+ default :
353+ return fmt .Errorf ("unsupported resource type: %s" , resourceType )
354+ }
355+ }
356+
357+ func (p * Plugin ) waitForDeployment (resourceName , namespace string ) error {
358+ log .Printf ("⏳ Waiting for deployment %s/%s to be ready..." , namespace , resourceName )
359+
360+ for i := 0 ; i < maxRetriesDeployment ; i ++ {
361+ output , err := p .kubectlGetJSONPath ("deployment" , resourceName , namespace , "{.status.conditions[?(@.type=='Available')].status}" )
362+ if err == nil && strings .TrimSpace (output ) == "True" {
363+ log .Printf ("✅ Deployment %s/%s is ready" , namespace , resourceName )
364+ return nil
365+ }
366+
367+ if i < maxRetriesDeployment - 1 {
368+ time .Sleep (retryDelayDeployment )
369+ }
370+ }
371+
372+ return fmt .Errorf ("timeout waiting for deployment %s/%s" , namespace , resourceName )
373+ }
374+
375+ func (p * Plugin ) waitForDaemonSet (resourceName , namespace string ) error {
376+ log .Printf ("⏳ Waiting for daemonset %s/%s to be ready..." , namespace , resourceName )
377+
378+ for i := 0 ; i < maxRetriesDaemonSet ; i ++ {
379+ output , err := p .kubectlGetJSONPath ("daemonset" , resourceName , namespace , "{.status.numberReady},{.status.desiredNumberScheduled}" )
380+ if err == nil {
381+ parts := strings .Split (strings .TrimSpace (output ), "," )
382+ if len (parts ) == 2 && parts [0 ] == parts [1 ] && parts [0 ] != "0" {
383+ log .Printf ("✅ DaemonSet %s/%s is ready" , namespace , resourceName )
384+ return nil
385+ }
386+ }
387+
388+ if i < maxRetriesDaemonSet - 1 {
389+ time .Sleep (retryDelayDaemonSet )
390+ }
391+ }
392+
393+ return fmt .Errorf ("timeout waiting for daemonset %s/%s" , namespace , resourceName )
394+ }
395+
396+ func (p * Plugin ) waitForStatefulSet (resourceName , namespace string ) error {
397+ log .Printf ("⏳ Waiting for statefulset %s/%s to be ready..." , namespace , resourceName )
398+ log .Printf (" (This may take up to %d minutes for larger clusters)" , maxRetriesStatefulSet * int (retryDelayStatefulSet .Seconds ())/ 60 )
399+
400+ for i := 0 ; i < maxRetriesStatefulSet ; i ++ {
401+ output , err := p .kubectlGetJSONPath ("statefulset" , resourceName , namespace , "{.status.readyReplicas},{.status.replicas}" )
402+ if err == nil {
403+ parts := strings .Split (strings .TrimSpace (output ), "," )
404+ if len (parts ) == 2 && parts [0 ] == parts [1 ] && parts [0 ] != "0" {
405+ log .Printf ("✅ StatefulSet %s/%s is ready (%s/%s replicas)" , namespace , resourceName , parts [0 ], parts [1 ])
406+ return nil
407+ }
408+ if len (parts ) == 2 && i % statefulSetLogInterval == 0 {
409+ log .Printf (" Progress: %s/%s replicas ready (attempt %d/%d)" , parts [0 ], parts [1 ], i + 1 , maxRetriesStatefulSet )
410+ }
411+ }
412+
413+ if i < maxRetriesStatefulSet - 1 {
414+ time .Sleep (retryDelayStatefulSet )
415+ }
416+ }
417+
418+ return fmt .Errorf ("timeout waiting for statefulset %s/%s after %d minutes" , namespace , resourceName , maxRetriesStatefulSet * int (retryDelayStatefulSet .Seconds ())/ 60 )
419+ }
420+
421+ func (p * Plugin ) kubectlGetJSONPath (resourceType , resourceName , namespace , jsonPath string ) (string , error ) {
422+ cb := commandbuilder.CommandBuilder {Name : constants .KubectlBin }
423+ cb .Add (commandbuilder.Arg {Type : commandbuilder .ArgTypeRaw , Value : "get" })
424+ cb .Add (commandbuilder.Arg {Type : commandbuilder .ArgTypeRaw , Value : resourceType })
425+ cb .Add (commandbuilder.Arg {Type : commandbuilder .ArgTypeRaw , Value : resourceName })
426+
427+ if namespace != "" {
428+ cb .Add (commandbuilder.Arg {Type : commandbuilder .ArgTypeLongParam , Name : "namespace" , Value : namespace })
429+ }
430+
431+ if p .KubeContext != "" {
432+ cb .Add (commandbuilder.Arg {Type : commandbuilder .ArgTypeLongParam , Name : "context" , Value : p .KubeContext })
433+ }
434+
435+ cb .Add (commandbuilder.Arg {Type : commandbuilder .ArgTypeLongParam , Name : "output" , Value : fmt .Sprintf ("jsonpath=%s" , jsonPath )})
436+
437+ output , err := cb .Command ().Output ()
438+ if err != nil {
439+ return "" , err
440+ }
441+
442+ return string (output ), nil
443+ }
444+
445+ // applyKubectlFiles applies additional kubectl manifest files after deployment
446+ // Supports both individual files and directories (applies all .yaml/.yml files in directory)
447+ func (p * Plugin ) applyKubectlFiles (release * types.Release ) error {
448+ // Skip if dry-run or diff-run
449+ if p .Dryrun || p .Diffrun {
450+ return nil
451+ }
452+
453+ // Skip if no kubectl files are specified
454+ if len (release .KubectlFiles ) == 0 {
455+ return nil
456+ }
457+
458+ log .Println ("Applying additional kubectl files for:" , release .Name )
459+
460+ for _ , path := range release .KubectlFiles {
461+ // Check if path is a file or directory
462+ fileInfo , err := os .Stat (path )
463+ if err != nil {
464+ return fmt .Errorf ("error accessing path \" %s\" : %v" , path , err )
465+ }
466+
467+ var filesToApply []string
468+ if fileInfo .IsDir () {
469+ // If it's a directory, get all YAML files in it
470+ log .Printf ("Processing directory: %s" , path )
471+ files , err := p .getYAMLFilesFromDir (path )
472+ if err != nil {
473+ return fmt .Errorf ("error reading directory \" %s\" : %v" , path , err )
474+ }
475+ filesToApply = files
476+ } else {
477+ // If it's a file, apply it directly
478+ filesToApply = []string {path }
479+ }
480+
481+ // Apply each file
482+ for _ , file := range filesToApply {
483+ log .Printf ("Applying kubectl file: %s" , file )
484+
485+ cb := commandbuilder.CommandBuilder {Name : constants .KubectlBin }
486+ cb .Add (commandbuilder.Arg {Type : commandbuilder .ArgTypeRaw , Value : "apply" })
487+ cb .Add (commandbuilder.Arg {Type : commandbuilder .ArgTypeLongParam , Name : "filename" , Value : file })
488+
489+ // Don't force namespace - let the manifest define its own namespace
490+ // This allows resources to be created in their specified namespaces
491+
492+ if p .KubeContext != "" {
493+ cb .Add (commandbuilder.Arg {Type : commandbuilder .ArgTypeLongParam , Name : "context" , Value : p .KubeContext })
494+ }
495+
496+ if err := cb .Run (); err != nil {
497+ return fmt .Errorf ("error applying kubectl file \" %s\" : %v" , file , err )
498+ }
499+
500+ log .Printf ("Successfully applied kubectl file: %s" , file )
501+ }
502+ }
503+
504+ return nil
505+ }
506+
507+ // getYAMLFilesFromDir returns all .yaml and .yml files from a directory
508+ // Excludes kustomization.yaml and Kustomization.yaml files
509+ func (p * Plugin ) getYAMLFilesFromDir (dirPath string ) ([]string , error ) {
510+ var yamlFiles []string
511+
512+ files , err := ioutil .ReadDir (dirPath )
513+ if err != nil {
514+ return nil , err
515+ }
516+
517+ for _ , file := range files {
518+ if file .IsDir () {
519+ continue
520+ }
521+
522+ fileName := file .Name ()
523+
524+ // Skip kustomization files
525+ if fileName == "kustomization.yaml" || fileName == "Kustomization.yaml" ||
526+ fileName == "kustomization.yml" || fileName == "Kustomization.yml" {
527+ log .Printf ("Skipping kustomization file: %s" , fileName )
528+ continue
529+ }
530+
531+ if strings .HasSuffix (fileName , ".yaml" ) || strings .HasSuffix (fileName , ".yml" ) {
532+ fullPath := dirPath + "/" + fileName
533+ yamlFiles = append (yamlFiles , fullPath )
534+ }
535+ }
536+
537+ if len (yamlFiles ) == 0 {
538+ log .Printf ("WARNING: No YAML files found in directory: %s" , dirPath )
539+ } else {
540+ log .Printf ("Found %d YAML file(s) in directory: %s" , len (yamlFiles ), dirPath )
541+ }
542+
543+ return yamlFiles , nil
544+ }
545+
274546func (p * Plugin ) fetchChart (release * types.Release ) error {
275547 cb := commandbuilder.CommandBuilder {Name : constants .HelmBin }
276548 cb .Add (commandbuilder.Arg {Type : commandbuilder .ArgTypeRaw , Value : "fetch" })
0 commit comments