Skip to content

Commit

Permalink
return to hiding hooks in the diff, render seperately (#185)
Browse files Browse the repository at this point in the history
  • Loading branch information
djeebus authored Apr 11, 2024
1 parent 1002f98 commit 30ca037
Show file tree
Hide file tree
Showing 8 changed files with 507 additions and 1 deletion.
9 changes: 9 additions & 0 deletions cmd/processors.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/zapier/kubechecks/pkg/checks"
"github.com/zapier/kubechecks/pkg/checks/diff"
"github.com/zapier/kubechecks/pkg/checks/hooks"
"github.com/zapier/kubechecks/pkg/checks/kubeconform"
"github.com/zapier/kubechecks/pkg/checks/preupgrade"
"github.com/zapier/kubechecks/pkg/checks/rego"
Expand All @@ -19,6 +20,14 @@ func getProcessors(ctr container.Container) ([]checks.ProcessorEntry, error) {
Processor: diff.Check,
})

if ctr.Config.EnableHooksRenderer {
procs = append(procs, checks.ProcessorEntry{
Name: "render hooks",
Processor: hooks.Check,
WorstState: ctr.Config.WorstHooksState,
})
}

if ctr.Config.EnableKubeConform {
procs = append(procs, checks.ProcessorEntry{
Name: "validating app against schema",
Expand Down
4 changes: 4 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ func init() {
int64Flag(flags, "max-concurrenct-checks", "Number of concurrent checks to run.",
newInt64Opts().
withDefault(32))
boolFlag(flags, "enable-hooks-renderer", "Render hooks.", newBoolOpts().withDefault(true))
stringFlag(flags, "worst-hooks-state", "The worst state that can be returned from the hooks renderer.",
newStringOpts().
withDefault("panic"))

panicIfError(viper.BindPFlags(flags))
setupLogOutput()
Expand Down
2 changes: 2 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ The full list of supported environment variables is described below:
|`KUBECHECKS_ARGOCD_API_SERVER_ADDR`|ArgoCD API Server Address.|`argocd-server`|
|`KUBECHECKS_ARGOCD_API_TOKEN`|ArgoCD API token.||
|`KUBECHECKS_ENABLE_CONFTEST`|Set to true to enable conftest policy checking of manifests.|`false`|
|`KUBECHECKS_ENABLE_HOOKS_RENDERER`|Render hooks.|`true`|
|`KUBECHECKS_ENABLE_KUBECONFORM`|Enable kubeconform checks.|`true`|
|`KUBECHECKS_ENABLE_PREUPGRADE`|Enable preupgrade checks.|`true`|
|`KUBECHECKS_ENSURE_WEBHOOKS`|Ensure that webhooks are created in repositories referenced by argo.|`false`|
Expand Down Expand Up @@ -67,5 +68,6 @@ The full list of supported environment variables is described below:
|`KUBECHECKS_WEBHOOK_URL_BASE`|The endpoint to listen on for incoming PR/MR event webhooks. For example, 'https://checker.mycompany.com'.||
|`KUBECHECKS_WEBHOOK_URL_PREFIX`|If your application is running behind a proxy that uses path based routing, set this value to match the path prefix. For example, '/hello/world'.||
|`KUBECHECKS_WORST_CONFTEST_STATE`|The worst state that can be returned from conftest.|`panic`|
|`KUBECHECKS_WORST_HOOKS_STATE`|The worst state that can be returned from the hooks renderer.|`panic`|
|`KUBECHECKS_WORST_KUBECONFORM_STATE`|The worst state that can be returned from kubeconform.|`panic`|
|`KUBECHECKS_WORST_PREUPGRADE_STATE`|The worst state that can be returned from preupgrade checks.|`panic`|
10 changes: 9 additions & 1 deletion pkg/checks/diff/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import (
"github.com/argoproj/argo-cd/v2/util/argo"
argodiff "github.com/argoproj/argo-cd/v2/util/argo/diff"
"github.com/argoproj/gitops-engine/pkg/diff"
"github.com/argoproj/gitops-engine/pkg/sync/hook"
"github.com/argoproj/gitops-engine/pkg/sync/ignore"
"github.com/argoproj/gitops-engine/pkg/utils/kube"
"github.com/ghodss/yaml"
"github.com/go-logr/zerologr"
Expand Down Expand Up @@ -97,6 +99,10 @@ func Check(ctx context.Context, request checks.Request) (msg.Result, error) {
resourceId := fmt.Sprintf("%s/%s %s/%s", item.key.Group, item.key.Kind, item.key.Namespace, item.key.Name)
log.Trace().Str("resource", resourceId).Msg("diffing object")

if item.target != nil && hook.IsHook(item.target) || item.live != nil && hook.IsHook(item.live) {
continue
}

diffRes, err := generateDiff(ctx, request, argoSettings, item)
if err != nil {
return msg.Result{}, err
Expand Down Expand Up @@ -283,7 +289,9 @@ func groupObjsByKey(localObs []*unstructured.Unstructured, liveObjs []*unstructu
objByKey := make(map[kube.ResourceKey]*unstructured.Unstructured)
for i := range localObs {
obj := localObs[i]
objByKey[kube.GetResourceKey(obj)] = obj
if !(hook.IsHook(obj) || ignore.Ignore(obj)) {
objByKey[kube.GetResourceKey(obj)] = obj
}
}
return objByKey, nil
}
Expand Down
167 changes: 167 additions & 0 deletions pkg/checks/hooks/check.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package hooks

import (
"context"
"fmt"
"slices"
"strconv"
"strings"

"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
"github.com/argoproj/gitops-engine/pkg/sync/resource"
"github.com/ghodss/yaml"
"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"

"github.com/zapier/kubechecks/pkg"
"github.com/zapier/kubechecks/pkg/checks"
"github.com/zapier/kubechecks/pkg/msg"
)

func Check(_ context.Context, request checks.Request) (msg.Result, error) {
grouped := make(groupedSyncWaves)

for _, manifest := range request.JsonManifests {
obj, err := v1alpha1.UnmarshalToUnstructured(manifest)
if err != nil {
return msg.Result{}, errors.Wrap(err, "failed to parse manifest")
}

waves, err := phasesAndWaves(obj)
if err != nil {
return msg.Result{}, errors.Wrap(err, "failed to get phases and waves")
}
for _, hookInfo := range waves {
grouped.addResource(hookInfo.hookType, hookInfo.hookWave, obj)
}
}

var phaseNames []argocdSyncPhase
var phaseDetails []string
results := grouped.getSortedPhasesAndWaves()
for _, pw := range results {
if !slices.Contains(phaseNames, pw.phase) {
phaseNames = append(phaseNames, pw.phase)
}

var waveDetails []string
for _, w := range pw.waves {
var resources []string
for _, r := range w.resources {
data, err := yaml.Marshal(r.Object)
renderedResource := strings.TrimSpace(string(data))
if err != nil {
return msg.Result{}, errors.Wrap(err, "failed to unmarshal yaml")
}

renderedResource = collapsible(
fmt.Sprintf("%s/%s %s/%s", r.GetAPIVersion(), r.GetKind(), r.GetNamespace(), r.GetName()),
code("yaml", renderedResource),
)
resources = append(resources, renderedResource)
}

sectionName := fmt.Sprintf("Wave %d (%s)", w.wave, plural(resources, "resource", "resources"))
waveDetail := collapsible(sectionName, strings.Join(resources, "\n\n"))
waveDetails = append(waveDetails, waveDetail)
}

phaseDetail := collapsible(
fmt.Sprintf("%s phase (%s)", pw.phase, plural(waveDetails, "wave", "waves")),
strings.Join(waveDetails, "\n\n"),
)
phaseDetails = append(phaseDetails, phaseDetail)
}

return msg.Result{
State: pkg.StateNone,
Summary: fmt.Sprintf("<b>Sync Phases: %s</b>", strings.Join(toStringSlice(phaseNames), ", ")),
Details: strings.Join(phaseDetails, "\n\n"),
NoChangesDetected: false,
}, nil
}

func plural[T any](items []T, singular, plural string) string {
var description string
if len(items) == 1 {
description = singular
} else {
description = plural
}

return fmt.Sprintf("%d %s", len(items), description)
}

func toStringSlice(hookTypes []argocdSyncPhase) []string {
result := make([]string, len(hookTypes))
for idx := range hookTypes {
result[idx] = string(hookTypes[idx])
}
return result
}

type hookInfo struct {
hookType argocdSyncPhase
hookWave waveNum
}

func code(format, content string) string {
return fmt.Sprintf("```%s\n%s\n```", format, content)
}

func collapsible(summary, details string) string {
return fmt.Sprintf("<details>\n<summary>%s</summary>\n\n%s\n</details>", summary, details)
}

func phasesAndWaves(obj *unstructured.Unstructured) ([]hookInfo, error) {
var (
syncWave int64
err error
hookInfos []hookInfo
)

syncWaveStr := obj.GetAnnotations()["argocd.argoproj.io/sync-wave"]
if syncWaveStr == "" {
syncWaveStr = obj.GetAnnotations()["helm.sh/hook-weight"]
}
if syncWaveStr != "" {
if syncWave, err = strconv.ParseInt(syncWaveStr, 10, waveNumBits); err != nil {
return nil, errors.Wrapf(err, "failed to parse sync wave %s", syncWaveStr)
}
}

for hookType := range hookTypes(obj) {
hookInfos = append(hookInfos, hookInfo{hookType: hookType, hookWave: waveNum(syncWave)})
}

return hookInfos, nil
}

// helm hook types: https://helm.sh/docs/topics/charts_hooks/
// helm to argocd map: https://argo-cd.readthedocs.io/en/stable/user-guide/helm/#helm-hooks
var helmHookToArgocdPhaseMap = map[string]argocdSyncPhase{
"crd-install": PreSyncPhase,
"pre-install": PreSyncPhase,
"pre-upgrade": PreSyncPhase,
"post-upgrade": PostSyncPhase,
"post-install": PostSyncPhase,
"post-delete": PostDeletePhase,
}

func hookTypes(obj *unstructured.Unstructured) map[argocdSyncPhase]struct{} {
types := make(map[argocdSyncPhase]struct{})
for _, text := range resource.GetAnnotationCSVs(obj, "argocd.argoproj.io/hook") {
types[argocdSyncPhase(text)] = struct{}{}
}

// we ignore Helm hooks if we have Argo hook
if len(types) == 0 {
for _, text := range resource.GetAnnotationCSVs(obj, "helm.sh/hook") {
if actualPhase := helmHookToArgocdPhaseMap[text]; actualPhase != "" {
types[actualPhase] = struct{}{}
}
}
}

return types
}
Loading

0 comments on commit 30ca037

Please sign in to comment.