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
1 change: 1 addition & 0 deletions internal/module/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,7 @@ func mapSimpleLinterRules(linterSettings *pkg.LintersSettings, configSettings *c
linterSettings.OpenAPI.Rules.HARule.SetLevel("", openAPIImpact)
linterSettings.OpenAPI.Rules.CRDsRule.SetLevel("", openAPIImpact)
linterSettings.OpenAPI.Rules.KeysRule.SetLevel("", openAPIImpact)
linterSettings.OpenAPI.Rules.BilingualRule.SetLevel("", openAPIImpact)

// RBAC rules
rbacImpact := configSettings.Rbac.Impact
Expand Down
9 changes: 5 additions & 4 deletions pkg/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,11 @@ type OpenAPILinterConfig struct {
ExcludeRules OpenAPIExcludeRules
}
type OpenAPILinterRules struct {
EnumRule RuleConfig
HARule RuleConfig
CRDsRule RuleConfig
KeysRule RuleConfig
EnumRule RuleConfig
HARule RuleConfig
CRDsRule RuleConfig
KeysRule RuleConfig
BilingualRule RuleConfig
}

type OpenAPIExcludeRules struct {
Expand Down
130 changes: 130 additions & 0 deletions pkg/linters/openapi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Proper OpenAPI schema validation is critical for module configuration, ensuring
| [high-availability](#high-availability) | Validates highAvailability field has no default value | ✅ | enabled |
| [keys](#keys) | Validates property names don't use banned names | ✅ | enabled |
| [deckhouse-crds](#deckhouse-crds) | Validates Deckhouse CRD structure and metadata | ✅ | enabled |
| [bilingual](#bilingual) | Validates translation files (`doc-ru-`) exist for OpenAPI and CRD files | ✅ | enabled |

## Rule Details

Expand Down Expand Up @@ -606,6 +607,90 @@ linters-settings:

**Note:** Only CRDs with `deckhouse.io` in their name are validated by this rule. Third-party CRDs are automatically skipped.

### bilingual

**Purpose:** Ensures that OpenAPI schema files and CRD files have corresponding Russian translation files (`doc-ru-` prefix). This maintains bilingual documentation for all resource definitions, which is required for proper documentation generation in Deckhouse modules.

**Description:**

Validates that every resource file in `openapi/` and `crds/` directories has a corresponding translation file with the `doc-ru-` prefix. Also checks for orphaned translation files that have no corresponding base file.

**What it checks:**

1. For each YAML file in `openapi/` (except `values.yaml` and test files): checks that a `doc-ru-` prefixed counterpart exists in the same directory
2. For each YAML file in `crds/` (except test files): checks that a `doc-ru-` prefixed counterpart exists in the same directory
3. For each `doc-ru-` file: checks that the corresponding base file exists (detects orphaned translations)

**Why it matters:**

1. **Bilingual Documentation**: Deckhouse modules require documentation in both English and Russian
2. **Documentation Completeness**: Missing translations lead to incomplete module documentation
3. **Consistency**: Keeps resource files and their translations in sync
4. **Orphan Detection**: Finds stale translation files that no longer have a base resource

**Examples:**

❌ **Incorrect** - Missing translation for OpenAPI config:

```
openapi/
config-values.yaml # ❌ No doc-ru- counterpart
values.yaml # Skipped (internal values)
```

**Error:**
```
Error: translation file is missing: expected "doc-ru-config-values.yaml" in the same directory
```

❌ **Incorrect** - Missing translation for CRD:

```
crds/
my-resource.yaml # ❌ No doc-ru- counterpart
```

**Error:**
```
Error: translation file is missing: expected "doc-ru-my-resource.yaml" in the same directory
```

❌ **Incorrect** - Orphaned translation file:

```
crds/
doc-ru-old-resource.yaml # ❌ No base file (old-resource.yaml is missing)
```

**Error:**
```
Error: translation file has no corresponding base file: expected "old-resource.yaml"
```

✅ **Correct** - All files have translations:

```
openapi/
config-values.yaml # ✅ Has doc-ru- counterpart
doc-ru-config-values.yaml # ✅ Has base file
values.yaml # Skipped (internal values)

crds/
my-resource.yaml # ✅ Has doc-ru- counterpart
doc-ru-my-resource.yaml # ✅ Has base file
```

**Configuration:**

```yaml
# .dmt.yaml
linters-settings:
openapi:
impact: error # Controls the impact level for all openapi rules including bilingual
```

---

## Configuration

The OpenAPI linter can be configured at the module level with rule-specific exclusions.
Expand Down Expand Up @@ -981,6 +1066,51 @@ Error: CRD contains "deprecated" key at path "spec.versions[].schema.openAPIV3Sc
description: Replacement for the deprecated oldField
```

### Issue: Missing translation file for OpenAPI or CRD

**Symptom:**
```
Error: translation file is missing: expected "doc-ru-config-values.yaml" in the same directory
```

**Cause:** A resource file in `openapi/` or `crds/` directory doesn't have a corresponding `doc-ru-` translation file.

**Solutions:**

1. **Create the translation file:**

```bash
# For openapi files
cp openapi/config-values.yaml openapi/doc-ru-config-values.yaml
# Edit doc-ru-config-values.yaml to add Russian descriptions

# For CRD files
cp crds/my-resource.yaml crds/doc-ru-my-resource.yaml
# Edit doc-ru-my-resource.yaml to add Russian descriptions
```

2. **Translation file naming convention:**

| Base file | Translation file |
|-----------|-----------------|
| `config-values.yaml` | `doc-ru-config-values.yaml` |
| `instance_class.yaml` | `doc-ru-instance_class.yaml` |
| `crds/my-resource.yaml` | `crds/doc-ru-my-resource.yaml` |

### Issue: Orphaned translation file

**Symptom:**
```
Error: translation file has no corresponding base file: expected "old-resource.yaml"
```

**Cause:** A `doc-ru-` translation file exists without a corresponding base resource file. This typically happens when a resource file was renamed or deleted but its translation was not.

**Solutions:**

1. **Remove the orphaned translation file** if the base resource was intentionally deleted
2. **Rename the translation file** to match the new base file name

### Issue: Enum validation in complex nested structures

**Symptom:**
Expand Down
55 changes: 55 additions & 0 deletions pkg/linters/openapi/openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,34 @@ func (o *OpenAPI) Run(m *module.Module) {
keyValidator.Run(file, errorLists)
crdValidator.Run(m.GetName(), file, errorLists)
}

// bilingual check: ensure doc-ru- translation files exist
bilingualErrorList := errorLists.WithMaxLevel(o.cfg.Rules.BilingualRule.GetLevel())
bilingualValidator := rules.NewBilingualRule(o.cfg, m.GetPath())

// check openAPI files have translations (excluding values.yaml)
for _, file := range openAPIFiles {
if isValuesFile(file) {
continue
}
bilingualValidator.Run(file, bilingualErrorList)
}

// check CRD files have translations
for _, file := range crdFiles {
bilingualValidator.Run(file, bilingualErrorList)
}

// check orphaned doc-ru- files (translation without base file)
docRuOpenAPIFiles := fsutils.GetFiles(m.GetPath(), true, filterDocRuOpenAPIFiles)
for _, file := range docRuOpenAPIFiles {
bilingualValidator.Run(file, bilingualErrorList)
}

docRuCRDFiles := fsutils.GetFiles(m.GetPath(), true, filterDocRuCRDFiles)
for _, file := range docRuCRDFiles {
bilingualValidator.Run(file, bilingualErrorList)
}
}

func (o *OpenAPI) Name() string {
Expand Down Expand Up @@ -107,3 +135,30 @@ func filterCRDsfiles(rootPath, path string) bool {

return crdsYamlRegex.MatchString(path)
}

func isValuesFile(path string) bool {
filename := filepath.Base(path)
return filename == "values.yaml" || filename == "values.yml"
}

func filterDocRuOpenAPIFiles(rootPath, path string) bool {
relPath := fsutils.Rel(rootPath, path)
filename := filepath.Base(relPath)

if !strings.HasPrefix(filename, "doc-ru-") {
return false
}

return openapiYamlRegex.MatchString(relPath)
}

func filterDocRuCRDFiles(rootPath, path string) bool {
relPath := fsutils.Rel(rootPath, path)
filename := filepath.Base(relPath)

if !strings.HasPrefix(filename, "doc-ru-") {
return false
}

return crdsYamlRegex.MatchString(relPath)
}
74 changes: 74 additions & 0 deletions pkg/linters/openapi/rules/bilingual.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
Copyright 2026 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 (
"fmt"
"os"
"path/filepath"
"strings"

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

const docRuPrefix = "doc-ru-"

type BilingualRule struct {
pkg.RuleMeta
rootPath string
}

func NewBilingualRule(_ *pkg.OpenAPILinterConfig, rootPath string) *BilingualRule {
return &BilingualRule{
RuleMeta: pkg.RuleMeta{
Name: "bilingual",
},
rootPath: rootPath,
}
}

// Run checks that the given resource file has a corresponding doc-ru- translation file.
func (r *BilingualRule) Run(path string, errorList *errors.LintRuleErrorsList) {
errorList = errorList.WithRule(r.GetName())

shortPath := fsutils.Rel(r.rootPath, path)
filename := filepath.Base(path)
dir := filepath.Dir(path)

if strings.HasPrefix(filename, docRuPrefix) {
// For doc-ru- files, check that the base file exists
baseFilename := strings.TrimPrefix(filename, docRuPrefix)
basePath := filepath.Join(dir, baseFilename)

if _, err := os.Stat(basePath); os.IsNotExist(err) {
errorList.WithFilePath(shortPath).
Errorf("translation file has no corresponding base file: expected %q", baseFilename)
}

return
}

// For base files, check that the doc-ru- counterpart exists
docRuPath := filepath.Join(dir, docRuPrefix+filename)

if _, err := os.Stat(docRuPath); os.IsNotExist(err) {
errorList.WithFilePath(shortPath).
Errorf("translation file is missing: expected %q in the same directory", fmt.Sprintf("%s%s", docRuPrefix, filename))
}
}
Loading
Loading