Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
302 changes: 293 additions & 9 deletions cli/cmd/lint.go

Large diffs are not rendered by default.

571 changes: 571 additions & 0 deletions cli/cmd/lint_integration_test.go

Large diffs are not rendered by default.

85 changes: 56 additions & 29 deletions cli/cmd/lint_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,24 @@ import (

// JSONLintOutput represents the complete JSON output structure for lint results
type JSONLintOutput struct {
Metadata LintMetadata `json:"metadata"`
HelmResults *HelmLintResults `json:"helm_results,omitempty"`
PreflightResults *PreflightLintResults `json:"preflight_results,omitempty"`
SupportBundleResults *SupportBundleLintResults `json:"support_bundle_results,omitempty"`
Summary LintSummary `json:"summary"`
Images *ImageExtractResults `json:"images,omitempty"` // Only if --verbose
Metadata LintMetadata `json:"metadata"`
HelmResults *HelmLintResults `json:"helm_results,omitempty"`
PreflightResults *PreflightLintResults `json:"preflight_results,omitempty"`
SupportBundleResults *SupportBundleLintResults `json:"support_bundle_results,omitempty"`
EmbeddedClusterResults *EmbeddedClusterLintResults `json:"embedded_cluster_results,omitempty"`
Summary LintSummary `json:"summary"`
Images *ImageExtractResults `json:"images,omitempty"` // Only if --verbose
}

// LintMetadata contains execution context and environment information
type LintMetadata struct {
Timestamp string `json:"timestamp"`
ConfigFile string `json:"config_file"`
HelmVersion string `json:"helm_version,omitempty"`
PreflightVersion string `json:"preflight_version,omitempty"`
SupportBundleVersion string `json:"support_bundle_version,omitempty"`
CLIVersion string `json:"cli_version"`
Timestamp string `json:"timestamp"`
ConfigFile string `json:"config_file"`
HelmVersion string `json:"helm_version,omitempty"`
PreflightVersion string `json:"preflight_version,omitempty"`
SupportBundleVersion string `json:"support_bundle_version,omitempty"`
EmbeddedClusterVersion string `json:"embedded_cluster_version,omitempty"`
CLIVersion string `json:"cli_version"`
}

// HelmLintResults contains all Helm chart lint results
Expand Down Expand Up @@ -57,7 +59,7 @@ type PreflightLintResult struct {

// SupportBundleLintResults contains all Support Bundle spec lint results
type SupportBundleLintResults struct {
Enabled bool `json:"enabled"`
Enabled bool `json:"enabled"`
Specs []SupportBundleLintResult `json:"specs"`
}

Expand All @@ -69,6 +71,20 @@ type SupportBundleLintResult struct {
Summary ResourceSummary `json:"summary"`
}

// EmbeddedClusterLintResults contains all Embedded Cluster config lint results
type EmbeddedClusterLintResults struct {
Enabled bool `json:"enabled"`
Configs []EmbeddedClusterLintResult `json:"configs"`
}

// EmbeddedClusterLintResult represents lint results for a single Embedded Cluster config
type EmbeddedClusterLintResult struct {
Path string `json:"path"`
Success bool `json:"success"`
Messages []LintMessage `json:"messages"`
Summary ResourceSummary `json:"summary"`
}

// LintMessage represents a single lint issue (wraps lint2.LintMessage with JSON tags)
type LintMessage struct {
Severity string `json:"severity"` // ERROR, WARNING, INFO
Expand Down Expand Up @@ -119,6 +135,9 @@ type ExtractedPaths struct {
// Support bundles: simple paths
SupportBundles []string

// Embedded cluster: simple paths
EmbeddedClusterPaths []string

// Shared: HelmChart manifests (used by preflight + image extraction)
HelmChartManifests map[string]*lint2.HelmChartManifest

Expand All @@ -129,6 +148,7 @@ type ExtractedPaths struct {
HelmVersion string
PreflightVersion string
SBVersion string
ECVersion string

// Metadata
ConfigPath string
Expand All @@ -144,23 +164,29 @@ type LintableResult interface {
}

// Implement LintableResult interface for ChartLintResult
func (c ChartLintResult) GetPath() string { return c.Path }
func (c ChartLintResult) GetSuccess() bool { return c.Success }
func (c ChartLintResult) GetMessages() []LintMessage { return c.Messages }
func (c ChartLintResult) GetPath() string { return c.Path }
func (c ChartLintResult) GetSuccess() bool { return c.Success }
func (c ChartLintResult) GetMessages() []LintMessage { return c.Messages }
func (c ChartLintResult) GetSummary() ResourceSummary { return c.Summary }

// Implement LintableResult interface for PreflightLintResult
func (p PreflightLintResult) GetPath() string { return p.Path }
func (p PreflightLintResult) GetSuccess() bool { return p.Success }
func (p PreflightLintResult) GetMessages() []LintMessage { return p.Messages }
func (p PreflightLintResult) GetPath() string { return p.Path }
func (p PreflightLintResult) GetSuccess() bool { return p.Success }
func (p PreflightLintResult) GetMessages() []LintMessage { return p.Messages }
func (p PreflightLintResult) GetSummary() ResourceSummary { return p.Summary }

// Implement LintableResult interface for SupportBundleLintResult
func (s SupportBundleLintResult) GetPath() string { return s.Path }
func (s SupportBundleLintResult) GetSuccess() bool { return s.Success }
func (s SupportBundleLintResult) GetMessages() []LintMessage { return s.Messages }
func (s SupportBundleLintResult) GetPath() string { return s.Path }
func (s SupportBundleLintResult) GetSuccess() bool { return s.Success }
func (s SupportBundleLintResult) GetMessages() []LintMessage { return s.Messages }
func (s SupportBundleLintResult) GetSummary() ResourceSummary { return s.Summary }

// Implement LintableResult interface for EmbeddedClusterLintResult
func (e EmbeddedClusterLintResult) GetPath() string { return e.Path }
func (e EmbeddedClusterLintResult) GetSuccess() bool { return e.Success }
func (e EmbeddedClusterLintResult) GetMessages() []LintMessage { return e.Messages }
func (e EmbeddedClusterLintResult) GetSummary() ResourceSummary { return e.Summary }

// Helper functions to convert between types

// convertLint2Messages converts lint2.LintMessage slice to LintMessage slice
Expand Down Expand Up @@ -193,13 +219,14 @@ func calculateResourceSummary(messages []lint2.LintMessage) ResourceSummary {
}

// newLintMetadata creates metadata for the lint output
func newLintMetadata(configFile, helmVersion, preflightVersion, supportBundleVersion, cliVersion string) LintMetadata {
func newLintMetadata(configFile, helmVersion, preflightVersion, supportBundleVersion, embeddedClusterVersion, cliVersion string) LintMetadata {
return LintMetadata{
Timestamp: time.Now().UTC().Format(time.RFC3339),
ConfigFile: configFile,
HelmVersion: helmVersion,
PreflightVersion: preflightVersion,
SupportBundleVersion: supportBundleVersion,
CLIVersion: cliVersion,
Timestamp: time.Now().UTC().Format(time.RFC3339),
ConfigFile: configFile,
HelmVersion: helmVersion,
PreflightVersion: preflightVersion,
SupportBundleVersion: supportBundleVersion,
EmbeddedClusterVersion: embeddedClusterVersion,
CLIVersion: cliVersion,
}
}
5 changes: 5 additions & 0 deletions cli/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,11 @@ func Execute(rootCmd *cobra.Command, stdin io.Reader, stdout io.Writer, stderr i
if debugFlag {
fmt.Fprintf(os.Stderr, "[DEBUG] Platform API origin: %s\n", platformOrigin)
}

// Propagate auth and origin to environment so child tools (e.g., embedded-cluster lint)
// can make vendor portal API calls without additional flags
_ = os.Setenv("REPLICATED_API_TOKEN", apiToken)
_ = os.Setenv("REPLICATED_API_ORIGIN", platformOrigin)
}

// allow override
Expand Down
153 changes: 151 additions & 2 deletions docs/lint-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ repl-lint:
support-bundle:
enabled: true
strict: false
embedded-cluster: # embedded cluster and kots linters do not exist as of yet
embedded-cluster: # embedded cluster linter validates EC config files
enabled: false
strict: false
kots:
kots: # kots linter does not exist as of yet
enabled: false
strict: false
tools: # tool resolution (optional)
Expand Down Expand Up @@ -297,3 +297,152 @@ spec:
**Problem:** Warning about orphaned HelmChart manifest

**Solution:** Either add the corresponding chart to your configuration or remove the unused HelmChart manifest. Warnings are informational and won't cause linting to fail.

## Embedded Cluster Configuration

The embedded-cluster linter validates Embedded Cluster configuration files. Embedded Cluster is Replicated's solution for packaging Kubernetes and your application together as a single appliance-style installer.

### What It Validates

The embedded-cluster linter validates:
- Configuration schema correctness
- Kubernetes version compatibility
- Extension configurations
- Network and storage settings
- High availability settings
- Custom branding configuration

### Platform Requirements

**Important:** The embedded-cluster linter binary is currently only available for **Linux AMD64**.

- ✅ **Linux (x86_64/amd64)**: Full support
- ❌ **macOS**: Not currently available
- ❌ **Windows**: Not currently available
- ❌ **ARM64**: Not currently available

If you're on an unsupported platform:
- The linter will fail gracefully with a clear error message
- Other linters (helm, preflight, support-bundle) will continue running
- Consider running in a Linux container or CI environment

### Configuration

Enable the embedded-cluster linter in your `.replicated` file:

```yaml
repl-lint:
version: 1
linters:
embedded-cluster:
enabled: true
strict: false # Set to true to treat warnings as errors
tools:
embedded-cluster: "latest" # Optional: pin to specific version
```

### Auto-Discovery

When no `.replicated` config exists, the linter automatically discovers embedded-cluster configs by:
- Finding files with `kind: Config` and `apiVersion: embeddedcluster.replicated.com/v1beta1`
- Validating only 0 or 1 config exists (multiple configs are not supported)

### Multiple Config Validation

**Only 0 or 1 embedded cluster config is allowed per project.**

If multiple configs are detected:
- Each config will show as failed with a clear error message
- Other linters (helm, preflight, support-bundle) continue running
- The linting command returns a non-zero exit code

### Example Configuration

**Minimal `.replicated` with embedded-cluster:**
```yaml
appId: ""
appSlug: "my-app"
manifests:
- "./embedded-cluster/*.yaml"

repl-lint:
version: 1
linters:
embedded-cluster:
enabled: true
strict: true
tools:
embedded-cluster: "latest"
```

**Example embedded-cluster config file (ec-config.yaml):**
```yaml
apiVersion: embeddedcluster.replicated.com/v1beta1
kind: Config
metadata:
name: my-app-config
spec:
version: "1.33+k8s-1.33"
roles:
controller:
name: "Controller"
description: "Kubernetes controller node"

extensions:
helm:
repositories:
- name: my-repo
url: https://charts.example.com

network:
podCIDR: "10.244.0.0/16"
serviceCIDR: "10.96.0.0/12"

unsupportedOverrides:
k0s: |
apiVersion: k0s.k0sproject.io/v1beta1
kind: ClusterConfig
spec:
network:
provider: calico
```

### Output Format

The embedded-cluster linter returns JSON output with structured validation results:

```json
{
"files": [
{
"path": "embedded-cluster/config.yaml",
"valid": true,
"errors": [],
"warnings": [],
"infos": []
}
]
}
```

**Validation Messages:**
- **errors**: Schema violations, invalid values, unsupported configurations
- **warnings**: Deprecated fields, potential issues, best practice violations
- **infos**: Informational messages about configuration choices

### Troubleshooting

**Problem:** "embedded-cluster binaries are only available for linux-amd64"

**Solution:**
- Run linting in a Linux environment or Docker container
- Use CI/CD on Linux runners
- The error won't block other linters from running

**Problem:** "Multiple embedded cluster configs found"

**Solution:** Only one embedded cluster config is allowed per project. Remove duplicate configs or move them to separate projects.

**Problem:** Validation errors about Kubernetes version

**Solution:** Ensure the `spec.version` field uses a supported Kubernetes version. Check the embedded-cluster documentation for supported versions.
4 changes: 2 additions & 2 deletions examples/.replicated.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ charts: [
preflights: [
{
path: "./preflights/**",
chartName: "helm-chart",
chartVersion: "1.0.0",
}
]
releaseLabel: "" ## some sort of semver pattern?
Expand All @@ -27,6 +25,8 @@ repl-lint:
disabled: false
support-bundle:
disabled: false
embedded-cluster:
disabled: false
tools:
helm: "latest"
preflight: "latest"
Expand Down
Loading
Loading