Skip to content
Merged
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ bin/
vendor/
.idea
.vscode
do-not-commit/
do-not-commit/
pkg/tools/embedded/
46 changes: 46 additions & 0 deletions .replicated.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
appId: ""
appSlug: ""
promoteToChannelIds: []
promoteToChannelNames: []
charts: [
{
path: "./chart/something",
chartVersion: "",
appVersion: "",
},
{
path: "./chart/new-chart/*",
chartVersion: "",
appVersion: "",
}
]
preflights: [
{
path: "./preflights/stuff",
valuesPath: "./chart/something", # directory to corresponding helm chart
}
]
releaseLabel: "" ## some sort of semver pattern?
manifests: ["replicated/**/*.yaml"]
repl-lint:
version: 1
linters:
helm:
disabled: false
strict: false
preflight:
disabled: false
strict: true
support-bundle:
disabled: false
strict: false
embedded-cluster:
disabled: true
strict: false
kots:
disabled: true
strict: false
tools:
helm: "3.19.0"
preflight: "0.123.9"
support-bundle: "0.123.9"
38 changes: 19 additions & 19 deletions docs/lint-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,25 @@ This defines only the minimal structure for the new linter. YAML and JSON are bo
## Format
```yaml
repl-lint:
version: 1 # lint config schema version
enabled: true # turn linting on/off
linters:
helm:
enabled: true # run helm lint
strict: false # if true, treat warnings as errors
preflight:
enabled: true
strict: true
support-bundle:
enabled: true
strict: false
embedded-cluster: # embedded cluster and kots linters do not exist as of yet
enabled: false
strict: false
kots:
enabled: false
strict: false
tools: # tool resolution (optional)
version: 1 # lint config schema version
enabled: true # turn linting on/off
linters:
helm:
enabled: true # run helm lint
strict: false # if true, treat warnings as errors
preflight:
enabled: true
strict: true
support-bundle:
enabled: true
strict: false
embedded-cluster: # embedded cluster and kots linters do not exist as of yet
enabled: false
strict: false
kots:
enabled: false
strict: false
tools: # tool resolution (optional)
```
Notes:
- Only keys listed above are recognized in this minimal spec. Unknown keys are rejected.
Expand Down
68 changes: 68 additions & 0 deletions pkg/tools/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package tools

import (
"fmt"
"os"
"path/filepath"
"runtime"
)

// GetCacheDir returns the cache directory for tools
// Location: ~/.replicated/tools
func GetCacheDir() (string, error) {
var home string

// Get home directory
if runtime.GOOS == "windows" {
home = os.Getenv("USERPROFILE")
} else {
home = os.Getenv("HOME")
}

if home == "" {
return "", fmt.Errorf("HOME environment variable not set")
}

return filepath.Join(home, ".replicated", "tools"), nil
}

// GetToolPath returns the cached path for a specific tool version
// Example: ~/.replicated/tools/helm/3.14.4/darwin-arm64/helm
func GetToolPath(name, version string) (string, error) {
cacheDir, err := GetCacheDir()
if err != nil {
return "", err
}

osArch := fmt.Sprintf("%s-%s", runtime.GOOS, runtime.GOARCH)

binaryName := name
if runtime.GOOS == "windows" {
binaryName = name + ".exe"
}

return filepath.Join(cacheDir, name, version, osArch, binaryName), nil
}

// IsCached checks if a tool version is already cached
func IsCached(name, version string) (bool, error) {
toolPath, err := GetToolPath(name, version)
if err != nil {
return false, err
}

info, err := os.Stat(toolPath)
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
}

// Make sure it's a file, not a directory
if info.IsDir() {
return false, nil
}

return true, nil
}
104 changes: 104 additions & 0 deletions pkg/tools/checksum.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package tools

import (
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
"strings"
"time"
)

// httpClient for checksum downloads with timeout
var checksumHTTPClient = &http.Client{
Timeout: 30 * time.Second,
}

// VerifyHelmChecksum verifies a Helm binary against its .sha256sum file
func VerifyHelmChecksum(data []byte, archiveURL string) error {
// Helm provides per-file checksums: <url>.sha256sum
checksumURL := archiveURL + ".sha256sum"

// Download checksum file with timeout
resp, err := checksumHTTPClient.Get(checksumURL)
if err != nil {
return fmt.Errorf("downloading checksum file: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != 200 {
return fmt.Errorf("checksum file not found (HTTP %d): %s", resp.StatusCode, checksumURL)
}

checksumData, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("reading checksum file: %w", err)
}

// Parse checksum (format: "abc123 helm-v3.14.4-darwin-arm64.tar.gz")
parts := strings.Fields(string(checksumData))
if len(parts) < 1 {
return fmt.Errorf("invalid checksum file format")
}
expectedSum := parts[0]

// Calculate actual checksum of the archive data
hash := sha256.Sum256(data)
actualSum := hex.EncodeToString(hash[:])

// Verify match
if actualSum != expectedSum {
return fmt.Errorf("checksum mismatch: got %s, want %s", actualSum, expectedSum)
}

return nil
}

// VerifyTroubleshootChecksum verifies preflight or support-bundle against checksums.txt
func VerifyTroubleshootChecksum(data []byte, version, filename string) error {
// Troubleshoot provides a single checksums file for all binaries
checksumURL := fmt.Sprintf("https://github.com/replicatedhq/troubleshoot/releases/download/v%s/troubleshoot_%s_checksums.txt", version, version)

// Download checksums file with timeout
resp, err := checksumHTTPClient.Get(checksumURL)
if err != nil {
return fmt.Errorf("downloading checksums file: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != 200 {
return fmt.Errorf("checksums file not found (HTTP %d): %s", resp.StatusCode, checksumURL)
}

checksumData, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("reading checksums file: %w", err)
}

// Find the checksum for our specific file
// Format: "abc123 preflight_darwin_all.tar.gz"
var expectedSum string
for _, line := range strings.Split(string(checksumData), "\n") {
parts := strings.Fields(line)
if len(parts) == 2 && parts[1] == filename {
expectedSum = parts[0]
break
}
}

if expectedSum == "" {
return fmt.Errorf("checksum not found for %s in checksums file", filename)
}

// Calculate actual checksum of the archive data
hash := sha256.Sum256(data)
actualSum := hex.EncodeToString(hash[:])

// Verify match
if actualSum != expectedSum {
return fmt.Errorf("checksum mismatch for %s: got %s, want %s", filename, actualSum, expectedSum)
}

return nil
}
Loading