diff --git a/README.md b/README.md index 525a0f6..b30ceef 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,13 @@ Whip your servers into line. Chief Whip is a _fast_ and _simple_ Ansible replace | templates | mysql | stat | | vault | postgresql | debug | +# Install + +``` +# for now +curl -fsSL https://sansec.io/downloads/darwin-arm64/whip -O ~/bin/whip +``` + # Demo > [!NOTE] @@ -26,20 +33,17 @@ Whip your servers into line. Chief Whip is a _fast_ and _simple_ Ansible replace # Philosophy -How will Chief Whip _stay_ fast and simple? By will only contain features that satisfy 95% of use cases. Convention over configuration. - -Fast! +How will Chief Whip _stay_ fast and simple? -1. Eliminate unnecessary SSH round trips: Ansibles biggest delay is caused by tasks that are sent one by one. Chief Whip bundles tasks into a single job. -2. Implemented in Golang instead of Python +Simple ==> Only build features that satisfy 95% of use cases. Convention over configuration. Support top used modules only. -Simple! +Fast ==> Eliminate unnecessary SSH round trips: Ansibles biggest delay is caused by tasks that are sent one by one. Chief Whip bundles tasks into a single job. Also, Chief Whip is written in Golang which runs faster than Python. # But why? -I really loved Ansible. Compared to the popular configuration management systems at the time (Puppet, Chef, CFEngine), it was a breeze of fresh air. Simple configuration files, easy to learn, effective documentation, simple push architecture. My team used it to manage some 2k+ servers without a fuss. +Curiously, Ansible also started out as fast and simple. Compared to the popular configuration management systems at the time (Puppet, Chef, CFEngine), it was a breeze of fresh air. Simple configuration files, easy to learn, effective documentation, simple push architecture. My team used it to manage some 2k+ servers without a fuss. -Until version 2 or so. After the RedHat acquisition, Ansible has quickly grown into commercial bloatware. It's funny how RedHat got rid of the old objectives page (Simple, Fast) and replaced it with a corporate bog of marketing fluff. The task parameter documentation is hidden behind compulsory white paper downloads. Core modules have grown to support 20 extra options to support esoteric use cases. And above all, its once legendary speed is gone. Ansible feels sluggish today. +Until version 2 or so. After the RedHat acquisition, Ansible grew into commercial bloatware. RedHat got rid of the old objectives page (Simple, Fast) and replaced it with corporate marketing fluff. The task parameter documentation is hidden behind white paper downloads. Core modules have grown to support 20 extra options to support esoteric use cases. And above all, its once legendary speed is gone. Ansible feels sluggish today. Ansible has grown too complex, as illustrated by this Hacker News comment: @@ -66,7 +70,7 @@ Ansible has grown too complex, as illustrated by this Hacker News comment: The latter, however we stick to most of Ansible's verbiage to ease a transition. -#### Isn't everybody using Docker, Kubernetes etc these days? +#### Isn't everybody using Docker, Kubernetes and Kamal etc these days? [Nope](https://trends.google.com/trends/explore?date=all&q=ansible). diff --git a/cmd/whip/main.go b/cmd/whip/main.go index 5e7dbff..76b8180 100644 --- a/cmd/whip/main.go +++ b/cmd/whip/main.go @@ -1,8 +1,10 @@ package main import ( + "fmt" "os" + "github.com/gwillem/go-buildversion" log "github.com/gwillem/go-simplelog" "github.com/gwillem/whip/internal/vault" @@ -39,10 +41,17 @@ It aims to be stand-in replacement for Ansible for 90% of use cases.`, } }, } + versionCmd = &cobra.Command{ + Use: "version", + Short: "Print the version number of Whip", + Run: func(_ *cobra.Command, _ []string) { + fmt.Println("whip", buildversion.String()) + }, + } ) func init() { - rootCmd.AddCommand(vaultEditCmd, vaultConvertCmd) + rootCmd.AddCommand(vaultEditCmd, vaultConvertCmd, versionCmd) rootCmd.CompletionOptions.HiddenDefaultCmd = true rootCmd.PersistentFlags().CountP("verbose", "v", "verbose output") } diff --git a/cmd/whip/whip.go b/cmd/whip/whip.go index 70139fe..51cb14a 100644 --- a/cmd/whip/whip.go +++ b/cmd/whip/whip.go @@ -14,6 +14,7 @@ import ( "github.com/gwillem/go-buildversion" log "github.com/gwillem/go-simplelog" "github.com/gwillem/whip/internal/assets" + "github.com/gwillem/whip/internal/fsutil" "github.com/gwillem/whip/internal/model" "github.com/gwillem/whip/internal/playbook" "github.com/gwillem/whip/internal/runners" @@ -22,8 +23,9 @@ import ( ) const ( - deputyPath = ".cache/whip/deputy" - defaultAssetPath = "files" + deputyPath = ".cache/whip/deputy" + defaultAssetPath = "files" + defaultPlaybookPath = ".whip/playbook.yml" ) //go:embed deputies @@ -31,11 +33,11 @@ var deputies embed.FS func runWhip(cmd *cobra.Command, args []string) { var playbookPath string - if len(args) == 0 { - // Look for ".whip/playbook.yml" in current and parent directories - playbookPath = findPlaybookPath() - } else { + if len(args) > 0 { playbookPath = args[0] + } else { + // Look for ".whip/playbook.yml" in current and parent directories + playbookPath = fsutil.FindAncestorPath(defaultPlaybookPath) } if playbookPath == "" { @@ -156,21 +158,6 @@ func runWhip(cmd *cobra.Command, args []string) { log.Ok(fmt.Sprintf("Finished whip in %.1fs", time.Since(whipStartTime).Seconds())) } -func findPlaybookPath() string { - dir, _ := os.Getwd() - for { - path := filepath.Join(dir, ".whip", "playbook.yml") - if _, err := os.Stat(path); err == nil { - return path - } - parent := filepath.Dir(dir) - if parent == dir { - return "" - } - dir = parent - } -} - func runPlaybookAtHost(job model.Job, t model.TargetName, results chan<- model.TaskResult) { runStart := time.Now() if len(job.Playbook) == 0 { diff --git a/cmd/whip/whip_test.go b/cmd/whip/whip_test.go index f07757f..06ab7d0 100644 --- a/cmd/whip/whip_test.go +++ b/cmd/whip/whip_test.go @@ -1,60 +1 @@ package main - -import ( - "os" - "path/filepath" - "testing" -) - -func TestFindPlaybookPath(t *testing.T) { - // Create a temporary directory structure - tempDir, err := os.MkdirTemp("", "whip_test") - if err != nil { - t.Fatalf("Failed to create temp dir: %v", err) - } - defer os.RemoveAll(tempDir) - - // Resolve any symlinks in tempDir - tempDir, err = filepath.EvalSymlinks(tempDir) - if err != nil { - t.Fatalf("Failed to resolve symlinks in temp dir: %v", err) - } - - // Create a .whip directory with a playbook.yml file - whipDir := filepath.Join(tempDir, "subdir", "subsubdir", ".whip") - err = os.MkdirAll(whipDir, 0o755) - if err != nil { - t.Fatalf("Failed to create .whip dir: %v", err) - } - playbookPath := filepath.Join(whipDir, "playbook.yml") - err = os.WriteFile(playbookPath, []byte("dummy content"), 0o644) - if err != nil { - t.Fatalf("Failed to create playbook.yml: %v", err) - } - - // Change to the deepest subdirectory - err = os.Chdir(filepath.Join(tempDir, "subdir", "subsubdir")) - if err != nil { - t.Fatalf("Failed to change directory: %v", err) - } - - // Run the function - result := findPlaybookPath() - - // Check the result - expected := playbookPath - if result != expected { - t.Errorf("Expected path %s, but got %s", expected, result) - } - - // Test when no playbook is found - err = os.Chdir("/") - if err != nil { - t.Fatalf("Failed to change to root directory: %v", err) - } - - result = findPlaybookPath() - if result != "" { - t.Errorf("Expected empty string when no playbook found, but got %s", result) - } -} diff --git a/go.mod b/go.mod index 4604dc6..25d0bed 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/gwillem/urlfilecache v0.0.0-20230402105623-8ef3b7b67c13 github.com/ieee0824/go-deepmerge v0.0.0-20170912170951-7ec7dbbd5a1f github.com/karrick/gobls v1.3.5 + github.com/klauspost/compress v1.17.8 github.com/mitchellh/mapstructure v1.5.0 github.com/nikolalohinski/gonja v1.5.0 github.com/pkg/sftp v1.13.5 @@ -37,7 +38,6 @@ require ( github.com/goph/emperror v0.17.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.17.8 // indirect github.com/konsorten/go-windows-terminal-sequences v1.0.1 // indirect github.com/kr/fs v0.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect diff --git a/go.sum b/go.sum index dc823ed..a36950b 100644 --- a/go.sum +++ b/go.sum @@ -157,8 +157,6 @@ github.com/goph/emperror v0.17.1 h1:6lOybhIvG/BB6VGoWfdv30FVZeZFBBZ9VvgzGXLVkyY= github.com/goph/emperror v0.17.1/go.mod h1:+ZbQ+fUNO/6FNiUo0ujtMjhgad9Xa6fQL9KhH4LNHic= github.com/gwillem/go-buildversion v0.0.0-20230402114832-b1addefa8764 h1:GL/gS9rv5F1oBgQwbT+pl0HefNbDZLjbcWmDNT/pUOU= github.com/gwillem/go-buildversion v0.0.0-20230402114832-b1addefa8764/go.mod h1:yf2A6rXn1ptErQTutA0vpEDZJG6E0b/aUnMnXelY5tU= -github.com/gwillem/go-simplelog v0.3.2-0.20240416140709-da7b4f7d631f h1:4Ny6GTNmMdBMwZYTC9BQKKFZcIoHfg0WoGstBI9Qju8= -github.com/gwillem/go-simplelog v0.3.2-0.20240416140709-da7b4f7d631f/go.mod h1:VzaKnjEPW0JUVseBHP2c4w1kOxMG+TZXLeuT3xWpnbA= github.com/gwillem/go-simplelog v0.3.2-0.20240425201514-40a6b7d1bcbb h1:Ofmn80/bLvFG9NGVj4sdUVC+n69V/hxPg1UaLuiJPfA= github.com/gwillem/go-simplelog v0.3.2-0.20240425201514-40a6b7d1bcbb/go.mod h1:VzaKnjEPW0JUVseBHP2c4w1kOxMG+TZXLeuT3xWpnbA= github.com/gwillem/urlfilecache v0.0.0-20230402105623-8ef3b7b67c13 h1:Kuu6BZRS1uaSatweU/S5pWKkwt+QFCMb2QjJrWcvTWA= diff --git a/internal/vault/age.go b/internal/vault/age.go index a4fdb65..011bd6c 100644 --- a/internal/vault/age.go +++ b/internal/vault/age.go @@ -9,11 +9,12 @@ import ( "filippo.io/age" log "github.com/gwillem/go-simplelog" + "github.com/gwillem/whip/internal/fsutil" ) const ( ageEnv = "WHIP_KEY" - ageEnvScript = "whip-secret" + ageEnvScript = ".whip/secret.sh" ) // headerGPG = []byte{0x85, 0x01, 0x8C, 0x03, 0x93, 0xE5, 0x4C, 0x74, 0x67, 0x58, 0x3E, 0x45} @@ -77,10 +78,13 @@ func (v *ageVault) getID() (id *age.X25519Identity, err error) { } keyStr := os.Getenv(ageEnv) if keyStr == "" { - keyStr, err = readFromScript(ageEnvScript) - if err != nil { - log.Error(err) - return nil, err + if sp := fsutil.FindAncestorPath(ageEnvScript); sp != "" { + log.Debug("Using script", sp, "to generate vault key") + keyStr, err = readFromScript(sp) + if err != nil { + log.Error(err) + return nil, err + } } } if keyStr != "" { @@ -92,7 +96,7 @@ func (v *ageVault) getID() (id *age.X25519Identity, err error) { } id, _ = age.GenerateX25519Identity() - return nil, fmt.Errorf("no $%s set, here's a new one: %s", ageEnv, id) + return nil, fmt.Errorf("no $%s set, here's a new one: %s\n(or create .whip/secret.sh to generate it dynamically)", ageEnv, id) } func (v *ageVault) Ready() bool { @@ -113,7 +117,7 @@ func readFromScript(path string) (string, error) { return "", fmt.Errorf("script %s is not executable", path) } - data, err := exec.Command("./" + path).CombinedOutput() + data, err := exec.Command(path).CombinedOutput() data = bytes.TrimSpace(data) // fmt.Printf("got '%s'\n", string(data)) if err != nil { diff --git a/internal/vault/convert.go b/internal/vault/convert.go index 59f0a4c..60ee665 100644 --- a/internal/vault/convert.go +++ b/internal/vault/convert.go @@ -23,7 +23,7 @@ func ConvertAnsibleToWhip(root string) error { counter := 0 // walk over the root and find ansible encrypted files - err := fsutil.Walk(root, func(path string, info os.FileInfo, err error) error { + err := afs.Walk(root, func(path string, info os.FileInfo, err error) error { if err != nil || info.IsDir() { return err } diff --git a/internal/vault/vault.go b/internal/vault/vault.go index fb92027..b24fed1 100644 --- a/internal/vault/vault.go +++ b/internal/vault/vault.go @@ -21,7 +21,7 @@ var ( allVaulters = []Vaulter{&ageVault{}, &ansibleVault{}} magicSize = findMagicSize() fs = afero.NewOsFs() - fsutil = afero.Afero{Fs: fs} + afs = afero.Afero{Fs: fs} ) const (