mirror of
https://github.com/chenasraf/sofmani.git
synced 2026-05-17 17:28:04 +00:00
feat: install on specific machines by machine id
This commit is contained in:
@@ -28,6 +28,8 @@ type AppConfig struct {
|
||||
Env *map[string]string `json:"env" yaml:"env"`
|
||||
// PlatformEnv is a map of platform-specific environment variables to set.
|
||||
PlatformEnv *platform.PlatformMap[map[string]string] `json:"platform_env" yaml:"platform_env"`
|
||||
// MachineAliases is a map of friendly names to machine IDs.
|
||||
MachineAliases *map[string]string `json:"machine_aliases" yaml:"machine_aliases"`
|
||||
// Filter is a list of installer names to filter by.
|
||||
Filter []string
|
||||
}
|
||||
@@ -46,6 +48,8 @@ type AppCliConfig struct {
|
||||
LogFile *string
|
||||
// ShowLogFile indicates that only the log file path should be shown.
|
||||
ShowLogFile bool
|
||||
// ShowMachineID indicates that only the machine ID should be shown.
|
||||
ShowMachineID bool
|
||||
}
|
||||
|
||||
// AppConfigDefaults provides default configurations for installer types.
|
||||
@@ -198,12 +202,13 @@ func boolPtr(b bool) *bool {
|
||||
func ParseCliConfig() *AppCliConfig {
|
||||
args := os.Args[1:]
|
||||
config := &AppCliConfig{
|
||||
ConfigFile: "",
|
||||
Debug: nil,
|
||||
CheckUpdates: nil,
|
||||
Filter: []string{},
|
||||
LogFile: nil,
|
||||
ShowLogFile: false,
|
||||
ConfigFile: "",
|
||||
Debug: nil,
|
||||
CheckUpdates: nil,
|
||||
Filter: []string{},
|
||||
LogFile: nil,
|
||||
ShowLogFile: false,
|
||||
ShowMachineID: false,
|
||||
}
|
||||
file := FindConfigFile()
|
||||
for len(args) > 0 {
|
||||
@@ -230,6 +235,8 @@ func ParseCliConfig() *AppCliConfig {
|
||||
// No value provided, just show log file path
|
||||
config.ShowLogFile = true
|
||||
}
|
||||
case "-m", "--machine-id":
|
||||
config.ShowMachineID = true
|
||||
case "-h", "--help":
|
||||
printHelp()
|
||||
os.Exit(0)
|
||||
@@ -248,8 +255,8 @@ func ParseCliConfig() *AppCliConfig {
|
||||
}
|
||||
args = args[1:]
|
||||
}
|
||||
// If only showing log file, don't require config file
|
||||
if config.ShowLogFile {
|
||||
// If only showing log file or machine ID, don't require config file
|
||||
if config.ShowLogFile || config.ShowMachineID {
|
||||
return config
|
||||
}
|
||||
if file == "" {
|
||||
@@ -270,6 +277,7 @@ func printHelp() {
|
||||
fmt.Println(" -U, --no-update Disable update checks")
|
||||
fmt.Println(" -f, --filter Filter by installer name (can be used multiple times)")
|
||||
fmt.Println(" -l, --log-file Set log file path, or show current path if no value given")
|
||||
fmt.Println(" -m, --machine-id Show machine ID and exit")
|
||||
fmt.Println(" -h, --help Show this help message")
|
||||
fmt.Println(" -v, --version Show version")
|
||||
fmt.Println("")
|
||||
|
||||
@@ -3,6 +3,7 @@ package appconfig
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/chenasraf/sofmani/machine"
|
||||
"github.com/chenasraf/sofmani/platform"
|
||||
"github.com/chenasraf/sofmani/utils"
|
||||
"github.com/samber/lo"
|
||||
@@ -24,6 +25,8 @@ type InstallerData struct {
|
||||
PlatformEnv *platform.PlatformMap[map[string]string] `json:"platform_env" yaml:"platform_env"`
|
||||
// Platforms is a list of platforms where this installer should run.
|
||||
Platforms *platform.Platforms `json:"platforms" yaml:"platforms"`
|
||||
// Machines is a list of machine IDs where this installer should run.
|
||||
Machines *machine.Machines `json:"machines" yaml:"machines"`
|
||||
// Steps is a list of sub-installers for group installers.
|
||||
Steps *[]InstallerData `json:"steps" yaml:"steps"`
|
||||
// Opts is a map of options specific to the installer type.
|
||||
|
||||
@@ -11,15 +11,17 @@ repository.
|
||||
|
||||
You can call `sofmani` with the following flags to alter the behavior for the current run:
|
||||
|
||||
| Flag | Description |
|
||||
| ------------------- | ------------------------------------------------------- |
|
||||
| `-d`, `--debug` | Enable debug mode. |
|
||||
| `-D`, `--no-debug` | Disable debug mode (default). |
|
||||
| `-u`, `--update` | Enable update checking. |
|
||||
| `-U`, `--no-update` | Disable update checking (default). |
|
||||
| `-f`, `--filter` | Filter by installer name (can be used multiple times)\* |
|
||||
| `-h`, `--help` | Display help information and exit. |
|
||||
| `-v`, `--version` | Display version information and exit. |
|
||||
| Flag | Description |
|
||||
| --------------------- | ------------------------------------------------------- |
|
||||
| `-d`, `--debug` | Enable debug mode. |
|
||||
| `-D`, `--no-debug` | Disable debug mode (default). |
|
||||
| `-u`, `--update` | Enable update checking. |
|
||||
| `-U`, `--no-update` | Disable update checking (default). |
|
||||
| `-f`, `--filter` | Filter by installer name (can be used multiple times)\* |
|
||||
| `-l`, `--log-file` | Set log file path, or show current path if no value. |
|
||||
| `-m`, `--machine-id` | Show machine ID and exit. |
|
||||
| `-h`, `--help` | Display help information and exit. |
|
||||
| `-v`, `--version` | Display version information and exit. |
|
||||
|
||||
Each of these flags overrides the loaded config file, so while your default config can choose not to
|
||||
check for updates by default, you or another user can add the `--update` flag to override this
|
||||
@@ -50,6 +52,21 @@ included.
|
||||
- To only installers that contain "sofmani", but exclude ones tagged "config", use
|
||||
`-f sofmani -f "!tag:config"`.
|
||||
|
||||
### Machine ID
|
||||
|
||||
The machine ID is a unique, deterministic identifier for the current machine. It is generated from
|
||||
stable system identifiers (hardware UUID on macOS, `/etc/machine-id` on Linux, registry GUID on
|
||||
Windows) and remains constant even if hostname or other system settings change.
|
||||
|
||||
To view the current machine's ID:
|
||||
|
||||
```sh
|
||||
sofmani --machine-id
|
||||
```
|
||||
|
||||
This ID can be used with the `machines` configuration option to run specific installers only on
|
||||
certain machines. See [Installer Configuration](./installer-configuration.md#fields) for details.
|
||||
|
||||
## Examples
|
||||
|
||||
Search for the config in one of the default directories, and enable update checking:
|
||||
|
||||
@@ -38,6 +38,19 @@ Here is a breakdown of all configuration options:
|
||||
- OS environment variables are passed and may be overridden for this config and all of its
|
||||
installers here.
|
||||
|
||||
- **`machine_aliases`** (Object)
|
||||
- A mapping of friendly names to machine IDs.
|
||||
- Use `sofmani --machine-id` to get the machine ID for each of your machines.
|
||||
- These aliases can then be used in installer `machines.only` and `machines.except` fields
|
||||
instead of the raw machine IDs.
|
||||
- Example:
|
||||
```yaml
|
||||
machine_aliases:
|
||||
work-laptop: 5fa2a8e8193868df
|
||||
home-desktop: a1b2c3d4e5f67890
|
||||
home-server: fedcba0987654321
|
||||
```
|
||||
|
||||
## Example Config
|
||||
|
||||
```yaml
|
||||
|
||||
@@ -47,6 +47,22 @@ These fields are shared by all installer types. Some fields may vary in behavior
|
||||
- **Type**: Array of Strings
|
||||
- **Description**: Platforms where the step should **not** execute; replaces `platforms.only`.
|
||||
|
||||
- **`machines`**
|
||||
|
||||
- **Type**: Object (optional)
|
||||
- **Description**: Machine-specific execution controls. Use this to run installers only on
|
||||
specific machines. Get the machine ID by running `sofmani --machine-id`. You can use either
|
||||
raw machine IDs or aliases defined in the top-level `machine_aliases` configuration.
|
||||
See `machines` subfields below.
|
||||
- **Subfields**:
|
||||
- **`machines.only`**
|
||||
- **Type**: Array of Strings
|
||||
- **Description**: Machine IDs or aliases where the step should execute. Supercedes
|
||||
`machines.except`.
|
||||
- **`machines.except`**
|
||||
- **Type**: Array of Strings
|
||||
- **Description**: Machine IDs or aliases where the step should **not** execute.
|
||||
|
||||
- **`steps`**
|
||||
|
||||
- **Type**: Array of Installers
|
||||
@@ -356,6 +372,49 @@ install:
|
||||
command: 'curl https://pyenv.run | bash'
|
||||
```
|
||||
|
||||
### Machine-specific installers
|
||||
|
||||
```yaml
|
||||
# Define friendly names for your machines (get IDs with `sofmani --machine-id`)
|
||||
machine_aliases:
|
||||
work-laptop: a1b2c3d4e5f67890
|
||||
home-desktop: 5fa2a8e8193868df
|
||||
home-server: fedcba0987654321
|
||||
|
||||
install:
|
||||
# Only install on specific machines using aliases
|
||||
- name: work-tools
|
||||
type: group
|
||||
machines:
|
||||
only: ['work-laptop']
|
||||
steps:
|
||||
- name: slack
|
||||
type: brew
|
||||
opts:
|
||||
cask: true
|
||||
- name: zoom
|
||||
type: brew
|
||||
opts:
|
||||
cask: true
|
||||
|
||||
# Install everywhere except the home server
|
||||
- name: desktop-apps
|
||||
type: group
|
||||
machines:
|
||||
except: ['home-server']
|
||||
steps:
|
||||
- name: firefox
|
||||
type: brew
|
||||
opts:
|
||||
cask: true
|
||||
|
||||
# You can also use raw machine IDs directly
|
||||
- name: special-tool
|
||||
type: brew
|
||||
machines:
|
||||
only: ['a1b2c3d4e5f67890'] # Raw machine ID also works
|
||||
```
|
||||
|
||||
### manifest
|
||||
|
||||
```yaml
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/chenasraf/sofmani/appconfig"
|
||||
"github.com/chenasraf/sofmani/logger"
|
||||
"github.com/chenasraf/sofmani/machine"
|
||||
"github.com/chenasraf/sofmani/platform"
|
||||
"github.com/chenasraf/sofmani/utils"
|
||||
)
|
||||
@@ -151,6 +152,16 @@ func RunInstaller(config *appconfig.AppConfig, installer IInstaller) error {
|
||||
logger.Debug("%s should not run on %s, skipping", name, curOS)
|
||||
return nil
|
||||
}
|
||||
|
||||
machineID := machine.GetMachineID()
|
||||
var machineAliases map[string]string
|
||||
if config.MachineAliases != nil {
|
||||
machineAliases = *config.MachineAliases
|
||||
}
|
||||
if !installer.GetData().Machines.GetShouldRunOnMachine(machineID, machineAliases) {
|
||||
logger.Debug("%s should not run on machine %s, skipping", name, machineID)
|
||||
return nil
|
||||
}
|
||||
if !FilterInstaller(installer, config.Filter) {
|
||||
logger.Debug("%s is filtered, skipping", name)
|
||||
return nil
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package installer
|
||||
|
||||
import (
|
||||
"github.com/chenasraf/sofmani/appconfig"
|
||||
"github.com/chenasraf/sofmani/platform"
|
||||
"maps"
|
||||
|
||||
"github.com/chenasraf/sofmani/appconfig"
|
||||
"github.com/chenasraf/sofmani/machine"
|
||||
"github.com/chenasraf/sofmani/platform"
|
||||
)
|
||||
|
||||
// InstallerWithDefaults applies default configurations to an installer data object.
|
||||
@@ -65,6 +67,9 @@ func InstallerWithDefaults(
|
||||
if override.Platforms != nil {
|
||||
data.Platforms = override.Platforms
|
||||
}
|
||||
if override.Machines != nil {
|
||||
data.Machines = override.Machines
|
||||
}
|
||||
if override.PreUpdate != nil {
|
||||
data.PreUpdate = override.PreUpdate
|
||||
}
|
||||
@@ -112,6 +117,10 @@ func FillDefaults(data *appconfig.InstallerData) {
|
||||
platforms := platform.Platforms{}
|
||||
data.Platforms = &platforms
|
||||
}
|
||||
if data.Machines == nil {
|
||||
machines := machine.Machines{}
|
||||
data.Machines = &machines
|
||||
}
|
||||
if data.Steps == nil {
|
||||
data.Steps = &[]appconfig.InstallerData{}
|
||||
}
|
||||
|
||||
47
machine/machine.go
Normal file
47
machine/machine.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package machine
|
||||
|
||||
// Machines defines which machines a configuration applies to.
|
||||
type Machines struct {
|
||||
// Only specifies a list of machine IDs or aliases where the configuration should apply.
|
||||
Only *[]string `json:"only" yaml:"only"`
|
||||
// Except specifies a list of machine IDs or aliases where the configuration should not apply.
|
||||
Except *[]string `json:"except" yaml:"except"`
|
||||
}
|
||||
|
||||
// GetShouldRunOnMachine determines if a configuration should run on the current machine
|
||||
// based on the Only and Except fields of the Machines struct.
|
||||
// The aliases parameter is a map of friendly names to machine IDs. When checking,
|
||||
// aliases are resolved first, falling back to treating the value as a literal machine ID.
|
||||
func (m *Machines) GetShouldRunOnMachine(machineID string, aliases map[string]string) bool {
|
||||
if m == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
if m.Only != nil {
|
||||
return containsMachineID(*m.Only, machineID, aliases)
|
||||
}
|
||||
if m.Except != nil {
|
||||
return !containsMachineID(*m.Except, machineID, aliases)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// containsMachineID checks if the machine ID is in the list, resolving aliases first.
|
||||
func containsMachineID(list []string, machineID string, aliases map[string]string) bool {
|
||||
for _, entry := range list {
|
||||
// First, try to resolve as an alias
|
||||
if aliases != nil {
|
||||
if resolvedID, ok := aliases[entry]; ok {
|
||||
if resolvedID == machineID {
|
||||
return true
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
// Fall back to treating entry as a literal machine ID
|
||||
if entry == machineID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
218
machine/machine_id.go
Normal file
218
machine/machine_id.go
Normal file
@@ -0,0 +1,218 @@
|
||||
package machine
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
cachedMachineID string
|
||||
machineIDOnce sync.Once
|
||||
)
|
||||
|
||||
// GetMachineID returns a deterministic, unique machine identifier.
|
||||
// The ID is stable across hostname changes and other system configuration changes.
|
||||
// It uses platform-specific stable identifiers:
|
||||
// - macOS: IOPlatformUUID (hardware UUID)
|
||||
// - Linux: /etc/machine-id or /var/lib/dbus/machine-id
|
||||
// - Windows: Registry MachineGuid
|
||||
//
|
||||
// The raw identifier is hashed with SHA-256 and truncated to 16 characters
|
||||
// for a more user-friendly format while maintaining uniqueness.
|
||||
func GetMachineID() string {
|
||||
machineIDOnce.Do(func() {
|
||||
var rawID string
|
||||
var err error
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
rawID, err = getMachineIDDarwin()
|
||||
case "linux":
|
||||
rawID, err = getMachineIDLinux()
|
||||
case "windows":
|
||||
rawID, err = getMachineIDWindows()
|
||||
default:
|
||||
err = fmt.Errorf("unsupported platform: %s", runtime.GOOS)
|
||||
}
|
||||
|
||||
if err != nil || rawID == "" {
|
||||
// Fallback: generate a persistent ID file in user config directory
|
||||
rawID, err = getOrCreateFallbackID()
|
||||
if err != nil {
|
||||
// Last resort: use a hash of available system info
|
||||
rawID = getFallbackSystemInfo()
|
||||
}
|
||||
}
|
||||
|
||||
// Hash the raw ID for privacy and consistent format
|
||||
cachedMachineID = hashMachineID(rawID)
|
||||
})
|
||||
|
||||
return cachedMachineID
|
||||
}
|
||||
|
||||
// getMachineIDDarwin retrieves the hardware UUID on macOS using ioreg.
|
||||
func getMachineIDDarwin() (string, error) {
|
||||
cmd := exec.Command("ioreg", "-rd1", "-c", "IOPlatformExpertDevice")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Parse the IOPlatformUUID from the output
|
||||
lines := strings.Split(string(output), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "IOPlatformUUID") {
|
||||
// Format: "IOPlatformUUID" = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
|
||||
parts := strings.Split(line, "=")
|
||||
if len(parts) == 2 {
|
||||
uuid := strings.TrimSpace(parts[1])
|
||||
uuid = strings.Trim(uuid, "\"")
|
||||
return uuid, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("IOPlatformUUID not found")
|
||||
}
|
||||
|
||||
// getMachineIDLinux retrieves the machine ID on Linux from /etc/machine-id
|
||||
// or /var/lib/dbus/machine-id as a fallback.
|
||||
func getMachineIDLinux() (string, error) {
|
||||
// Try /etc/machine-id first (systemd)
|
||||
if data, err := os.ReadFile("/etc/machine-id"); err == nil {
|
||||
id := strings.TrimSpace(string(data))
|
||||
if id != "" {
|
||||
return id, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to /var/lib/dbus/machine-id
|
||||
if data, err := os.ReadFile("/var/lib/dbus/machine-id"); err == nil {
|
||||
id := strings.TrimSpace(string(data))
|
||||
if id != "" {
|
||||
return id, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("machine-id not found")
|
||||
}
|
||||
|
||||
// getMachineIDWindows retrieves the MachineGuid from the Windows registry.
|
||||
func getMachineIDWindows() (string, error) {
|
||||
cmd := exec.Command("reg", "query",
|
||||
`HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography`,
|
||||
"/v", "MachineGuid")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Parse the MachineGuid from the output
|
||||
lines := strings.Split(string(output), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "MachineGuid") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) >= 3 {
|
||||
return fields[len(fields)-1], nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("MachineGuid not found")
|
||||
}
|
||||
|
||||
// getOrCreateFallbackID creates or reads a persistent machine ID file
|
||||
// in the user's config directory as a fallback mechanism.
|
||||
func getOrCreateFallbackID() (string, error) {
|
||||
configDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
sofmaniDir := configDir + "/sofmani"
|
||||
idFile := sofmaniDir + "/machine-id"
|
||||
|
||||
// Try to read existing ID
|
||||
if data, err := os.ReadFile(idFile); err == nil {
|
||||
id := strings.TrimSpace(string(data))
|
||||
if id != "" {
|
||||
return id, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Create new ID based on available system info
|
||||
newID := generateFallbackID()
|
||||
|
||||
// Ensure directory exists
|
||||
if err := os.MkdirAll(sofmaniDir, 0755); err != nil {
|
||||
return newID, nil // Return the ID even if we can't persist it
|
||||
}
|
||||
|
||||
// Write the ID file
|
||||
if err := os.WriteFile(idFile, []byte(newID), 0644); err != nil {
|
||||
return newID, nil // Return the ID even if we can't persist it
|
||||
}
|
||||
|
||||
return newID, nil
|
||||
}
|
||||
|
||||
// generateFallbackID generates a unique ID using available system information.
|
||||
func generateFallbackID() string {
|
||||
info := getFallbackSystemInfo()
|
||||
hash := sha256.Sum256([]byte(info))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
// getFallbackSystemInfo gathers available system information for ID generation.
|
||||
func getFallbackSystemInfo() string {
|
||||
var parts []string
|
||||
|
||||
// Add hostname (may change, but better than nothing)
|
||||
if hostname, err := os.Hostname(); err == nil {
|
||||
parts = append(parts, hostname)
|
||||
}
|
||||
|
||||
// Add user home directory path
|
||||
if home, err := os.UserHomeDir(); err == nil {
|
||||
parts = append(parts, home)
|
||||
}
|
||||
|
||||
// Add config directory path
|
||||
if configDir, err := os.UserConfigDir(); err == nil {
|
||||
parts = append(parts, configDir)
|
||||
}
|
||||
|
||||
// Add OS and architecture
|
||||
parts = append(parts, runtime.GOOS, runtime.GOARCH)
|
||||
|
||||
return strings.Join(parts, "|")
|
||||
}
|
||||
|
||||
// hashMachineID creates a SHA-256 hash of the raw machine ID
|
||||
// and returns a truncated hex string (16 characters).
|
||||
func hashMachineID(rawID string) string {
|
||||
hash := sha256.Sum256([]byte(rawID))
|
||||
fullHex := hex.EncodeToString(hash[:])
|
||||
// Return first 16 characters for a more user-friendly format
|
||||
return fullHex[:16]
|
||||
}
|
||||
|
||||
// SetMachineID overrides the detected machine ID. This is primarily used for testing.
|
||||
func SetMachineID(id string) {
|
||||
cachedMachineID = id
|
||||
// Reset the once so that future calls won't re-detect
|
||||
machineIDOnce.Do(func() {})
|
||||
}
|
||||
|
||||
// ResetMachineID clears the cached machine ID. This is primarily used for testing.
|
||||
func ResetMachineID() {
|
||||
cachedMachineID = ""
|
||||
machineIDOnce = sync.Once{}
|
||||
}
|
||||
177
machine/machine_test.go
Normal file
177
machine/machine_test.go
Normal file
@@ -0,0 +1,177 @@
|
||||
package machine
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestMachines_GetShouldRunOnMachine(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
machines *Machines
|
||||
machineID string
|
||||
aliases map[string]string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "nil machines should run on any machine",
|
||||
machines: nil,
|
||||
machineID: "abc123",
|
||||
aliases: nil,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "empty machines should run on any machine",
|
||||
machines: &Machines{},
|
||||
machineID: "abc123",
|
||||
aliases: nil,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "only matching machine ID should run",
|
||||
machines: &Machines{
|
||||
Only: &[]string{"abc123", "def456"},
|
||||
},
|
||||
machineID: "abc123",
|
||||
aliases: nil,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "only non-matching machine ID should not run",
|
||||
machines: &Machines{
|
||||
Only: &[]string{"abc123", "def456"},
|
||||
},
|
||||
machineID: "xyz789",
|
||||
aliases: nil,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "except matching machine ID should not run",
|
||||
machines: &Machines{
|
||||
Except: &[]string{"abc123", "def456"},
|
||||
},
|
||||
machineID: "abc123",
|
||||
aliases: nil,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "except non-matching machine ID should run",
|
||||
machines: &Machines{
|
||||
Except: &[]string{"abc123", "def456"},
|
||||
},
|
||||
machineID: "xyz789",
|
||||
aliases: nil,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "only takes precedence over except",
|
||||
machines: &Machines{
|
||||
Only: &[]string{"abc123"},
|
||||
Except: &[]string{"abc123"},
|
||||
},
|
||||
machineID: "abc123",
|
||||
aliases: nil,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "alias matching should run",
|
||||
machines: &Machines{
|
||||
Only: &[]string{"work-laptop"},
|
||||
},
|
||||
machineID: "abc123",
|
||||
aliases: map[string]string{"work-laptop": "abc123"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "alias non-matching should not run",
|
||||
machines: &Machines{
|
||||
Only: &[]string{"work-laptop"},
|
||||
},
|
||||
machineID: "xyz789",
|
||||
aliases: map[string]string{"work-laptop": "abc123"},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "except with alias matching should not run",
|
||||
machines: &Machines{
|
||||
Except: &[]string{"home-server"},
|
||||
},
|
||||
machineID: "abc123",
|
||||
aliases: map[string]string{"home-server": "abc123"},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "except with alias non-matching should run",
|
||||
machines: &Machines{
|
||||
Except: &[]string{"home-server"},
|
||||
},
|
||||
machineID: "xyz789",
|
||||
aliases: map[string]string{"home-server": "abc123"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "mixed aliases and literal IDs in only",
|
||||
machines: &Machines{
|
||||
Only: &[]string{"work-laptop", "def456"},
|
||||
},
|
||||
machineID: "abc123",
|
||||
aliases: map[string]string{"work-laptop": "abc123"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "literal ID fallback when no alias match",
|
||||
machines: &Machines{
|
||||
Only: &[]string{"abc123"},
|
||||
},
|
||||
machineID: "abc123",
|
||||
aliases: map[string]string{"other": "xyz789"},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := tt.machines.GetShouldRunOnMachine(tt.machineID, tt.aliases)
|
||||
if got != tt.want {
|
||||
t.Errorf("GetShouldRunOnMachine() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetMachineID(t *testing.T) {
|
||||
// Reset any cached value
|
||||
ResetMachineID()
|
||||
|
||||
// Get the machine ID
|
||||
id := GetMachineID()
|
||||
|
||||
// Should not be empty
|
||||
if id == "" {
|
||||
t.Error("GetMachineID() returned empty string")
|
||||
}
|
||||
|
||||
// Should be 16 characters (truncated hash)
|
||||
if len(id) != 16 {
|
||||
t.Errorf("GetMachineID() returned ID with length %d, want 16", len(id))
|
||||
}
|
||||
|
||||
// Should be deterministic (same value on subsequent calls)
|
||||
id2 := GetMachineID()
|
||||
if id != id2 {
|
||||
t.Errorf("GetMachineID() not deterministic: got %s then %s", id, id2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetMachineID(t *testing.T) {
|
||||
// Save original and restore after test
|
||||
originalID := GetMachineID()
|
||||
defer SetMachineID(originalID)
|
||||
|
||||
// Set a custom ID
|
||||
customID := "testmachineid123"
|
||||
SetMachineID(customID)
|
||||
|
||||
// Verify the custom ID is returned
|
||||
got := GetMachineID()
|
||||
if got != customID {
|
||||
t.Errorf("After SetMachineID(%s), GetMachineID() = %s", customID, got)
|
||||
}
|
||||
}
|
||||
15
main.go
15
main.go
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/chenasraf/sofmani/appconfig"
|
||||
"github.com/chenasraf/sofmani/installer"
|
||||
"github.com/chenasraf/sofmani/logger"
|
||||
"github.com/chenasraf/sofmani/machine"
|
||||
"github.com/chenasraf/sofmani/utils"
|
||||
)
|
||||
|
||||
@@ -28,6 +29,12 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
// Handle --machine-id: show machine ID and exit
|
||||
if cliConfig.ShowMachineID {
|
||||
fmt.Println(machine.GetMachineID())
|
||||
return
|
||||
}
|
||||
|
||||
// Set custom log file if provided
|
||||
if cliConfig.LogFile != nil {
|
||||
logger.SetLogFile(*cliConfig.LogFile)
|
||||
@@ -55,6 +62,14 @@ func main() {
|
||||
logger.Debug("%s", line)
|
||||
}
|
||||
|
||||
// Set MACHINE_ID environment variable
|
||||
machineID := machine.GetMachineID()
|
||||
logger.Debug("Setting env MACHINE_ID=%s", machineID)
|
||||
if err := os.Setenv("MACHINE_ID", machineID); err != nil {
|
||||
logger.Error("failed to set environment variable MACHINE_ID: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if cfg.Env != nil {
|
||||
for k, v := range *cfg.Env {
|
||||
logger.Debug("Setting env %s=%s", k, v)
|
||||
|
||||
Reference in New Issue
Block a user