Skip to content

Commit

Permalink
Use goroutines to parallelize things while identifying drifts
Browse files Browse the repository at this point in the history
This should reduce the time taken to identify drifts.
  • Loading branch information
nikhilsbhat committed Apr 15, 2023
1 parent 362fd01 commit adf370d
Show file tree
Hide file tree
Showing 6 changed files with 164 additions and 64 deletions.
1 change: 1 addition & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ issues:
linters-settings:
funlen:
lines: 160
statements: 48

lll:
line-length: 165
Expand Down
22 changes: 11 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@
[![shields](https://img.shields.io/github/downloads/nikhilsbhat/helm-drift/total.svg)](https://github.com/nikhilsbhat/helm-drift/releases)
[![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/helm-drift)](https://artifacthub.io/packages/search?repo=helm-drift)

The helm plugin that helps in identifying deviations(mostly due to in-place edits) in the configurations that are deployed via helm chart.
The Helm plugin that comes in handy while identifying configuration drifts (mostly due to in-place edits) from the deployed Helm charts.

## Introduction

Kubernetes' resources can be deployed via package manager helm, it is easier to deploy but to manage the same require more effort.
Kubernetes' resources can be deployed via the package manager Helm; it is easier to deploy, but managing them requires more effort.

If helm is used, strictly all resources should be managed by helm itself, but there are places where manual interventions are needed.</br>
This results in configuration drift from helm charts deployed.
These changes can be overridden by next helm release, what if the required changes are lost before adding it back to helm chart?
If Helm is used, strictly all resources should be managed by Helm itself, but there are places where manual interventions are needed.</br>
This results in configuration drift from deployed helm charts..
These changes can be overridden by the next helm release, but what if the required changes are lost before adding them back to the helm chart?

This helm plugin is intended to solve the same problem by validating the resources that are part of appropriate chart/release against kubernetes.
This Helm drift plugin is intended to solve the same problem by validating the resources that are part of an appropriate chart or release against Kubernetes.

This leverages kubectl [diff](https://kubernetes.io/docs/reference/generated/kubectl/kubectl-commands#diff) to identify the drifts.

Expand Down Expand Up @@ -124,7 +124,7 @@ Use the executable just like any other go-cli application.
```bash
helm drift [command] [flags]
```
Make sure appropriate command is used for the actions, to check the available commands and flags use `helm drift --help`
Make sure the appropriate command is used for the actions. To check the available commands and flags, use `helm drift --help`

```bash
Identifies drifts (mostly due to in place edits) in the kubernetes workloads provisioned via helm charts.
Expand Down Expand Up @@ -232,9 +232,9 @@ Updated documentation on all available commands and flags can be found [here](ht
## Caveats
Identifying drifts on `CRD`'s would be tricky, plugin might not respond with correct data.
Identifying drifts on `CRDs` would be tricky, and the plugin might not respond with the correct data.
If helm hooks defined in chart, one might find drifts always on helm hooks which is marked with `hook-succeeded` and `hook-failed` when identifying drifts from charts.</br>
Things would work perfectly when identifying drifts from installed release.
If helm hooks are defined in the chart, one might find drifts always on helm hooks, which are marked with `hook-succeeded` and `hook-failed` when identifying drifts from charts..</br>
Things would work perfectly when identifying drifts from the installed release.
Support for adding a `flag` to skip helm `hooks` if required is under development.
Support for adding a `flag` to skip helm `hooks, if required, is under development.
54 changes: 42 additions & 12 deletions pkg/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,67 @@ package pkg

import (
"fmt"
"strings"
"sync"

"github.com/nikhilsbhat/helm-drift/pkg/command"
"github.com/nikhilsbhat/helm-drift/pkg/deviation"
"github.com/nikhilsbhat/helm-drift/pkg/errors"
)

func (drift *Drift) Diff(driftedRelease deviation.DriftedRelease) (deviation.DriftedRelease, error) {
diffs := make([]deviation.Deviation, 0)

var drifted bool

var waitGroup sync.WaitGroup

errChan := make(chan error)

waitGroup.Add(len(driftedRelease.Deviations))

go func() {
waitGroup.Wait()
close(errChan)
}()

for _, dvn := range driftedRelease.Deviations {
manifestPath := dvn.ManifestPath
go func(dvn deviation.Deviation) {
defer waitGroup.Done()

drift.log.Debugf("calculating diff for %s", manifestPath)
manifestPath := dvn.ManifestPath

arguments := []string{fmt.Sprintf("-f=%s", manifestPath)}
drift.log.Debugf("calculating diff for %s", manifestPath)

cmd := command.NewCommand("kubectl", drift.log)
arguments := []string{fmt.Sprintf("-f=%s", manifestPath)}

cmd.SetKubeCmd(driftedRelease.Namespace, arguments...)
cmd := command.NewCommand("kubectl", drift.log)

dft, err := cmd.RunKubeCmd(dvn)
if err != nil {
return driftedRelease, err
}
cmd.SetKubeCmd(driftedRelease.Namespace, arguments...)

dft, err := cmd.RunKubeCmd(dvn)
if err != nil {
errChan <- err
}

if dft.HasDrift {
drifted = dft.HasDrift
if dft.HasDrift {
drifted = dft.HasDrift
}

diffs = append(diffs, dft)
}(dvn)
}

var diffErrors []string

for errCh := range errChan {
if errCh != nil {
diffErrors = append(diffErrors, errCh.Error())
}
}

diffs = append(diffs, dft)
if len(diffErrors) != 0 {
return deviation.DriftedRelease{}, &errors.DriftError{Message: fmt.Sprintf("calculating diff errored with: %s", strings.Join(diffErrors, "\n"))}
}

driftedRelease.Deviations = diffs
Expand Down
84 changes: 57 additions & 27 deletions pkg/disk.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"sync"

"github.com/nikhilsbhat/helm-drift/pkg/deviation"
"github.com/nikhilsbhat/helm-drift/pkg/errors"
"github.com/nikhilsbhat/helm-drift/pkg/k8s"
"github.com/thoas/go-funk"
)
Expand Down Expand Up @@ -36,43 +39,70 @@ func (drift *Drift) renderToDisk(manifests []string, chartName, releaseName, rel

deviations := make([]deviation.Deviation, 0)

var waitGroup sync.WaitGroup

errChan := make(chan error)

waitGroup.Add(len(manifests))

go func() {
waitGroup.Wait()
close(errChan)
}()

for _, manifest := range manifests {
name, err := k8s.NewName().Get(manifest)
if err != nil {
return releaseDrifted, err
}
go func(manifest string) {
defer waitGroup.Done()

kind, err := k8s.NewKind().Get(manifest)
if err != nil {
return releaseDrifted, err
}
name, err := k8s.NewName().Get(manifest)
if err != nil {
errChan <- err
}

if len(drift.Kind) != 0 {
if !funk.Contains(drift.Kind, kind) {
continue
kind, err := k8s.NewKind().Get(manifest)
if err != nil {
errChan <- err
}
}

if len(drift.Name) != 0 {
if name != drift.Name {
continue
if len(drift.Kind) != 0 {
if !funk.Contains(drift.Kind, kind) {
return
}
}
}

drift.log.Debugf("generating manifest %s", name)
if len(drift.Name) != 0 {
if name != drift.Name {
return
}
}

manifestPath := filepath.Join(templatePath, fmt.Sprintf("%s.%s.yaml", name, kind))
if err = os.WriteFile(manifestPath, []byte(manifest), manifestFilePermission); err != nil {
return releaseDrifted, err
}
drift.log.Debugf("generating manifest %s", name)

manifestPath := filepath.Join(templatePath, fmt.Sprintf("%s.%s.yaml", name, kind))
if err = os.WriteFile(manifestPath, []byte(manifest), manifestFilePermission); err != nil {
errChan <- err
}

dvn := deviation.Deviation{
Kind: kind,
Resource: name,
TemplatePath: templatePath,
ManifestPath: manifestPath,
dvn := deviation.Deviation{
Kind: kind,
Resource: name,
TemplatePath: templatePath,
ManifestPath: manifestPath,
}
deviations = append(deviations, dvn)
}(manifest)
}

var diskErrors []string

for err := range errChan {
if err != nil {
diskErrors = append(diskErrors, err.Error())
}
deviations = append(deviations, dvn)
}

if len(diskErrors) != 0 {
return deviation.DriftedRelease{}, &errors.DriftError{Message: fmt.Sprintf("rendering helm manifests to disk errored: %s", strings.Join(diskErrors, "\n"))}
}

releaseDrifted.Deviations = deviations
Expand Down
59 changes: 45 additions & 14 deletions pkg/drift_all.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package pkg

import (
"fmt"
"strings"
"sync"
"time"

"github.com/nikhilsbhat/helm-drift/pkg/deviation"
"github.com/nikhilsbhat/helm-drift/pkg/errors"
helmRelease "helm.sh/helm/v3/pkg/release"
)

func (drift *Drift) GetAllDrift() error {
Expand Down Expand Up @@ -34,28 +39,54 @@ func (drift *Drift) GetAllDrift() error {

driftedReleases := make([]deviation.DriftedRelease, 0)

var waitGroup sync.WaitGroup

errChan := make(chan error)

waitGroup.Add(len(releases))

go func() {
waitGroup.Wait()
close(errChan)
}()

for _, release := range releases {
drift.log.Debugf("identifying drifts for release '%s'", release.Name)
go func(release *helmRelease.Release) {
defer waitGroup.Done()
drift.log.Debugf("identifying drifts for release '%s'", release.Name)

kubeKindTemplates := drift.getTemplates([]byte(release.Manifest))
kubeKindTemplates := drift.getTemplates([]byte(release.Manifest))

deviations, err := drift.renderToDisk(kubeKindTemplates, "", release.Name, release.Namespace)
if err != nil {
return err
}
deviations, err := drift.renderToDisk(kubeKindTemplates, "", release.Name, release.Namespace)
if err != nil {
errChan <- err
}

out, err := drift.Diff(deviations)
if err != nil {
return err
}
out, err := drift.Diff(deviations)
if err != nil {
errChan <- err
}

if len(out.Deviations) == 0 {
drift.log.Infof("no drifts identified for relase '%s'", release.Name)
if len(out.Deviations) == 0 {
drift.log.Infof("no drifts identified for relase '%s'", release.Name)

continue
return
}

driftedReleases = append(driftedReleases, out)
}(release)
}

var driftErrors []string

for errCh := range errChan {
if errCh != nil {
driftErrors = append(driftErrors, errCh.Error())
}
}

driftedReleases = append(driftedReleases, out)
if len(driftErrors) != 0 {
return &errors.DriftError{Message: fmt.Sprintf("identifying drifts errored with: %s", strings.Join(driftErrors, "\n"))}
}

drift.timeSpent = time.Since(startTime).Seconds()
Expand Down
8 changes: 8 additions & 0 deletions pkg/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ type PreValidationError struct {
Message string
}

type DriftError struct {
Message string
}

func (e *PreValidationError) Error() string {
return e.Message
}

func (e *DriftError) Error() string {
return e.Message
}

0 comments on commit adf370d

Please sign in to comment.