Skip to content
Open
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
13 changes: 10 additions & 3 deletions cmd/satellite/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ type SatelliteOptions struct {
HarborRegistryURL string
DirectDelivery bool
ImageDir string
Headless bool
}

func main() {
Expand All @@ -83,6 +84,7 @@ func main() {
HarborRegistryURL: envCfg.HarborRegistryURL,
DirectDelivery: envCfg.DirectDelivery,
ImageDir: envCfg.ImageDir,
Headless: envCfg.Headless,
}
shutdownTimeout := envCfg.ShutdownTimeout

Expand All @@ -106,6 +108,7 @@ func main() {
flag.StringVar(&opts.HarborRegistryURL, "harbor-registry-url", opts.HarborRegistryURL, "Override Harbor registry URL from Ground Control (e.g., http://10.0.0.1:8080)")
flag.BoolVar(&opts.DirectDelivery, "direct-delivery", opts.DirectDelivery, "[Experimental] Write image tarballs directly to k3s/RKE2 agent images directory")
flag.StringVar(&opts.ImageDir, "image-dir", opts.ImageDir, "Override image directory for direct delivery (auto-detected if empty)")
flag.BoolVar(&opts.Headless, "headless", opts.Headless, "Run satellite in headless mode without Ground Control")

flag.Parse()
if opts.Token == "" {
Expand Down Expand Up @@ -136,8 +139,8 @@ func main() {
pathConfig.ZotStorageDir = opts.RegistryDataDir
}

// For --fallback-only mode, relax token/gc-url requirements
if !opts.FallbackOnly {
// For --fallback-only and --headless modes, relax token/gc-url requirements
if !opts.FallbackOnly && !opts.Headless {
if !opts.SPIFFEEnabled && (opts.Token == "" || opts.GroundControlURL == "") {
fmt.Println("Missing required arguments: --token and --ground-control-url or matching env vars (or enable SPIFFE with --spiffe-enabled).")
os.Exit(1)
Expand All @@ -159,6 +162,10 @@ func main() {
os.Exit(1)
}

if opts.Headless {
fmt.Println("Satellite is running in headless mode")
}

err = run(opts, pathConfig, shutdownTimeout)
if err != nil {
fmt.Printf("fatal: %v\n", err)
Expand Down Expand Up @@ -254,7 +261,7 @@ func run(opts SatelliteOptions, pathConfig *config.PathConfig, shutdownTimeout s
defer cancel()
wg, ctx := errgroup.WithContext(ctx)

cm, warnings, err := config.InitConfigManager(opts.Token, opts.GroundControlURL, pathConfig.ConfigFile, pathConfig.PrevConfigFile, opts.JSONLogging, opts.UseUnsecure)
cm, warnings, err := config.InitConfigManager(opts.Token, opts.GroundControlURL, pathConfig.ConfigFile, pathConfig.PrevConfigFile, opts.JSONLogging, opts.UseUnsecure, opts.Headless)
if err != nil {
fmt.Printf("Error initiating the config manager: %v\n", err)
return err
Expand Down
1 change: 1 addition & 0 deletions internal/env/harbor-satellite.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type HarborSatellite struct {
HarborRegistryURL string `env:"HARBOR_REGISTRY_URL"`
DirectDelivery bool `env:"DIRECT_DELIVERY" envDefault:"false"`
ImageDir string `env:"IMAGE_DIR"`
Headless bool `env:"HEADLESS" envDefault:"false"`
}

func (h HarborSatellite) ApplyDefaults() HarborSatellite {
Expand Down
114 changes: 58 additions & 56 deletions internal/satellite/satellite.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,70 +34,72 @@ func (s *Satellite) Run(ctx context.Context) error {
fetchAndReplicateStateProcess := state.NewFetchAndReplicateStateProcess(s.cm, s.stateFilePath, log)
s.stateProcess = fetchAndReplicateStateProcess

// Create ZTR scheduler if not already done
if !s.cm.IsZTRDone() {
var ztrScheduler *scheduler.Scheduler
var err error

if s.cm.IsSPIFFEEnabled() {
log.Info().Msg("SPIFFE authentication enabled, using SPIFFE-based ZTR")
spiffeZtrProcess, processErr := state.NewSpiffeZtrProcess(s.cm)
if processErr != nil {
log.Error().Err(processErr).Msg("Failed to create SPIFFE ZTR process")
return processErr
if !s.cm.IsHeadless() {
// Create ZTR scheduler if not already done
if !s.cm.IsZTRDone() {
var ztrScheduler *scheduler.Scheduler
var err error

if s.cm.IsSPIFFEEnabled() {
log.Info().Msg("SPIFFE authentication enabled, using SPIFFE-based ZTR")
spiffeZtrProcess, processErr := state.NewSpiffeZtrProcess(s.cm)
if processErr != nil {
log.Error().Err(processErr).Msg("Failed to create SPIFFE ZTR process")
return processErr
}
ztrScheduler, err = scheduler.NewSchedulerWithInterval(
s.cm.GetRegistrationInterval(),
spiffeZtrProcess,
log,
)
} else {
log.Info().Msg("Using token-based ZTR")
ztrProcess := state.NewZtrProcess(s.cm)
ztrScheduler, err = scheduler.NewSchedulerWithInterval(
s.cm.GetRegistrationInterval(),
ztrProcess,
log,
)
}
ztrScheduler, err = scheduler.NewSchedulerWithInterval(
s.cm.GetRegistrationInterval(),
spiffeZtrProcess,
log,
)
} else {
log.Info().Msg("Using token-based ZTR")
ztrProcess := state.NewZtrProcess(s.cm)
ztrScheduler, err = scheduler.NewSchedulerWithInterval(
s.cm.GetRegistrationInterval(),
ztrProcess,
log,
)

if err != nil {
log.Error().Err(err).Msg("Failed to create ZTR scheduler")
return err
}
s.schedulers = append(s.schedulers, ztrScheduler)
ztrScheduler.Start(ctx)
}

// Create state replication scheduler
stateScheduler, err := scheduler.NewSchedulerWithInterval(
s.cm.GetStateReplicationInterval(),
fetchAndReplicateStateProcess,
log,
)
if err != nil {
log.Error().Err(err).Msg("Failed to create ZTR scheduler")
log.Error().Err(err).Msg("Failed to create state replication scheduler")
return err
}
s.schedulers = append(s.schedulers, ztrScheduler)
ztrScheduler.Start(ctx)
}

// Create state replication scheduler
stateScheduler, err := scheduler.NewSchedulerWithInterval(
s.cm.GetStateReplicationInterval(),
fetchAndReplicateStateProcess,
log,
)
if err != nil {
log.Error().Err(err).Msg("Failed to create state replication scheduler")
return err
}
s.schedulers = append(s.schedulers, stateScheduler)
stateScheduler.Start(ctx)
s.schedulers = append(s.schedulers, stateScheduler)
stateScheduler.Start(ctx)

// Create status report scheduler with pending CRI results
statusReportProcess := state.NewStatusReportingProcess(s.cm)
if len(s.criResults) > 0 {
statusReportProcess.SetPendingCRIResults(s.criResults)
}
statusScheduler, err := scheduler.NewSchedulerWithInterval(
s.cm.GetHeartbeatInterval(),
statusReportProcess,
log,
)
if err != nil {
log.Error().Err(err).Msg("Failed to create status report scheduler")
return err
// Create status report scheduler with pending CRI results
statusReportProcess := state.NewStatusReportingProcess(s.cm)
if len(s.criResults) > 0 {
statusReportProcess.SetPendingCRIResults(s.criResults)
}
statusScheduler, err := scheduler.NewSchedulerWithInterval(
s.cm.GetHeartbeatInterval(),
statusReportProcess,
log,
)
if err != nil {
log.Error().Err(err).Msg("Failed to create status report scheduler")
return err
}
s.schedulers = append(s.schedulers, statusScheduler)
statusScheduler.Start(ctx)
}
s.schedulers = append(s.schedulers, statusScheduler)
statusScheduler.Start(ctx)

return ctx.Err()
}
Expand Down
24 changes: 17 additions & 7 deletions pkg/config/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,10 @@ type ConfigManager struct {
mu sync.RWMutex
encryptor *secure.ConfigEncryptor
encryptEnabled bool
headless bool
}

func NewConfigManager(configPath, prevConfigPath, token, defaultGroundControlURL string, jsonLog bool, config *Config) (*ConfigManager, error) {
func NewConfigManager(configPath, prevConfigPath, token, defaultGroundControlURL string, jsonLog bool, config *Config, headless bool) (*ConfigManager, error) {
cryptoProvider := crypto.NewAESProvider()
deviceIdentity := identity.NewLinuxDeviceIdentity()
encryptor := secure.NewConfigEncryptor(cryptoProvider, deviceIdentity)
Expand All @@ -58,9 +59,16 @@ func NewConfigManager(configPath, prevConfigPath, token, defaultGroundControlURL
JsonLog: jsonLog,
encryptor: encryptor,
encryptEnabled: config.AppConfig.EncryptConfig,
headless: headless,
}, nil
}

func (cm *ConfigManager) IsHeadless() bool {
cm.mu.RLock()
defer cm.mu.RUnlock()
return cm.headless
}

func (cm *ConfigManager) With(mutators ...func(*Config)) *ConfigManager {
cm.mu.Lock()
defer cm.mu.Unlock()
Expand Down Expand Up @@ -162,7 +170,7 @@ func (cm *ConfigManager) ReloadConfig() ([]ConfigChange, []string, error) {
return nil, nil, fmt.Errorf("failed to read config from disk: %w", err)
}

validatedConfig, warnings, err := ValidateAndEnforceDefaults(newConfig, cm.DefaultGroundControlURL)
validatedConfig, warnings, err := ValidateAndEnforceDefaults(newConfig, cm.DefaultGroundControlURL, cm.headless)
if err != nil {
return nil, warnings, fmt.Errorf("failed to validate reloaded config: %w", err)
}
Expand All @@ -174,12 +182,14 @@ func (cm *ConfigManager) ReloadConfig() ([]ConfigChange, []string, error) {
return changes, warnings, nil
}

func InitConfigManager(token, groundControlURL, configPath, prevConfigPath string, jsonLogging, useUnsecure bool) (*ConfigManager, []string, error) {
func InitConfigManager(token, groundControlURL, configPath, prevConfigPath string, jsonLogging, useUnsecure, headless bool) (*ConfigManager, []string, error) {
var cfg *Config
var err error

if _, err := url.ParseRequestURI(groundControlURL); err != nil {
return nil, nil, fmt.Errorf("invalid URL provided for ground_control_url env var: %w", err)
if !headless {
if _, err := url.ParseRequestURI(groundControlURL); err != nil {
return nil, nil, fmt.Errorf("invalid URL provided for ground_control_url env var: %w", err)
}
}

cfg, err = readAndReturnConfig(configPath)
Expand All @@ -194,12 +204,12 @@ func InitConfigManager(token, groundControlURL, configPath, prevConfigPath strin
cfg.AppConfig.UseUnsecure = true
}

cfg, warnings, err := ValidateAndEnforceDefaults(cfg, groundControlURL)
cfg, warnings, err := ValidateAndEnforceDefaults(cfg, groundControlURL, headless)
if err != nil {
return nil, warnings, fmt.Errorf("invalid config: %w", err)
}

cm, err := NewConfigManager(configPath, prevConfigPath, token, groundControlURL, jsonLogging, cfg)
cm, err := NewConfigManager(configPath, prevConfigPath, token, groundControlURL, jsonLogging, cfg, headless)
if err != nil {
return nil, warnings, fmt.Errorf("failed to create config manager: %w", err)
}
Expand Down
8 changes: 5 additions & 3 deletions pkg/config/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import (
// It applies default values where required, verifies URLs, cron expressions,
// and handles the logic for bring-your-own-registry vs default registry setup.
// Returns warnings for any defaulted or ignored fields and a fatal error for critical misconfigurations.
func ValidateAndEnforceDefaults(config *Config, defaultGroundControlURL string) (*Config, []string, error) {
func ValidateAndEnforceDefaults(config *Config, defaultGroundControlURL string, headless bool) (*Config, []string, error) {
if config == nil {
config = &Config{}
}
Expand All @@ -35,8 +35,10 @@ func ValidateAndEnforceDefaults(config *Config, defaultGroundControlURL string)
config.AppConfig.GroundControlURL = URL(defaultGroundControlURL)
}

if _, err := url.ParseRequestURI(string(config.AppConfig.GroundControlURL)); err != nil {
return nil, nil, fmt.Errorf("invalid URL provided for ground_control_url: %w", err)
if !headless {
if _, err := url.ParseRequestURI(string(config.AppConfig.GroundControlURL)); err != nil {
return nil, nil, fmt.Errorf("invalid URL provided for ground_control_url: %w", err)
}
}

warnings = append(warnings, validateAndEnforceLogLevel(config)...)
Expand Down