ork

package module
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 12, 2026 License: AGPL-3.0 Imports: 5 Imported by: 0

README

Ork

Ork is a Go package for SSH-based server automation. Think of it like Ansible, but in Go - you define Nodes (remote servers) and run commands or playbooks against them.

Installation

go get github.com/dracory/ork

Quick Start

The core concept is the Node - a representation of a remote server:

package main

import (
    "log"
    "github.com/dracory/ork"
    "github.com/dracory/ork/config"
)

func main() {
    // Create a node (remote server) - multiple ways:
    
    // Option 1: From host (most common)
    node := ork.NewNodeForHost("server.example.com")
    
    // Option 2: From config (useful for complex setups)
    cfg := config.NodeConfig{
        SSHHost: "server.example.com",
        SSHPort: "22",
    }
    node := ork.NewNodeFromConfig(cfg)
    
    // Run a command
    output, err := node.RunCommand("uptime")
    if err != nil {
        log.Fatal(err)
    }
    log.Println(output)
}

Configuration

Use the fluent API to configure connection settings:

// From host
node := ork.NewNodeForHost("server.example.com").
    SetPort("2222").
    SetUser("deploy").
    SetKey("production.prv")

output, err := node.RunCommand("uptime")

Persistent Connections

For multiple operations, establish a persistent connection:

node := ork.NewNodeForHost("server.example.com").
    SetPort("2222").
    SetUser("deploy")

if err := node.Connect(); err != nil {
    log.Fatal(err)
}
defer node.Close()

// These commands reuse the same SSH connection
output1, _ := node.RunCommand("uptime")
output2, _ := node.RunCommand("df -h")

Playbooks

Run pre-built automation tasks (playbooks) against a node:

node := ork.NewNodeForHost("server.example.com").
    SetArg("username", "alice").
    SetArg("shell", "/bin/bash")

result := node.RunPlaybook(playbooks.NewUserCreate())
if result.Error != nil {
    log.Fatalf("Playbook failed: %v", result.Error)
}
if result.Changed {
    log.Printf("User created: %s", result.Message)
} else {
    log.Println("User already exists - no changes made")
}
Available Playbooks
ork Package playbook Package String Args Description
PlaybookPing IDPing ping - Check SSH connectivity
PlaybookAptUpdate IDAptUpdate apt-update - Refresh package database
PlaybookAptUpgrade IDAptUpgrade apt-upgrade - Install available updates
PlaybookAptStatus IDAptStatus apt-status - Show available updates
PlaybookReboot IDReboot reboot - Reboot server
PlaybookSwapCreate IDSwapCreate swap-create size (GB) Create swap file
PlaybookSwapDelete IDSwapDelete swap-delete - Remove swap file
PlaybookSwapStatus IDSwapStatus swap-status - Show swap status
PlaybookUserCreate IDUserCreate user-create username Create user with sudo
PlaybookUserDelete IDUserDelete user-delete username Delete user
PlaybookUserStatus IDUserStatus user-status username (opt) Show user info

Idempotency

All playbooks now support idempotent execution. Use RunPlaybook() to see whether any changes were actually made:

// RunPlaybook returns detailed result information
result := node.RunPlaybook(playbooks.NewAptUpgrade())
if result.Error != nil {
    log.Fatal(result.Error)
}

if result.Changed {
    log.Printf("Changes made: %s", result.Message)
} else {
    log.Println("No changes needed - system already in desired state")
}
Result Structure
type Result struct {
    Changed bool              // Whether changes were made
    Message string            // Human-readable description
    Details map[string]string // Additional information
    Error   error             // Non-nil if execution failed
}
Direct Playbook Access (Advanced)

For programmatic playbook handling, use the playbook package directly:

import (
    "github.com/dracory/ork/playbook"
    "github.com/dracory/ork/playbooks"
)

// Execute directly with config
aptUpgrade := playbooks.NewAptUpgrade()
aptUpgrade.SetConfig(cfg)
result := aptUpgrade.Run()

// Or check before running
pb := playbooks.NewSwapCreate()
pb.SetConfig(cfg)
needsChange, _ := pb.Check()
if !needsChange {
    log.Println("Swap already exists, skipping...")
    return
}

Advanced Usage

Inspecting Configuration
node := ork.NewNodeForHost("server.example.com").
    SetPort("2222").
    SetUser("deploy")

fmt.Printf("Host: %s\n", node.GetConfig().SSHHost)
fmt.Printf("Port: %s\n", node.GetConfig().SSHPort)
fmt.Printf("User: %s\n", node.GetConfig().RootUser)

// Get full config for integration with internal packages
cfg := node.GetConfig()
Custom Playbooks

Extend Ork with custom automation tasks by implementing the Playbook interface:

Custom Playbooks with Full Idempotency

For full idempotency support, implement all methods:

type MyCustomPlaybook struct{}

func (p *MyCustomPlaybook) GetID() string { return "my-task" }
func (p *MyCustomPlaybook) Description() string { return "Does something" }

// Check() - returns true if changes needed
func (p *MyCustomPlaybook) Check(cfg config.NodeConfig) (bool, error) {
    // Check if already configured
    output, _ := ssh.RunOnce(cfg.SSHHost, cfg.SSHPort, cfg.RootUser, cfg.SSHKey, "cat /etc/my-config")
    return !strings.Contains(output, "configured"), nil
}

// Run() - execute and return Result
func (p *MyCustomPlaybook) Run(cfg config.NodeConfig) playbook.Result {
    needsChange, _ := p.Check(cfg)
    if !needsChange {
        return playbook.Result{
            Changed: false,
            Message: "Already configured",
        }
    }
    
    // Apply changes...
    _, err := ssh.RunOnce(cfg.SSHHost, cfg.SSHPort, cfg.RootUser, cfg.SSHKey, "setup-command")
    if err != nil {
        return playbook.Result{Changed: false, Error: err}
    }
    
    return playbook.Result{
        Changed: true,
        Message: "Configuration applied",
    }
}

Internal Packages

For advanced use cases or when you need fine-grained control, you can use the internal packages directly:

  • ssh - SSH connection utilities and command execution
  • config - Configuration types for remote operations
  • playbook - Base interfaces and registry for organizing playbooks
  • playbooks - Reusable playbook implementations (ping, apt, reboot, swap, user)

For advanced use cases, use the internal packages directly:

package main

import (
    "log"

    "github.com/dracory/ork/config"
    "github.com/dracory/ork/playbooks"
)

func main() {
    cfg := config.NodeConfig{
        SSHHost:  "db3.sinevia.com",
        SSHPort:  "40022",
        SSHKey:   "2024_sinevia.prv",
        RootUser: "root",
    }

    // Ping server to check connectivity
    ping := playbooks.NewPing()
    ping.SetConfig(cfg)
    result := ping.Run()
    if result.Error != nil {
        log.Fatal(result.Error)
    }

    // Update packages
    aptUpdate := playbooks.NewAptUpdate()
    aptUpdate.SetConfig(cfg)
    result = aptUpdate.Run()
    if result.Error != nil {
        log.Fatal(result.Error)
    }

    // Create a 2GB swap file
    cfg.Args = map[string]string{"size": "2"}
    swapCreate := playbooks.NewSwapCreate()
    swapCreate.SetConfig(cfg)
    result = swapCreate.Run()
    if result.Error != nil {
        log.Fatal(result.Error)
    }
}
Package Overview
  • ork - Main API: NodeInterface, NewNode(), NewNodeForHost(), NewNodeFromConfig(), RunCommand(), RunPlaybook(), RunPlaybookByID()
  • config - Configuration types
  • ssh - SSH client with connection management
  • playbook - Playbook interface and registry
  • playbooks - Built-in playbook implementations

License

This project is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0). You can find a copy of the license at https://www.gnu.org/licenses/agpl-3.0.en.html

For commercial use, please use my contact page to obtain a commercial license.

Documentation

Overview

Package ork provides a simple, intuitive API for SSH-based server automation.

The core concept is the Node - a representation of a remote server that you can connect to and run commands or playbooks against.

Basic usage:

node := ork.NewNode("server.example.com")
output, err := node.Run("uptime")

With configuration:

node := ork.NewNode("server.example.com").
    SetPort("2222").
    SetUser("deploy")
output, err := node.Run("uptime")

Persistent connections for multiple operations:

node := ork.NewNode("server.example.com")
if err := node.Connect(); err != nil {
    log.Fatal(err)
}
defer node.Close()

output1, _ := node.Run("uptime")
output2, _ := node.Run("df -h")

Running playbooks:

node := ork.NewNode("server.example.com").
    SetArg("username", "alice")
err := node.Playbook("user-create")

For advanced use cases, the internal packages remain accessible:

  • config - Configuration types
  • ssh - SSH client
  • playbook - Playbook interface and registry
  • playbooks - Built-in playbook implementations

Index

Constants

View Source
const (
	// PlaybookPing checks SSH connectivity
	PlaybookPing = playbook.IDPing

	// PlaybookAptUpdate refreshes the package database
	PlaybookAptUpdate = playbook.IDAptUpdate

	// PlaybookAptUpgrade installs available updates
	PlaybookAptUpgrade = playbook.IDAptUpgrade

	// PlaybookAptStatus shows available updates
	PlaybookAptStatus = playbook.IDAptStatus

	// PlaybookReboot reboots the server
	PlaybookReboot = playbook.IDReboot

	// PlaybookSwapCreate creates a swap file (requires "size" arg in GB)
	PlaybookSwapCreate = playbook.IDSwapCreate

	// PlaybookSwapDelete removes the swap file
	PlaybookSwapDelete = playbook.IDSwapDelete

	// PlaybookSwapStatus shows swap status
	PlaybookSwapStatus = playbook.IDSwapStatus

	// PlaybookUserCreate creates a user with sudo (requires "username" arg)
	PlaybookUserCreate = playbook.IDUserCreate

	// PlaybookUserDelete deletes a user (requires "username" arg)
	PlaybookUserDelete = playbook.IDUserDelete

	// PlaybookUserStatus shows user info (accepts optional "username" arg)
	PlaybookUserStatus = playbook.IDUserStatus
)

Playbook ID constants for use with RunPlaybook. These constants provide compile-time safety and IDE autocomplete for playbook IDs. They are aliases to the constants in the playbook package.

Example:

node := ork.NewNodeForHost("server.example.com")
err := node.RunPlaybook(ork.PlaybookPing)

Variables

This section is empty.

Functions

This section is empty.

Types

type NodeInterface

type NodeInterface interface {

	// GetArg retrieves a single argument value by key.
	// Returns empty string if the argument is not set.
	//
	// Example:
	//
	//	node := ork.NewNode("server.example.com").SetArg("username", "alice")
	//	fmt.Println(node.GetArg("username"))  // Output: alice
	GetArg(key string) string

	// GetArgs returns a copy of the entire arguments map.
	// Modifying the returned map will not affect the node's internal state.
	//
	// Example:
	//
	//	node := ork.NewNode("server.example.com").SetArg("username", "alice")
	//	args := node.GetArgs()
	//	fmt.Println(args["username"])  // Output: alice
	GetArgs() map[string]string

	// GetConfig returns a copy of the underlying config.NodeConfig.
	// This allows integration with code that uses the config package directly.
	// The returned configuration includes all accumulated settings (host, port, user, key, args).
	//
	// Example:
	//
	//	node := ork.NewNode("server.example.com").
	//	    SetPort("2222").
	//	    SetUser("deploy")
	//	cfg := node.GetConfig()
	//	fmt.Printf("Connecting to %s\n", cfg.SSHAddr())
	GetConfig() config.NodeConfig

	// GetHost returns the configured SSH host (hostname or IP address).
	//
	// Example:
	//
	//	node := ork.NewNode("server.example.com")
	//	fmt.Println(node.GetHost())  // Output: server.example.com
	GetHost() string

	// GetUser returns the configured SSH user.
	// Returns "root" if not explicitly set.
	//
	// Example:
	//
	//	node := ork.NewNode("server.example.com").SetUser("deploy")
	//	fmt.Println(node.GetUser())  // Output: deploy
	GetUser() string

	// GetKey returns the configured SSH private key filename.
	// Returns "id_rsa" if not explicitly set.
	//
	// Example:
	//
	//	node := ork.NewNode("server.example.com").SetKey("production.prv")
	//	fmt.Println(node.GetKey())  // Output: production.prv
	GetKey() string

	// GetPort returns the configured SSH port.
	// Returns "22" if not explicitly set.
	//
	// Example:
	//
	//	node := ork.NewNode("server.example.com").SetPort("2222")
	//	fmt.Println(node.GetPort())  // Output: 2222
	GetPort() string

	// SetPort sets the SSH port for the connection.
	// Returns the NodeInterface to enable method chaining.
	// Default is "22" if not set.
	//
	// Example:
	//
	//	node := ork.NewNode("server.example.com").SetPort("2222")
	SetPort(port string) NodeInterface

	// SetUser sets the SSH user for the connection.
	// Returns the NodeInterface to enable method chaining.
	// Default is "root" if not set.
	//
	// Example:
	//
	//	node := ork.NewNode("server.example.com").SetUser("deploy")
	SetUser(user string) NodeInterface

	// SetKey sets the SSH private key filename for authentication.
	// The key is resolved to ~/.ssh/<keyname>.
	// Returns the NodeInterface to enable method chaining.
	// Default is "id_rsa" if not set.
	//
	// Example:
	//
	//	node := ork.NewNode("server.example.com").SetKey("production.prv")
	SetKey(key string) NodeInterface

	// SetArg adds a single argument to the arguments map.
	// This adds to existing arguments without replacing them.
	// Arguments are passed to playbooks for configuration.
	// Returns the NodeInterface to enable method chaining.
	//
	// Example:
	//
	//	node := ork.NewNode("server.example.com").
	//	    SetArg("username", "alice").
	//	    SetArg("shell", "/bin/bash")
	SetArg(key, value string) NodeInterface

	// SetArgs replaces the entire arguments map with the provided map.
	// Any existing arguments are discarded.
	// Arguments are passed to playbooks for configuration.
	// Returns the NodeInterface to enable method chaining.
	//
	// Example:
	//
	//	args := map[string]string{
	//	    "username": "alice",
	//	    "shell": "/bin/bash",
	//	}
	//	node := ork.NewNode("server.example.com").SetArgs(args)
	SetArgs(args map[string]string) NodeInterface

	// Connect establishes a persistent SSH connection to the remote server.
	// The connection is maintained until Close() is called.
	// Subsequent RunCommand() and RunPlaybook() calls will reuse this connection.
	//
	// Returns an error if the connection fails, with a descriptive message
	// including the host and port.
	//
	// Example:
	//
	//	node := ork.NewNode("server.example.com")
	//	if err := node.Connect(); err != nil {
	//	    log.Fatalf("Failed to connect: %v", err)
	//	}
	//	defer node.Close()
	Connect() error

	// Close terminates the persistent SSH connection and releases resources.
	// After calling Close(), IsConnected() will return false.
	// It is safe to call Close() multiple times or on a non-connected node.
	//
	// Returns an error if closing the connection fails.
	//
	// Example:
	//
	//	node := ork.NewNode("server.example.com")
	//	node.Connect()
	//	defer node.Close()
	Close() error

	// IsConnected returns true if a persistent SSH connection is currently active.
	// Returns false if Connect() has not been called or if Close() was called.
	//
	// Example:
	//
	//	node := ork.NewNode("server.example.com")
	//	fmt.Println(node.IsConnected())  // Output: false
	//	node.Connect()
	//	fmt.Println(node.IsConnected())  // Output: true
	//	node.Close()
	//	fmt.Println(node.IsConnected())  // Output: false
	IsConnected() bool

	// RunCommand executes a shell command on the remote server.
	// If a persistent connection is active (via Connect()), it is reused.
	// Otherwise, a one-time connection is created for this command.
	//
	// Returns the command output as a string and any error that occurred.
	// If the command execution fails, the error message includes the command
	// and failure reason.
	//
	// Example with persistent connection:
	//
	//	node := ork.NewNode("server.example.com")
	//	node.Connect()
	//	defer node.Close()
	//
	//	output1, _ := node.RunCommand("uptime")
	//	output2, _ := node.RunCommand("df -h")  // Reuses same connection
	//
	// Example without persistent connection:
	//
	//	node := ork.NewNode("server.example.com")
	//	output, err := node.RunCommand("uptime")  // Creates one-time connection
	RunCommand(cmd string) (string, error)

	// RunPlaybook executes a playbook instance directly and returns detailed result information.
	// This is the preferred method for executing playbooks as it provides idempotency support
	// through the Result.Changed field, indicating whether any changes were actually made.
	//
	// This method allows running custom or programmatically created playbooks without registry lookup.
	//
	// Example:
	//
	//	pb := playbooks.NewAptUpgrade()
	//	result := node.RunPlaybook(pb)
	//	if result.Error != nil {
	//	    log.Fatalf("Playbook failed: %v", result.Error)
	//	}
	//	if result.Changed {
	//	    log.Printf("Changes made: %s", result.Message)
	//	}
	RunPlaybook(pb playbook.PlaybookInterface) playbook.Result

	// RunPlaybookByID executes a playbook by ID from the registry.
	// Deprecated: Use RunPlaybook() instead. Run playbooks by creating the playbook
	// instance directly (e.g., playbooks.NewPing()) and passing it to RunPlaybook().
	// This provides better type safety and IDE autocomplete support.
	//
	// Optional PlaybookOptions can be provided to override node-level arguments for this
	// specific execution. This allows per-playbook variable scoping without affecting
	// the node's state.
	RunPlaybookByID(id string, opts ...playbook.PlaybookOptions) playbook.Result
}

NodeInterface defines the contract for managing a remote server via SSH. Implementations must support configuration via setter methods, connection management, command execution, and playbook execution.

The interface provides two patterns for configuration:

  • Fluent builder pattern: Chain setter methods for readable configuration
  • Getter methods: Inspect current configuration state

Connection management is explicit, allowing users to control the SSH connection lifecycle. Operations (RunCommand, RunPlaybook) can work with or without a persistent connection.

Example usage with fluent builder pattern:

node := ork.NewNode("server.example.com").
    SetPort("2222").
    SetUser("deploy").
    SetKey("production.prv")

if err := node.Connect(); err != nil {
    log.Fatal(err)
}
defer node.Close()

output, err := node.RunCommand("uptime")
if err != nil {
    log.Fatal(err)
}
fmt.Println(output)

Example usage without persistent connection:

node := ork.NewNode("server.example.com")
output, err := node.RunCommand("uptime")  // Creates one-time connection

Example usage with playbooks:

node := ork.NewNode("server.example.com").
    SetArg("username", "alice").
    SetArg("shell", "/bin/bash")

if err := node.RunPlaybook("user-create"); err != nil {
    log.Fatal(err)
}

func NewNode

func NewNode() NodeInterface

NewNode creates a new Node with default configuration values. Unlike NewNodeForHost, this function takes no arguments and creates a node with an empty host. Use SetArg or SetArgs to configure the node.

Default values:

  • Host: "" (empty - must be set before connecting)
  • Port: "22"
  • User: "root"
  • Key: "id_rsa"
  • Args: empty map

Example:

node := ork.NewNode().
    SetHost("server.example.com").
    SetPort("2222").
    SetUser("deploy")

if err := node.Connect(); err != nil {
    log.Fatal(err)
}

func NewNodeForHost added in v0.3.0

func NewNodeForHost(host string) NodeInterface

NewNode creates a new Node with default configuration values. The host parameter specifies the remote server (hostname or IP address).

Default values:

  • Port: "22"
  • User: "root"
  • Key: "id_rsa"
  • Args: empty map

The returned NodeInterface can be configured using setter methods (SetPort, SetUser, SetKey, SetArg, SetArgs) before connecting.

Example:

node := ork.NewNode("server.example.com")
// Equivalent to:
// Node{
//     cfg: config.NodeConfig{
//         SSHHost: "server.example.com",
//         SSHPort: "22",
//         RootUser: "root",
//         SSHKey: "id_rsa",
//         Args: map[string]string{},
//     },
//     connected: false,
// }

Example with configuration:

node := ork.NewNode("server.example.com").
    SetPort("2222").
    SetUser("deploy").
    SetKey("production.prv")

func NewNodeFromConfig added in v0.3.0

func NewNodeFromConfig(cfg config.NodeConfig) NodeInterface

NewNodeFromConfig creates a new Node from an existing config.NodeConfig. This is useful when you have a pre-built configuration and want to create a Node from it directly.

The config is copied internally, so modifications to the original config after calling this function will not affect the Node.

Example:

cfg := config.NodeConfig{
    SSHHost:  "server.example.com",
    SSHPort:  "2222",
    RootUser: "deploy",
    SSHKey:   "production.prv",
    Args: map[string]string{"env": "production"},
}
node := ork.NewNodeFromConfig(cfg)

if err := node.Connect(); err != nil {
    log.Fatal(err)
}

Directories

Path Synopsis
Package config provides configuration types for SSH-based automation.
Package config provides configuration types for SSH-based automation.
Package playbook provides the base types and interfaces for creating automation playbooks using SSH-based remote execution.
Package playbook provides the base types and interfaces for creating automation playbooks using SSH-based remote execution.
Package playbooks provides reusable playbook implementations for common server automation tasks.
Package playbooks provides reusable playbook implementations for common server automation tasks.
Package ssh provides SSH connectivity utilities for remote server automation.
Package ssh provides SSH connectivity utilities for remote server automation.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL