Documentation
¶
Overview ¶
Example (ComparisonOldVsNew) ¶
Example_comparisonOldVsNew shows the difference between old and new patterns.
package main
import (
"fmt"
iolib "github.com/cloudposse/atmos/pkg/io"
"github.com/cloudposse/atmos/pkg/terminal"
"github.com/cloudposse/atmos/pkg/ui"
)
func main() {
ioCtx, _ := iolib.NewContext()
term := terminal.New()
formatter := ui.NewFormatter(ioCtx, term)
// ===== OLD PATTERN (being phased out) =====
// Mixed concerns - unclear where output goes.
//
// out := ui.NewOutput(ioCtx)
// out.Print("data") // Where does this go? stdout? stderr?
// out.Success("done!") // This goes to stderr, but not obvious.
// out.Markdown("# Doc") // Is this data or UI? stdout or stderr?
// ===== NEW PATTERN (preferred) =====
// Explicit channels - always clear where output goes.
// Data → stdout (explicit).
fmt.Fprintf(ioCtx.Data(), "data\n")
// UI → stderr (explicit).
successMsg := formatter.Success("done!")
fmt.Fprintf(ioCtx.UI(), "%s\n", successMsg)
// Markdown - developer chooses channel based on context.
helpMarkdown, _ := formatter.Markdown("# Documentation")
// Help text → stdout (pipeable).
fmt.Fprint(ioCtx.Data(), helpMarkdown)
// Error details → stderr (UI message).
errorMarkdown, _ := formatter.Markdown("**Error:** Something failed")
fmt.Fprint(ioCtx.UI(), errorMarkdown)
}
Example (DecisionTree) ¶
Example_decisionTree shows how to decide where to output.
package main
import (
"fmt"
iolib "github.com/cloudposse/atmos/pkg/io"
"github.com/cloudposse/atmos/pkg/terminal"
"github.com/cloudposse/atmos/pkg/ui"
)
func main() {
ioCtx, _ := iolib.NewContext()
term := terminal.New()
formatter := ui.NewFormatter(ioCtx, term)
// DECISION TREE:
// 1. WHERE should it go?
// ├─ Pipeable data (JSON, YAML, results) → ioCtx.Data()
// ├─ Human messages (status, errors, help) → ioCtx.UI()
// └─ User input → ioCtx.Input()
//
// 2. HOW should it look?
// ├─ Plain text → fmt.Fprintf(channel, text)
// ├─ Colored/styled → fmt.Fprintf(channel, formatter.Success(text))
// └─ Markdown rendered → fmt.Fprint(channel, formatter.Markdown(md))
//
// 3. WHEN to format?
// ├─ Always for UI channel → Use formatter.* methods
// ├─ Conditionally for data channel → Check term.IsTTY()
// └─ Never for piped output → Auto-handled by I/O layer
// Example: Command help.
// Help is DATA (can be saved, piped) but uses markdown formatting.
helpContent := "# atmos terraform apply\n\nApplies Terraform configuration..."
rendered, _ := formatter.Markdown(helpContent)
fmt.Fprint(ioCtx.Data(), rendered)
// Example: Processing status.
// Status is UI (human-readable only) with formatting.
status := formatter.Info("⏳ Processing 150 components...")
fmt.Fprintf(ioCtx.UI(), "%s\n", status)
// Example: Success message.
// Success is UI with semantic formatting.
msg := formatter.Success("✓ Deployment complete!")
fmt.Fprintf(ioCtx.UI(), "%s\n", msg)
// Example: JSON output.
// JSON is DATA (no formatting).
jsonString := `{"result": "success"}`
fmt.Fprintf(ioCtx.Data(), "%s\n", jsonString)
// Example: Error with explanation.
// Error is UI with markdown for rich explanation.
errorTitle := formatter.Error("Failed to load stack configuration")
errorDetails, _ := formatter.Markdown("**Reason:** Invalid YAML syntax\n\n```yaml\nstack: invalid\n```")
fmt.Fprintf(ioCtx.UI(), "%s\n\n%s\n", errorTitle, errorDetails)
}
Example (KeyPrinciples) ¶
Example_keyPrinciples demonstrates the architectural principles.
package main
import (
"fmt"
iolib "github.com/cloudposse/atmos/pkg/io"
"github.com/cloudposse/atmos/pkg/terminal"
"github.com/cloudposse/atmos/pkg/ui"
)
func main() {
ioCtx, _ := iolib.NewContext()
term := terminal.New()
formatter := ui.NewFormatter(ioCtx, term)
// KEY PRINCIPLE 1: I/O layer provides CHANNELS and CAPABILITIES.
// - Channels: Where does output go? (stdout, stderr, stdin).
// - Capabilities: What can the terminal do? (color, TTY, width).
// - NO formatting logic in I/O layer.
// Access channels.
_ = ioCtx.Data() // stdout - pipeable data.
_ = ioCtx.UI() // stderr - human messages.
_ = ioCtx.Input() // stdin - user input.
// Access capabilities.
_ = term.IsTTY(terminal.Stdout)
_ = term.ColorProfile()
_ = term.Width(terminal.Stdout)
// KEY PRINCIPLE 2: UI layer provides FORMATTING.
// - Returns formatted strings (pure functions).
// - NEVER writes to streams directly.
// - Uses I/O layer for capability detection.
// Get formatted strings.
_ = formatter.Success("Success!") // Returns string.
_ = formatter.Warning("Warning!") // Returns string.
_ = formatter.Error("Error!") // Returns string.
_, _ = formatter.Markdown("# Title") // Returns string.
// KEY PRINCIPLE 3: Application layer COMBINES both.
// - Gets formatted string from UI layer.
// - Chooses channel from I/O layer.
// - Uses fmt.Fprintf to write.
msg := formatter.Success("Done!")
fmt.Fprintf(ioCtx.UI(), "%s\n", msg)
}
Example (NewPattern) ¶
ExampleNewPattern demonstrates the new I/O and UI pattern. This shows the clear separation between I/O (channels) and UI (formatting).
package main
import (
"fmt"
iolib "github.com/cloudposse/atmos/pkg/io"
"github.com/cloudposse/atmos/pkg/terminal"
"github.com/cloudposse/atmos/pkg/ui"
)
func main() {
// ===== SETUP =====
// Create I/O context - provides channels and masking.
ioCtx, _ := iolib.NewContext()
// Create terminal instance - provides TTY detection and color capabilities.
term := terminal.New()
// Create UI formatter - provides formatting functions (returns strings).
formatter := ui.NewFormatter(ioCtx, term)
// ===== PATTERN 1: Data Output (stdout) =====
// Data channel is for pipeable output (JSON, YAML, results).
// Plain data.
fmt.Fprintf(ioCtx.Data(), "plain data output\n")
// Formatted data (markdown help text).
helpMarkdown, _ := formatter.Markdown("# Help\n\nThis is help text.")
fmt.Fprint(ioCtx.Data(), helpMarkdown)
// ===== PATTERN 2: UI Messages (stderr) =====
// UI channel is for human-readable messages.
// Plain message (asking for user confirmation).
fmt.Fprintf(ioCtx.UI(), "Would you like to continue? (y/n): ")
// Formatted success message.
successMsg := formatter.Success("✓ Configuration loaded!")
fmt.Fprintf(ioCtx.UI(), "%s\n", successMsg)
// Formatted warning.
warningMsg := formatter.Warning("⚠ Stack is using deprecated settings")
fmt.Fprintf(ioCtx.UI(), "%s\n", warningMsg)
// Formatted error.
errorMsg := formatter.Error("✗ Failed to load stack")
details := formatter.Muted(" Check your atmos.yaml syntax")
fmt.Fprintf(ioCtx.UI(), "%s\n%s\n", errorMsg, details)
// ===== PATTERN 3: Markdown for UI (stderr) =====
// Error explanation with rich formatting.
errorMarkdown, _ := formatter.Markdown("**Error:** Invalid configuration\n\n```yaml\nstack: invalid\n```")
fmt.Fprint(ioCtx.UI(), errorMarkdown)
// ===== PATTERN 4: Conditional Formatting =====
// Only show progress if stderr is a TTY.
if term.IsTTY(terminal.Stderr) {
progress := formatter.Info("⏳ Processing 150 components...")
fmt.Fprintf(ioCtx.UI(), "%s\n", progress)
}
// ===== PATTERN 5: Using Terminal Capabilities =====
// Set terminal title (if supported).
term.SetTitle("Atmos - Processing")
defer term.RestoreTitle()
// Check color support.
if formatter.SupportsColor() {
coloredMsg := formatter.Success("Color is supported!")
fmt.Fprintf(ioCtx.UI(), "%s\n", coloredMsg)
}
// Get terminal width for adaptive formatting.
width := term.Width(terminal.Stdout)
if width > 0 {
fmt.Fprintf(ioCtx.UI(), "Terminal width: %d columns\n", width)
}
}
Index ¶
- Constants
- Variables
- func Initialize() error
- func MaskWriter(w stdio.Writer) stdio.Writer
- func RegisterPattern(pattern string) error
- func RegisterSecret(secret string)
- func RegisterValue(value string)
- type Config
- type Context
- type ContextOption
- type Masker
- type Stream
- type Streams
- type TerminalWriter
Examples ¶
Constants ¶
const (
// MaskReplacement is the string used to replace masked values.
MaskReplacement = "***MASKED***"
)
Variables ¶
var ( // Data is the global writer for machine-readable output (stdout). // All writes are automatically masked based on registered secrets. // Use this for JSON, YAML, or any output meant for piping/automation. // // Example: // fmt.Fprintf(io.Data, `{"version": "%s"}\n`, version) // // Safe default: Falls back to os.Stdout until Initialize() is called. Data stdio.Writer = os.Stdout // UI is the global writer for human-readable output (stderr). // All writes are automatically masked based on registered secrets. // Use this for messages, progress, logs meant for terminal display. // // Example: // fmt.Fprintf(io.UI, "Processing...\n") // logger := log.New(io.UI) // // Safe default: Falls back to os.Stderr until Initialize() is called. UI stdio.Writer = os.Stderr )
Functions ¶
func Initialize ¶
func Initialize() error
Initialize sets up the global I/O writers with automatic masking. This should be called once in cmd/root.go PersistentPreRun. If not called explicitly, it will be called automatically on first use.
func MaskWriter ¶
MaskWriter wraps any io.Writer with automatic masking. Use this when you need to write to custom file handles with masking enabled.
Example:
f, _ := os.Create("output.log")
maskedFile := io.MaskWriter(f)
fmt.Fprintf(maskedFile, "SECRET=%s\n", secret) // Automatically masked
func RegisterPattern ¶
RegisterPattern registers a regex pattern for masking. Use this to mask values matching a specific pattern.
Example:
io.RegisterPattern(`api_key=[A-Za-z0-9]+`)
func RegisterSecret ¶
func RegisterSecret(secret string)
RegisterSecret registers a secret value for masking. The secret and its common encodings (base64, URL, JSON) will be masked. This adds to the global masker used by io.Data and io.UI.
Example:
apiToken := getToken() io.RegisterSecret(apiToken) fmt.Fprintf(io.UI, "Token: %s\n", apiToken) // Automatically masked
func RegisterValue ¶
func RegisterValue(value string)
RegisterValue registers a literal value for masking. Use this for values that don't need encoding variants.
Example:
io.RegisterValue(sessionID)
Types ¶
type Config ¶
type Config struct {
// From global flags
RedirectStderr string
DisableMasking bool // --disable-masking flag for debugging
// From atmos.yaml
AtmosConfig schema.AtmosConfiguration
}
Config holds I/O configuration for channels and masking.
type Context ¶
type Context interface {
// Primary output method - ALL writes should go through this for masking
Write(stream Stream, content string) error
// Channel access - explicit and clear (deprecated - use Write() instead)
Data() stdio.Writer // stdout - for pipeable data (JSON, YAML, results)
UI() stdio.Writer // stderr - for human messages (status, errors, prompts)
Input() stdio.Reader // stdin - for user input
// Raw channels (unmasked - requires justification)
RawData() stdio.Writer // Unmasked stdout
RawUI() stdio.Writer // Unmasked stderr
// Configuration
Config() *Config
// Output masking
Masker() Masker
// Legacy compatibility (deprecated - use Data()/UI() instead)
Streams() Streams
}
Context provides access to I/O channels and masking. This is the main entry point for I/O operations.
Key Principle: I/O layer provides CHANNELS and MASKING, not formatting or terminal detection. - Channels: Where does output go? (stdout, stderr, stdin) - Masking: Secret redaction for security
ALL output must flow through Write() for automatic masking. The UI layer (pkg/ui/) handles formatting and rendering. The terminal layer (pkg/terminal/) handles TTY detection and capabilities.
func GetContext ¶
func GetContext() Context
GetContext returns the global I/O context for advanced usage. Most code should use io.Data and io.UI instead.
func NewContext ¶
func NewContext(opts ...ContextOption) (Context, error)
NewContext creates a new I/O context with default configuration.
type ContextOption ¶
type ContextOption func(*context)
ContextOption configures Context behavior.
func WithMasker ¶
func WithMasker(masker Masker) ContextOption
WithMasker sets a custom masker (for testing).
func WithStreams ¶
func WithStreams(streams Streams) ContextOption
WithStreams sets custom streams (for testing).
type Masker ¶
type Masker interface {
// RegisterValue registers a literal value to be masked.
// The value will be replaced with ***MASKED*** in all output.
RegisterValue(value string)
// RegisterSecret registers a secret with encoding variations.
// Automatically registers base64, URL, and JSON encoded versions.
RegisterSecret(secret string)
// RegisterPattern registers a regex pattern to mask.
// Returns error if pattern is invalid.
RegisterPattern(pattern string) error
// RegisterRegex registers a compiled regex pattern to mask.
RegisterRegex(pattern *regexp.Regexp)
// RegisterAWSAccessKey registers an AWS access key and attempts to mask the paired secret key.
RegisterAWSAccessKey(accessKeyID string)
// Mask applies all registered masks to the input string.
Mask(input string) string
// Clear removes all registered masks.
Clear()
// Count returns the number of registered masks.
Count() int
// Enabled returns whether masking is enabled.
Enabled() bool
}
Masker handles automatic masking of sensitive data in output.
type Stream ¶
type Stream int
Stream identifies an I/O stream for writing output. Used with Context.Write(stream, content) for centralized masking.
type Streams ¶
type Streams interface {
// Input returns the input stream (typically stdin).
Input() stdio.Reader
// Output returns the output stream (typically stdout) with automatic masking.
// Deprecated: Use Context.Data() instead.
Output() stdio.Writer
// Error returns the error stream (typically stderr) with automatic masking.
// Deprecated: Use Context.UI() instead.
Error() stdio.Writer
// RawOutput returns the unmasked output stream.
// Use only when absolutely necessary (e.g., binary output).
// Requires explicit justification in code review.
// Deprecated: Use Context.RawData() instead.
RawOutput() stdio.Writer
// RawError returns the unmasked error stream.
// Use only when absolutely necessary.
// Requires explicit justification in code review.
// Deprecated: Use Context.RawUI() instead.
RawError() stdio.Writer
}
Streams provides access to input/output streams with automatic masking. Deprecated: Use Context.Data()/UI() directly instead. This interface exists for backward compatibility during migration.
type TerminalWriter ¶
type TerminalWriter struct {
// contains filtered or unexported fields
}
TerminalWriter adapts io.Context to satisfy terminal.IOWriter interface. This avoids circular dependency while allowing terminal to write through I/O layer.
func NewTerminalWriter ¶
func NewTerminalWriter(ctx Context) *TerminalWriter
NewTerminalWriter creates a terminal-compatible writer from io.Context.