Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion internal/module/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,8 @@ func mapTemplatesRules(linterSettings *pkg.LintersSettings, configSettings *conf
rules.ServicePortRule.SetLevel(globalRules.ServicePortRule.Impact, fallbackImpact)
rules.ClusterDomainRule.SetLevel(globalRules.ClusterDomainRule.Impact, fallbackImpact)
rules.RegistryRule.SetLevel(globalRules.RegistryRule.Impact, fallbackImpact)
rules.EnabledModulesRule.SetLevel(globalRules.EnabledModulesRule.Impact, fallbackImpact)

rules.WebhookConfigurationRule.SetLevel(globalRules.WebhookConfigurationRule.Impact, fallbackImpact)
}

// mapOpenAPIRules configures OpenAPI linter rules
Expand Down Expand Up @@ -455,6 +456,7 @@ func mapTemplatesExclusionsAndSettings(linterSettings *pkg.LintersSettings, conf
excludes.Ingress = configExcludes.Ingress.Get()
excludes.EnabledModules.Files = pkg.StringRuleExcludeList(configExcludes.EnabledModules.Files)
excludes.EnabledModules.Directories = pkg.DirectoryRuleExcludeList(configExcludes.EnabledModules.Directories)
excludes.WebhookConfiguration = configExcludes.WebhookConfiguration.Get()

// Additional settings
linterSettings.Templates.PrometheusRuleSettings.Disable = configSettings.Templates.PrometheusRules.Disable
Expand Down
38 changes: 20 additions & 18 deletions pkg/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,17 +131,18 @@ type TemplatesLinterConfig struct {
GrafanaDashboardsSettings GrafanaDashboardsSettings
}
type TemplatesLinterRules struct {
VPARule RuleConfig
PDBRule RuleConfig
IngressRule RuleConfig
PrometheusRule RuleConfig
GrafanaRule RuleConfig
KubeRBACProxyRule RuleConfig
ServicePortRule RuleConfig
ClusterDomainRule RuleConfig
RegistryRule RuleConfig
HTTPRouteRule RuleConfig
EnabledModulesRule RuleConfig
VPARule RuleConfig
PDBRule RuleConfig
IngressRule RuleConfig
PrometheusRule RuleConfig
GrafanaRule RuleConfig
KubeRBACProxyRule RuleConfig
ServicePortRule RuleConfig
ClusterDomainRule RuleConfig
RegistryRule RuleConfig
HTTPRouteRule RuleConfig
EnabledModulesRule RuleConfig
WebhookConfigurationRule RuleConfig
}

type PrometheusRuleSettings struct {
Expand All @@ -152,13 +153,14 @@ type GrafanaDashboardsSettings struct {
Disable bool
}
type TemplatesExcludeRules struct {
VPAAbsent KindRuleExcludeList
PDBAbsent KindRuleExcludeList
ServicePort ServicePortExcludeList
KubeRBACProxy StringRuleExcludeList
Ingress KindRuleExcludeList
HTTPRoute KindRuleExcludeList
EnabledModules EnabledModulesExcludeRule
VPAAbsent KindRuleExcludeList
PDBAbsent KindRuleExcludeList
ServicePort ServicePortExcludeList
KubeRBACProxy StringRuleExcludeList
Ingress KindRuleExcludeList
HTTPRoute KindRuleExcludeList
EnabledModules EnabledModulesExcludeRule
WebhookConfiguration KindRuleExcludeList
}

type EnabledModulesExcludeRule struct {
Expand Down
21 changes: 11 additions & 10 deletions pkg/config/global/global.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,16 +129,17 @@ type TemplatesLinterConfig struct {
}

type TemplatesLinterRules struct {
VPARule RuleConfig `mapstructure:"vpa"`
PDBRule RuleConfig `mapstructure:"pdb"`
IngressRule RuleConfig `mapstructure:"ingress"`
PrometheusRule RuleConfig `mapstructure:"prometheus-rules"`
GrafanaRule RuleConfig `mapstructure:"grafana-dashboards"`
KubeRBACProxyRule RuleConfig `mapstructure:"kube-rbac-proxy"`
ServicePortRule RuleConfig `mapstructure:"service-port"`
ClusterDomainRule RuleConfig `mapstructure:"cluster-domain"`
RegistryRule RuleConfig `mapstructure:"registry"`
EnabledModulesRule RuleConfig `mapstructure:"enabled-modules"`
VPARule RuleConfig `mapstructure:"vpa"`
PDBRule RuleConfig `mapstructure:"pdb"`
IngressRule RuleConfig `mapstructure:"ingress"`
PrometheusRule RuleConfig `mapstructure:"prometheus-rules"`
GrafanaRule RuleConfig `mapstructure:"grafana-dashboards"`
KubeRBACProxyRule RuleConfig `mapstructure:"kube-rbac-proxy"`
ServicePortRule RuleConfig `mapstructure:"service-port"`
ClusterDomainRule RuleConfig `mapstructure:"cluster-domain"`
RegistryRule RuleConfig `mapstructure:"registry"`
EnabledModulesRule RuleConfig `mapstructure:"enabled-modules"`
WebhookConfigurationRule RuleConfig `mapstructure:"webhook-configuration-annotations"`
}

func (c LinterConfig) IsWarn() bool {
Expand Down
34 changes: 18 additions & 16 deletions pkg/config/linters_settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,25 +218,27 @@ type TemplatesSettings struct {
}

type TemplatesLinterRules struct {
VPARule RuleConfig `mapstructure:"vpa"`
PDBRule RuleConfig `mapstructure:"pdb"`
IngressRule RuleConfig `mapstructure:"ingress"`
PrometheusRule RuleConfig `mapstructure:"prometheus-rules"`
GrafanaRule RuleConfig `mapstructure:"grafana-dashboards"`
KubeRBACProxyRule RuleConfig `mapstructure:"kube-rbac-proxy"`
ServicePortRule RuleConfig `mapstructure:"service-port"`
ClusterDomainRule RuleConfig `mapstructure:"cluster-domain"`
RegistryRule RuleConfig `mapstructure:"registry"`
EnabledModulesRule RuleConfig `mapstructure:"enabled-modules"`
VPARule RuleConfig `mapstructure:"vpa"`
PDBRule RuleConfig `mapstructure:"pdb"`
IngressRule RuleConfig `mapstructure:"ingress"`
PrometheusRule RuleConfig `mapstructure:"prometheus-rules"`
GrafanaRule RuleConfig `mapstructure:"grafana-dashboards"`
KubeRBACProxyRule RuleConfig `mapstructure:"kube-rbac-proxy"`
ServicePortRule RuleConfig `mapstructure:"service-port"`
ClusterDomainRule RuleConfig `mapstructure:"cluster-domain"`
RegistryRule RuleConfig `mapstructure:"registry"`
EnabledModulesRule RuleConfig `mapstructure:"enabled-modules"`
WebhookConfigurationRule RuleConfig `mapstructure:"webhook-configuration-annotations"`
}

type TemplatesExcludeRules struct {
VPAAbsent KindRuleExcludeList `mapstructure:"vpa"`
PDBAbsent KindRuleExcludeList `mapstructure:"pdb"`
ServicePort ServicePortExcludeList `mapstructure:"service-port"`
KubeRBACProxy StringRuleExcludeList `mapstructure:"kube-rbac-proxy"`
Ingress KindRuleExcludeList `mapstructure:"ingress"`
EnabledModules EnabledModulesExcludeRule `mapstructure:"enabled-modules"`
VPAAbsent KindRuleExcludeList `mapstructure:"vpa"`
PDBAbsent KindRuleExcludeList `mapstructure:"pdb"`
ServicePort ServicePortExcludeList `mapstructure:"service-port"`
KubeRBACProxy StringRuleExcludeList `mapstructure:"kube-rbac-proxy"`
Ingress KindRuleExcludeList `mapstructure:"ingress"`
EnabledModules EnabledModulesExcludeRule `mapstructure:"enabled-modules"`
WebhookConfiguration KindRuleExcludeList `mapstructure:"webhook-configuration-annotations"`
}

type EnabledModulesExcludeRule struct {
Expand Down
123 changes: 123 additions & 0 deletions pkg/linters/templates/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Proper template validation prevents runtime issues, ensures applications are pro
| [registry](#registry) | Validates registry secret configuration | ❌ | enabled |
| [werf](#werf) | Validates image names in `werf.yaml` do not contain underscores | ❌ | enabled |
| [enabled-modules](#enabled-modules) | Detects usage of `.Values.global.enabledModules` in templates | ✅ | enabled |
| [webhook-configuration-annotations](#webhook-configuration-annotations) | Checks webhook configurations have werf.io/weight or deploy-dependency annotations | ✅ | enabled |

## Rule Details

Expand Down Expand Up @@ -1850,6 +1851,110 @@ linters-settings:
- templates/vendor/ # Exclude entire directory
```

### webhook-configuration-annotations

**Purpose:** Ensures every `ValidatingWebhookConfiguration` and `MutatingWebhookConfiguration` has at least one ordering annotation: `werf.io/weight` or an annotation with the `werf.io/deploy-dependency-` prefix (e.g. `werf.io/deploy-dependency-deployment`, `werf.io/deploy-dependency-service`). These annotations control werf deploy ordering: `werf.io/deploy-dependency-*` declares a dependency on another resource (the recommended approach), while `werf.io/weight` sets explicit ordering priority.

**Description:**

Iterates all parsed Kubernetes resources, filters for webhook configuration kinds, and checks that each webhook configuration declares its position in the deploy order via annotations. Without these annotations, webhook configurations may deploy in an undefined order, potentially causing cluster API disruptions.
Comment thread
ldmonster marked this conversation as resolved.

**What it checks:**

1. Every `ValidatingWebhookConfiguration` has either `werf.io/weight` or an annotation starting with `werf.io/deploy-dependency-`
2. Every `MutatingWebhookConfiguration` has either `werf.io/weight` or an annotation starting with `werf.io/deploy-dependency-`
3. Note: `werf.io/deploy-on` alone is not sufficient — it controls deploy *stages*, not deploy *ordering*
4. Resources with neither annotation are reported as errors (configurable via `impact`, set to `warn` to downgrade)

**Why it matters:**

Webhook backing services (its Deployment, Service, etc) should be deployed before the webhook itself (MutatingWebhookConfiguration or ValidationWebhookConfiguration). Otherwise, if the module rollout was not finished properly (network issues, OOM and so on), the cluster might be left in a state where webhook is deployed, but has no backing services. And if so, resources that this webhook validates/mutates could not be created or updated anymore. To avoid this, deployment order of the webhook and its backing services must be enforced via annotations.

**Examples:**

❌ **Incorrect** - Webhook configuration without ordering annotations:

```yaml
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: my-webhook
webhooks:
- name: check.example.com
clientConfig:
service:
name: my-service
namespace: d8-my-module
rules:
- operations: ["CREATE", "UPDATE"]
apiGroups: [""]
apiVersions: ["v1"]
resources: ["pods"]
```

**Error:**
```
ValidatingWebhookConfiguration "my-webhook" must have either "werf.io/deploy-dependency" or "werf.io/weight" annotation
```

✅ **Correct** - With deploy ordering annotations:

```yaml
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: my-webhook
annotations:
werf.io/deploy-dependency-deployment: state=ready,kind=Deployment,name=my-app,namespace=d8-my-module
werf.io/deploy-dependency-service: state=present,kind=Service,name=my-svc,namespace=d8-my-module
webhooks:
- name: check.example.com
...
```

✅ **Correct** - With weight annotation only:

```yaml
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: my-webhook
annotations:
werf.io/weight: "10"
webhooks:
- name: check.example.com
...
```

**Configuration:**

The rule defaults to `warning` level and can be configured via global or module `.dmtlint.yaml`.

**Impact level** — set the severity of the check:

```yaml
# .dmtlint.yaml (global) or <module>/.dmtlint.yaml
linters-settings:
templates:
rules:
webhook-configuration-annotations:
impact: warn # error | warn (default: error)
```

**Excluding resources** — skip specific webhook configurations by kind and name:

```yaml
# .dmtlint.yaml (global) or <module>/.dmtlint.yaml
linters-settings:
templates:
exclude-rules:
webhook-configuration-annotations:
- kind: ValidatingWebhookConfiguration
name: istio-sidecar-injector # managed externally by istio operator
- kind: MutatingWebhookConfiguration
name: cert-manager-webhook # managed externally by cert-manager operator
```

## Configuration

The Templates linter can be configured at the module level with rule-specific settings and exclusions.
Expand Down Expand Up @@ -1904,6 +2009,8 @@ linters-settings:
impact: error
enabled-modules:
impact: warning
webhook-configuration-annotations:
impact: error
```

### Rule-Level Exclusions
Expand Down Expand Up @@ -1961,6 +2068,13 @@ linters-settings:
- templates/legacy-deployment.yaml
directories:
- templates/vendor/

# webhook-configuration-annotations exclusions (by kind and name)
webhook-configuration-annotations:
- kind: ValidatingWebhookConfiguration
name: istio-sidecar-injector
- kind: MutatingWebhookConfiguration
name: cert-manager-webhook
```

### Complete Configuration Example
Expand All @@ -1979,6 +2093,11 @@ linters-settings:
prometheus-rules:
disable: false

# Rule-specific impact levels
rules:
webhook-configuration-annotations:
impact: warn # downgrade from default error to warn

# Rule-specific exclusions
exclude-rules:
vpa:
Expand All @@ -2003,6 +2122,10 @@ linters-settings:

kube-rbac-proxy:
- d8-development

webhook-configuration-annotations:
- kind: ValidatingWebhookConfiguration
name: istio-sidecar-injector
```

### Configuration in Module Directory
Expand Down
88 changes: 88 additions & 0 deletions pkg/linters/templates/rules/webhook_configuration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
Copyright 2025 Flant JSC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package rules

import (
"strings"

"github.com/deckhouse/dmt/pkg"
"github.com/deckhouse/dmt/pkg/errors"
)

const (
WebhookConfigurationRuleName = "webhook-configuration-annotations"

AnnotationWeight = "werf.io/weight"
AnnotationDeployDependency = "werf.io/deploy-dependency"
AnnotationDeployDependencyPrefix = "werf.io/deploy-dependency-"
)

func NewWebhookConfigurationRule(excludeRules []pkg.KindRuleExclude) *WebhookConfigurationRule {
return &WebhookConfigurationRule{
RuleMeta: pkg.RuleMeta{
Name: WebhookConfigurationRuleName,
},
KindRule: pkg.KindRule{
ExcludeRules: excludeRules,
},
}
}

type WebhookConfigurationRule struct {
pkg.RuleMeta
pkg.KindRule
}

// hasDeployDependencyAnnotation checks whether any annotation key starts with
// "werf.io/deploy-dependency". In practice werf uses suffixed keys such as
// "werf.io/deploy-dependency-deployment" or "werf.io/deploy-dependency-service",
// so a prefix match is required instead of an exact key match.
func hasDeployDependencyAnnotation(annotations map[string]string) bool {
for key := range annotations {
if strings.HasPrefix(key, AnnotationDeployDependencyPrefix) {
return true
}
}

return false
}

func (r *WebhookConfigurationRule) ValidateWebhookConfigurationAnnotations(m pkg.Module, errorList *errors.LintRuleErrorsList) {
errorList = errorList.WithRule(r.GetName())

for _, object := range m.GetStorage() {
kind := object.Unstructured.GetKind()
if kind != "ValidatingWebhookConfiguration" && kind != "MutatingWebhookConfiguration" {
continue
}

if !r.Enabled(kind, object.Unstructured.GetName()) {
continue
}

annotations := object.Unstructured.GetAnnotations()

_, hasWeight := annotations[AnnotationWeight]
hasDeployDependency := hasDeployDependencyAnnotation(annotations)

if !hasWeight && !hasDeployDependency {
errorList.WithObjectID(object.Identity()).
WithFilePath(object.GetPath()).
Errorf("%s %q must have either %q annotation or an annotation with %q prefix", kind, object.Unstructured.GetName(), AnnotationWeight, AnnotationDeployDependency)
}
}
}
Loading
Loading