Skip to content

Commit 2a4bea9

Browse files
Linter (#3140)
* added embedded cluster lint command for replicated cli * added basic yaml syntax checking * Create README.md * addressing bugbot concerns * linter failure fix * added json output format * refactored linter for new custom domain unit tests * sanitize error
1 parent 1599091 commit 2a4bea9

15 files changed

+2101
-1
lines changed

cmd/installer/cli/lint.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package cli
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"os"
8+
9+
"github.com/replicatedhq/embedded-cluster/pkg/lint"
10+
"github.com/spf13/cobra"
11+
)
12+
13+
// LintCmd creates a hidden command for linting embedded cluster configuration files
14+
func LintCmd(ctx context.Context) *cobra.Command {
15+
var verbose bool
16+
var outputFormat string
17+
18+
cmd := &cobra.Command{
19+
Use: "lint [flags] [file...]",
20+
Short: "Lint embedded cluster configuration files",
21+
Hidden: true, // Hidden command as requested
22+
Long: `Lint embedded cluster configuration files to validate:
23+
- YAML syntax (duplicate keys, unclosed quotes, invalid structure)
24+
- Port specifications in unsupportedOverrides that are already supported
25+
- Custom domains against the Replicated app's configured domains
26+
27+
Environment variables (optional for custom domain validation):
28+
- REPLICATED_API_TOKEN: Authentication token for Replicated API
29+
- REPLICATED_API_ORIGIN: API endpoint (e.g., https://api.replicated.com/vendor)
30+
- REPLICATED_APP: Application ID or slug`,
31+
Args: cobra.MinimumNArgs(1),
32+
RunE: func(cmd *cobra.Command, args []string) error {
33+
// Validate output format
34+
if outputFormat != "text" && outputFormat != "json" {
35+
return fmt.Errorf("invalid output format %q: must be 'text' or 'json'", outputFormat)
36+
}
37+
38+
// Get environment variables
39+
apiToken := os.Getenv("REPLICATED_API_TOKEN")
40+
apiOrigin := os.Getenv("REPLICATED_API_ORIGIN")
41+
appID := os.Getenv("REPLICATED_APP")
42+
43+
// Create validator with verbose flag
44+
validator := lint.NewValidator(apiToken, apiOrigin, appID)
45+
validator.SetVerbose(verbose && outputFormat != "json") // Disable verbose in JSON mode
46+
47+
// For JSON output, accumulate all results
48+
var jsonResults lint.JSONOutput
49+
hasErrors := false
50+
51+
// Validate each file
52+
for _, file := range args {
53+
if outputFormat != "json" {
54+
fmt.Printf("Linting %s...\n", file)
55+
}
56+
57+
result, err := validator.ValidateFile(file)
58+
if err != nil {
59+
if outputFormat == "json" {
60+
// Add as a file with error
61+
jsonResults.Files = append(jsonResults.Files, lint.FileResult{
62+
Path: file,
63+
Valid: false,
64+
Errors: []lint.ValidationIssue{{Field: "", Message: err.Error()}},
65+
})
66+
} else {
67+
fmt.Fprintf(os.Stderr, "ERROR: Failed to validate %s: %v\n", file, err)
68+
}
69+
hasErrors = true
70+
continue
71+
}
72+
73+
if outputFormat == "json" {
74+
// Add to JSON results
75+
jsonResults.Files = append(jsonResults.Files, result.ToJSON(file))
76+
if len(result.Errors) > 0 {
77+
hasErrors = true
78+
}
79+
} else {
80+
// Display warnings
81+
for _, warning := range result.Warnings {
82+
fmt.Fprintf(os.Stderr, "WARNING: %s: %s\n", file, warning)
83+
}
84+
85+
// Display errors
86+
for _, validationErr := range result.Errors {
87+
fmt.Fprintf(os.Stderr, "ERROR: %s: %s\n", file, validationErr)
88+
hasErrors = true
89+
}
90+
91+
// Display result
92+
if len(result.Errors) == 0 && len(result.Warnings) == 0 {
93+
fmt.Printf("✓ %s passed validation\n", file)
94+
} else if len(result.Errors) == 0 && len(result.Warnings) > 0 {
95+
fmt.Printf("⚠ %s passed with %d warning(s)\n", file, len(result.Warnings))
96+
} else {
97+
fmt.Printf("✗ %s failed validation\n", file)
98+
}
99+
}
100+
}
101+
102+
// Output JSON if requested
103+
if outputFormat == "json" {
104+
output, err := json.MarshalIndent(jsonResults, "", " ")
105+
if err != nil {
106+
return fmt.Errorf("failed to marshal JSON output: %w", err)
107+
}
108+
fmt.Println(string(output))
109+
}
110+
111+
// Only fail if there are errors (not warnings)
112+
if hasErrors {
113+
return fmt.Errorf("validation failed with errors")
114+
}
115+
116+
return nil
117+
},
118+
}
119+
120+
cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose output showing API endpoints and detailed information")
121+
cmd.Flags().StringVarP(&outputFormat, "output", "o", "text", "Output format: text or json")
122+
123+
return cmd
124+
}

cmd/installer/cli/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ func RootCmd(ctx context.Context) *cobra.Command {
121121
cmd.AddCommand(RestoreCmd(ctx, appSlug, appTitle))
122122
cmd.AddCommand(AdminConsoleCmd(ctx, appTitle))
123123
cmd.AddCommand(SupportBundleCmd(ctx))
124+
cmd.AddCommand(LintCmd(ctx))
124125

125126
return cmd
126127
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ require (
5353
go.yaml.in/yaml/v3 v3.0.4
5454
golang.org/x/crypto v0.43.0
5555
golang.org/x/term v0.36.0
56+
gopkg.in/yaml.v2 v2.4.0
5657
gotest.tools v2.2.0+incompatible
5758
helm.sh/helm/v3 v3.19.0
5859
k8s.io/api v0.34.1
@@ -346,7 +347,6 @@ require (
346347
google.golang.org/protobuf v1.36.10 // indirect
347348
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
348349
gopkg.in/inf.v0 v0.9.1 // indirect
349-
gopkg.in/yaml.v2 v2.4.0 // indirect
350350
gopkg.in/yaml.v3 v3.0.1 // indirect
351351
k8s.io/apiserver v0.34.1 // indirect
352352
k8s.io/component-base v0.34.1 // indirect

pkg/lint/README.md

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
# Embedded Cluster Lint Package
2+
3+
A hidden lint command for validating Embedded Cluster configuration files.
4+
5+
## Purpose
6+
7+
Validates Embedded Cluster YAML configuration files to catch common issues:
8+
- **YAML syntax errors** (duplicate keys, unclosed quotes, invalid structure)
9+
- **Port configuration issues** (ports in unsupportedOverrides that are already supported)
10+
- **Custom domain validation** (domains must exist in the Replicated app)
11+
12+
## Usage
13+
14+
```bash
15+
# Basic usage
16+
embedded-cluster lint config.yaml
17+
18+
# Multiple files
19+
embedded-cluster lint config1.yaml config2.yaml config3.yaml
20+
21+
# Verbose mode (shows API endpoints and configuration)
22+
embedded-cluster lint -v config.yaml
23+
24+
# JSON output (for CI/CD and scripting)
25+
embedded-cluster lint -o json config.yaml
26+
embedded-cluster lint --output json config.yaml # Long form
27+
28+
# With custom domain validation enabled
29+
REPLICATED_API_TOKEN="your-token" \
30+
REPLICATED_API_ORIGIN="https://api.replicated.com/vendor" \
31+
REPLICATED_APP="your-app-id" \
32+
embedded-cluster lint config.yaml
33+
34+
# JSON output with custom domain validation
35+
REPLICATED_API_TOKEN="your-token" \
36+
REPLICATED_API_ORIGIN="https://api.replicated.com/vendor" \
37+
REPLICATED_APP="your-app-id" \
38+
embedded-cluster lint -o json config.yaml
39+
```
40+
41+
## Validation Rules
42+
43+
### 1. YAML Syntax Validation (ERROR)
44+
Validates basic YAML syntax before attempting content validation.
45+
46+
**Catches:**
47+
- Duplicate keys
48+
- Unclosed quotes
49+
- Invalid indentation
50+
- Tabs mixed with spaces
51+
- Malformed YAML structures
52+
53+
**Example:**
54+
```
55+
ERROR: Failed to validate config.yaml: YAML syntax error at line 6: key "version" already set in map
56+
```
57+
58+
### 2. Port Range Validation (WARNING)
59+
Checks if ports specified in `unsupportedOverrides` are within the default supported range (80-32767).
60+
61+
**Why it matters:** Ports in this range don't need to be in unsupportedOverrides - they're already supported by default.
62+
63+
**Example:**
64+
```
65+
WARNING: config.yaml: unsupportedOverrides.builtInExtensions[adminconsole].service.nodePort: port 30000 is already supported (supported range: 80-32767) and should not be in unsupportedOverrides
66+
```
67+
68+
### 3. Custom Domain Validation (ERROR)
69+
When environment variables are provided, validates that custom domains exist in the app's configuration.
70+
71+
**Requires all three environment variables:**
72+
- `REPLICATED_API_TOKEN` - Authentication token for Replicated API
73+
- `REPLICATED_API_ORIGIN` - API endpoint (e.g., `https://api.replicated.com/vendor`)
74+
- `REPLICATED_APP` - Application ID or slug
75+
76+
**If any are missing:** Shows informative message and skips domain validation.
77+
78+
**Example:**
79+
```
80+
ERROR: config.yaml: domains.replicatedAppDomain: custom domain "invalid.example.com" not found in app's configured domains
81+
```
82+
83+
## Exit Codes
84+
85+
- **0**: Validation passed (may have warnings)
86+
- **1**: Validation failed (has errors)
87+
88+
Warnings do NOT cause a non-zero exit code.
89+
90+
## JSON Output
91+
92+
Use the `-o json` or `--output json` flag for machine-parseable output:
93+
94+
```bash
95+
embedded-cluster lint -o json config.yaml
96+
embedded-cluster lint --output json config.yaml
97+
```
98+
99+
### JSON Format
100+
101+
```json
102+
{
103+
"files": [
104+
{
105+
"path": "config.yaml",
106+
"valid": true,
107+
"warnings": [
108+
{
109+
"field": "unsupportedOverrides.builtInExtensions[adminconsole].service.nodePort",
110+
"message": "port 8080 is already supported (supported range: 80-32767)"
111+
}
112+
]
113+
}
114+
]
115+
}
116+
```
117+
118+
### JSON Fields
119+
120+
- `files[]` - Array of file results
121+
- `path` - File path
122+
- `valid` - `true` if no errors (warnings don't affect this)
123+
- `errors[]` - Array of validation errors (if any)
124+
- `field` - YAML path to the problematic field
125+
- `message` - Error description
126+
- `warnings[]` - Array of validation warnings (if any)
127+
- `field` - YAML path to the field
128+
- `message` - Warning description
129+
130+
### CI/CD Integration
131+
132+
```bash
133+
# Example: Fail CI build on errors, allow warnings
134+
if ! embedded-cluster lint -o json config.yaml > results.json 2>&1; then
135+
echo "Validation failed with errors"
136+
cat results.json | jq '.files[].errors'
137+
exit 1
138+
fi
139+
140+
# Check if there are any warnings
141+
if cat results.json | jq -e '.files[].warnings | length > 0' > /dev/null; then
142+
echo "::warning::Lint warnings found"
143+
cat results.json | jq '.files[].warnings'
144+
fi
145+
```
146+
147+
## Verbose Mode
148+
149+
Use the `-v` flag to see detailed information:
150+
151+
```bash
152+
embedded-cluster lint -v config.yaml
153+
```
154+
155+
**Shows:**
156+
- Environment configuration (token shown as `<set>`)
157+
- API endpoints being called
158+
- HTTP response status codes
159+
- Custom domains found
160+
- Fallback attempts to alternate endpoints
161+
162+
**Example output:**
163+
```
164+
Environment configuration:
165+
REPLICATED_API_ORIGIN: https://api.replicated.com/vendor
166+
REPLICATED_APP: my-app
167+
REPLICATED_API_TOKEN: <set>
168+
Starting custom domain validation
169+
Fetching channels from: https://api.replicated.com/vendor/v3/app/my-app/channels
170+
Attempting to fetch custom domains from: https://api.replicated.com/vendor/v3/app/my-app/custom-hostnames
171+
Response status: 200 200 OK
172+
```
173+
174+
## Testing
175+
176+
### Run all tests
177+
```bash
178+
go test ./pkg/lint/... -v
179+
```
180+
181+
### Run specific test suites
182+
```bash
183+
# YAML syntax validation
184+
go test ./pkg/lint/... -run TestValidateYAMLSyntax
185+
186+
# Port validation
187+
go test ./pkg/lint/... -run TestValidatePorts
188+
189+
# API client
190+
go test ./pkg/lint/... -run TestAPIClient
191+
```
192+
193+
### Test with example specs
194+
```bash
195+
# Test syntax errors
196+
./bin/embedded-cluster lint ./pkg/lint/testdata/specs/syntax-error-duplicate-key.yaml
197+
198+
# Test port warnings
199+
./bin/embedded-cluster lint ./pkg/lint/testdata/specs/01-warning-port-in-range.yaml
200+
201+
# Test valid configuration
202+
./bin/embedded-cluster lint ./pkg/lint/testdata/specs/04-valid-ports-outside-range.yaml
203+
```
204+
205+
## Package Structure
206+
207+
- `validator.go` - Core validation logic
208+
- `validator_test.go` - Validation tests
209+
- `api_client.go` - Replicated API client for custom domain fetching
210+
- `api_client_test.go` - API client tests
211+
- `testdata/specs/` - Example YAML files for testing
212+
213+
## Development
214+
215+
The lint command is currently **hidden** (not shown in `--help` output). To make it visible, modify `cmd/installer/cli/lint.go` and set `Hidden: false`.
216+
217+
### Adding New Validation Rules
218+
219+
1. Add validation function to `validator.go`
220+
2. Call it from `ValidateFile()`
221+
3. Return warnings or errors as appropriate
222+
4. Add test cases to `validator_test.go`
223+
5. Create example spec files in `testdata/specs/`

0 commit comments

Comments
 (0)