mirror of
https://github.com/replicatedhq/troubleshoot.git
synced 2026-04-15 07:16:34 +00:00
* created roadmap and yaml claude agent * Update roadmap.md * chore(deps): bump sigstore/cosign-installer from 3.9.2 to 3.10.0 (#1857) Bumps [sigstore/cosign-installer](https://github.com/sigstore/cosign-installer) from 3.9.2 to 3.10.0. - [Release notes](https://github.com/sigstore/cosign-installer/releases) - [Commits](https://github.com/sigstore/cosign-installer/compare/v3.9.2...v3.10.0) --- updated-dependencies: - dependency-name: sigstore/cosign-installer dependency-version: 3.10.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump the security group with 2 updates (#1858) Bumps the security group with 2 updates: [github.com/vmware-tanzu/velero](https://github.com/vmware-tanzu/velero) and [helm.sh/helm/v3](https://github.com/helm/helm). Updates `github.com/vmware-tanzu/velero` from 1.16.2 to 1.17.0 - [Release notes](https://github.com/vmware-tanzu/velero/releases) - [Changelog](https://github.com/vmware-tanzu/velero/blob/main/CHANGELOG.md) - [Commits](https://github.com/vmware-tanzu/velero/compare/v1.16.2...v1.17.0) Updates `helm.sh/helm/v3` from 3.18.6 to 3.19.0 - [Release notes](https://github.com/helm/helm/releases) - [Commits](https://github.com/helm/helm/compare/v3.18.6...v3.19.0) --- updated-dependencies: - dependency-name: github.com/vmware-tanzu/velero dependency-version: 1.17.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: security - dependency-name: helm.sh/helm/v3 dependency-version: 3.19.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: security ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump helm.sh/helm/v3 from 3.18.6 to 3.19.0 in /examples/sdk/helm-template in the security group (#1859) chore(deps): bump helm.sh/helm/v3 Bumps the security group in /examples/sdk/helm-template with 1 update: [helm.sh/helm/v3](https://github.com/helm/helm). Updates `helm.sh/helm/v3` from 3.18.6 to 3.19.0 - [Release notes](https://github.com/helm/helm/releases) - [Commits](https://github.com/helm/helm/compare/v3.18.6...v3.19.0) --- updated-dependencies: - dependency-name: helm.sh/helm/v3 dependency-version: 3.19.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: security ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Add cron job support bundle scheduler Complete implementation with K8s integration: - pkg/schedule/job.go: Job management and persistence - pkg/schedule/daemon.go: Real-time scheduler daemon - pkg/schedule/cli.go: CLI commands (create, list, delete, daemon) - pkg/schedule/schedule_test.go: Comprehensive unit tests - cmd/troubleshoot/cli/root.go: CLI integration * fixing bugbot * Fix all bugbot errors: auto-update stability, job cooldown timing, and daemon execution * Deleting Agent * removed unused flags * fixing auto-upload * fixing markdown files * namespace not required flag for auto collectors to work * loosened cron job validation * writes logs to logfile * fix: resolve autoFromEnv variable scoping issue for CI - Ensure autoFromEnv variable and its usage are in correct scope - Fix build errors: declared and not used / undefined variable - All functionality preserved and tested locally - Force add to override gitignore --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Noah Campbell <noah.edward.campbell@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
213 lines
5.3 KiB
Go
213 lines
5.3 KiB
Go
package schedule
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Job represents a scheduled support bundle collection job
|
|
type Job struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Schedule string `json:"schedule"` // Cron expression
|
|
Namespace string `json:"namespace"`
|
|
Auto bool `json:"auto"` // Auto-discovery
|
|
Upload string `json:"upload,omitempty"`
|
|
Enabled bool `json:"enabled"`
|
|
RunCount int `json:"runCount"`
|
|
LastRun time.Time `json:"lastRun,omitempty"`
|
|
Created time.Time `json:"created"`
|
|
}
|
|
|
|
// Manager handles job operations
|
|
type Manager struct {
|
|
storageDir string
|
|
}
|
|
|
|
// NewManager creates a new job manager
|
|
func NewManager() (*Manager, error) {
|
|
homeDir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get user home directory: %w", err)
|
|
}
|
|
|
|
storageDir := filepath.Join(homeDir, ".troubleshoot", "scheduled-jobs")
|
|
if err := os.MkdirAll(storageDir, 0755); err != nil {
|
|
return nil, fmt.Errorf("failed to create storage directory %s: %w", storageDir, err)
|
|
}
|
|
|
|
return &Manager{storageDir: storageDir}, nil
|
|
}
|
|
|
|
// CreateJob creates a new scheduled job
|
|
func (m *Manager) CreateJob(name, schedule, namespace string, auto bool, upload string) (*Job, error) {
|
|
// Input validation
|
|
if strings.TrimSpace(name) == "" {
|
|
return nil, fmt.Errorf("job name cannot be empty")
|
|
}
|
|
|
|
// Sanitize job name for filesystem safety
|
|
name = strings.TrimSpace(name)
|
|
if len(name) > 100 {
|
|
return nil, fmt.Errorf("job name too long, maximum 100 characters")
|
|
}
|
|
|
|
// Check for invalid filename characters
|
|
invalidChars := []string{"/", "\\", ":", "*", "?", "\"", "<", ">", "|", "\x00"}
|
|
for _, char := range invalidChars {
|
|
if strings.Contains(name, char) {
|
|
return nil, fmt.Errorf("job name contains invalid character: %s", char)
|
|
}
|
|
}
|
|
|
|
// Cron validation - check it has 5 parts and basic field validation
|
|
if err := validateCronSchedule(schedule); err != nil {
|
|
return nil, fmt.Errorf("invalid cron schedule: %w", err)
|
|
}
|
|
|
|
job := &Job{
|
|
ID: generateJobID(),
|
|
Name: name,
|
|
Schedule: schedule,
|
|
Namespace: namespace,
|
|
Auto: auto,
|
|
Upload: upload,
|
|
Enabled: true,
|
|
Created: time.Now(),
|
|
}
|
|
|
|
if err := m.saveJob(job); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return job, nil
|
|
}
|
|
|
|
// ListJobs returns all saved jobs
|
|
func (m *Manager) ListJobs() ([]*Job, error) {
|
|
files, err := filepath.Glob(filepath.Join(m.storageDir, "*.json"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var jobs []*Job
|
|
for _, file := range files {
|
|
job, err := m.loadJobFromFile(file)
|
|
if err != nil {
|
|
continue // Skip invalid files
|
|
}
|
|
jobs = append(jobs, job)
|
|
}
|
|
|
|
return jobs, nil
|
|
}
|
|
|
|
// GetJob retrieves a job by name or ID
|
|
func (m *Manager) GetJob(nameOrID string) (*Job, error) {
|
|
jobs, err := m.ListJobs()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, job := range jobs {
|
|
if job.Name == nameOrID || job.ID == nameOrID {
|
|
return job, nil
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf("job not found: %s", nameOrID)
|
|
}
|
|
|
|
// DeleteJob removes a job
|
|
func (m *Manager) DeleteJob(nameOrID string) error {
|
|
job, err := m.GetJob(nameOrID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
jobFile := filepath.Join(m.storageDir, job.ID+".json")
|
|
return os.Remove(jobFile)
|
|
}
|
|
|
|
// saveJob saves a job to a JSON file
|
|
func (m *Manager) saveJob(job *Job) error {
|
|
data, err := json.MarshalIndent(job, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
jobFile := filepath.Join(m.storageDir, job.ID+".json")
|
|
return os.WriteFile(jobFile, data, 0644)
|
|
}
|
|
|
|
// loadJobFromFile loads a job from a JSON file
|
|
func (m *Manager) loadJobFromFile(filename string) (*Job, error) {
|
|
data, err := os.ReadFile(filename)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var job Job
|
|
err = json.Unmarshal(data, &job)
|
|
return &job, err
|
|
}
|
|
|
|
// validateCronSchedule performs basic cron schedule validation
|
|
func validateCronSchedule(schedule string) error {
|
|
parts := strings.Fields(schedule)
|
|
if len(parts) != 5 {
|
|
return fmt.Errorf("expected 5 fields (minute hour day-of-month month day-of-week), got %d", len(parts))
|
|
}
|
|
|
|
// Validate each field has reasonable values
|
|
fieldNames := []string{"minute", "hour", "day-of-month", "month", "day-of-week"}
|
|
fieldRanges := [][2]int{{0, 59}, {0, 23}, {1, 31}, {1, 12}, {0, 6}}
|
|
|
|
for i, field := range parts {
|
|
if err := validateCronField(field, fieldRanges[i][0], fieldRanges[i][1], fieldNames[i]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateCronField validates a single cron field
|
|
func validateCronField(field string, min, max int, fieldName string) error {
|
|
if field == "*" {
|
|
return nil
|
|
}
|
|
|
|
// Handle */N syntax
|
|
if strings.HasPrefix(field, "*/") {
|
|
intervalStr := strings.TrimPrefix(field, "*/")
|
|
if interval, err := strconv.Atoi(intervalStr); err != nil || interval <= 0 {
|
|
return fmt.Errorf("invalid %s interval: %s", fieldName, intervalStr)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Handle exact values (including comma-separated lists)
|
|
values := strings.Split(field, ",")
|
|
for _, val := range values {
|
|
val = strings.TrimSpace(val)
|
|
if fieldValue, err := strconv.Atoi(val); err != nil {
|
|
return fmt.Errorf("invalid %s value: %s", fieldName, val)
|
|
} else if fieldValue < min || fieldValue > max {
|
|
return fmt.Errorf("%s value %d out of range [%d-%d]", fieldName, fieldValue, min, max)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// generateJobID generates a simple job ID
|
|
func generateJobID() string {
|
|
return fmt.Sprintf("job-%d", time.Now().UnixNano())
|
|
}
|