Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

return to hiding hooks in the diff, render seperately #185

Merged
merged 10 commits into from
Apr 11, 2024
Merged
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
Loading