Skip to content

Commit 292acdc

Browse files
authored
Merge pull request #75 from KannanThiru/develop-wait-for-resources
add logic for waiting for dependent resources
2 parents 25a8929 + 2cb8785 commit 292acdc

File tree

5 files changed

+394
-13
lines changed

5 files changed

+394
-13
lines changed

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,36 @@ Manages Helm charts running in Kubernetes clusters.
1818
* Share chart overrides across clusters with a `default.yaml` file.
1919
* Make cluster-specific chart overrides when necessary.
2020

21+
### Managing Dependencies on Addons sequence (Optional feature)
22+
23+
* Next consecutive installation fails due previous pods take time to come up
24+
* Implemented Wait for Resource feature before moving to the next pipeline
25+
- WaitforDeployment
26+
- WaitforDaemonset
27+
- WaitforStatefulset
28+
* May need to collect desired resource getting installed using `helm template` when dependency as needed for continuous execution of pipeline.
29+
* `Kubectlfiles` options enabled in case some external configuration needed for components outside of helm install
30+
31+
```yaml
32+
name: cluster1-lab
33+
releases:
34+
#
35+
- name: sample-server
36+
namespace: kube-system
37+
version: 3.9.0
38+
chartPath: stable/sample-server
39+
waitforDeployment:
40+
- sample-server
41+
- sample-server-2
42+
waitforDaemonset:
43+
- sample-server-ds
44+
waitforStatefulset:
45+
- sample-db-server
46+
kubectlFiles:
47+
- sample-server.yaml
48+
- sample-server-lab
49+
```
50+
2151
### Other features
2252
* Use it as a [Drone](https://drone.io/) plugin for CI/CD.
2353
* Read secrets from environment variables.

plugin.go

Lines changed: 274 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
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

2021
const (
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

2433
var (
@@ -154,14 +163,31 @@ func (p *Plugin) updateHelmRepos() error {
154163

155164
func (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+
274546
func (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

Comments
 (0)