Files
83e6cffe5f Cron job clean (#1862)
* 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>
2025-09-30 14:11:15 -05:00

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())
}