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
15 changes: 11 additions & 4 deletions context/local/advice/platform-support.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@

#### Platform Support
- Consider cross-platform compatibility
- Test on multiple operating systems when applicable
- Use Train's platform detection system
- Handle platform-specific edge cases

> **Moved.** The canonical, in-depth guide to extending Train's platform / family
> detection now lives in the shared, cross-repo product area:
>
> - **Extending platform support (worked examples):**
> [`context/shared/by-product/train/advice/platform-support.md`](../../shared/by-product/train/advice/platform-support.md)
> - **Platform detection design:**
> [`context/shared/by-product/train/design/platform-detection.md`](../../shared/by-product/train/design/platform-detection.md)
>
> This file is retained only as a pointer so existing references keep working.

30 changes: 15 additions & 15 deletions context/local/advice/transport-development.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
### Transport Development

#### Transport Development Guidelines
- Inherit from `Train::Plugins::Transport`
- Implement required methods: `connection`, `options`
- Use Train's connection management patterns
- Handle platform-specific requirements
- Provide appropriate error handling
- Support Train's audit logging when applicable
> **Moved.** The canonical, in-depth guidance for authoring a Train transport /
> plugin now lives in the shared, cross-repo product area (it is integration
> guidance consumed by plugin authors in separate repos):
>
> - **Transport development reference & checklist:**
> [`context/shared/by-product/train/advice/transport-development.md`](../../shared/by-product/train/advice/transport-development.md)
> - **End-to-end plugin authoring hub:**
> [`context/shared/by-product/train/advice/writing-a-plugin.md`](../../shared/by-product/train/advice/writing-a-plugin.md)
> - **Connection contract:**
> [`context/shared/by-product/train/interfaces/connection-api.md`](../../shared/by-product/train/interfaces/connection-api.md)
> - **Plugin archetypes (OS-command vs API):**
> [`context/shared/by-product/train/design/plugin-archetypes.md`](../../shared/by-product/train/design/plugin-archetypes.md)
>
> This file is retained only as a pointer so existing references keep working.


#### Transport Development
- Follow the plugin architecture pattern
- Use Train's connection management
- Implement proper platform detection
- Handle authentication securely
- Support Train's file and command interfaces
- Provide meaningful error messages
91 changes: 91 additions & 0 deletions context/local/architecture/diagrams/01-component-context.puml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
@startuml 01-component-context
!include _style.puml
title Train — Component / Context View

' How Train sits between its consumers and its plugins.

package "Consumers" {
package "Chef InSpec" {
[Inspec::Backend] as InspecBackend <<core>>
[InSpec resources\n(command, file, os, ...)] as InspecResources
[Inspec::Config\n(unpack_train_credentials)] as InspecConfig
}
package "Chef Infra Client" {
[Chef target_io\n(TargetIO::File / Dir / etc.)] as ChefTargetIO
[Chef::Application] as ChefApp
}
}

package "Train core (train gem)" {
[Train.create /\nload_transport\n(registry)] as TrainEntry <<core>>
[Transport\n(plugin base)] as Transport <<core>>
[BaseConnection] as BaseConnection <<abstract>>
[Options\n(Class/InstanceOptions)] as Options <<core>>
[Platforms + Detect\n(Scanner, Specifications)] as Platforms <<core>>
[File / Stat] as File <<core>>
[AuditLog] as AuditLog <<core>>
[errors] as Errors <<core>>
}

package "Plugins" {
package "OS-command transports" <<Rectangle>> {
[local *] as PLocal <<oscmd>>
[ssh *] as PSsh <<oscmd>>
[train-winrm] as PWinrm <<oscmd>>
}
package "OS-command by history\n(should be API — anti-pattern)" <<Rectangle>> {
[docker / podman *] as PDocker <<historical>>
[train-kubernetes] as PK8s <<historical>>
}
package "API transports" <<Rectangle>> {
[azure / gcp / vmware *] as PAzure <<api>>
[train-aws] as PAws <<api>>
[train-rest] as PRest <<api>>
}
}

note top of PLocal
* = in-repo transport (ships in train / train-core).
Bare "train-*" names are **external plugin gems**,
installed separately and discovered via RubyGems.
See design/architecture.md#in-repo-transports-vs-external-plugin-gems.
(winrm is external despite the stale train-core gemspec text.)
end note

InspecConfig --> TrainEntry : credentials hash
InspecBackend --> TrainEntry : Train.create(name, opts)
InspecBackend --> BaseConnection : holds connection
InspecResources --> BaseConnection : run_command / file / os
ChefApp --> TrainEntry : Train.create
ChefTargetIO --> BaseConnection : file / run_command\n(reads transport_options[:sudo/:user])

TrainEntry --> Transport : resolves & instantiates
Transport --> BaseConnection : #connection
BaseConnection --> Options : merge/validate
BaseConnection --> AuditLog : log cmd/file (if enabled)
BaseConnection --> Platforms : #platform (lazy scan)
BaseConnection --> File : #file
BaseConnection ..> Errors : raises

Transport <|-- PLocal
Transport <|-- PSsh
Transport <|-- PDocker
Transport <|-- PWinrm
Transport <|-- PK8s
Transport <|-- PAzure
Transport <|-- PAws
Transport <|-- PRest

note bottom of Platforms
API transports skip real detection and
call force_platform! instead of scanning.
end note

note right of PK8s
docker/podman/kubernetes shell out to a CLI
(docker/podman/kubectl exec) that fronts an API.
They are OS-command by history but should be
API transports. Lessons, not models.
end note

@enduml
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
88 changes: 88 additions & 0 deletions context/local/architecture/diagrams/02-core-classes.puml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
@startuml 02-core-classes
!include _style.puml
title Train — Core Classes

top to bottom direction

class "Train" as Train <<core>> {
+self.create(name, *args) : Transport
+self.options(name) : Hash
+self.load_transport(name) : Class
+self.target_config(config) : Hash
+self.unpack_target_from_uri(uri, opts) : Hash
+self.validate_backend(creds, default="local") : String
}

class "Plugins" as Plugins <<core>> {
+self.registry : Hash<String,Class>
+self.plugin(version=1) : Class
}

class "Plugins::Transport" as Transport <<core>> {
+initialize(options={})
+connection(_options=nil) : BaseConnection {abstract}
+self.name(name)
--
+self.option(name, conf, &blk)
+self.default_options : Hash
+validate_options(opts)
}

abstract class "BaseConnection" as BaseConnection <<abstract>> {
+initialize(options=nil)
+run_command(cmd, opts={}, &blk) : CommandResult
+file(path, *args) : File
+platform() : Platform
+upload(locals, remote)
+download(remotes, local)
+force_platform!(name, details=nil) : Platform
+enable_cache(type) / disable_cache(type)
+close() / wait_until_ready() / login_command()
--
#run_command_via_connection(cmd, [opts], &blk) {abstract}
#file_via_connection(path, *args) {abstract}
--
-@cache_enabled : {file:true, command:false, api_call:false}
-@audit_log : AuditLog
}

class "Options" as Options <<core>> {
ClassOptions: option / default_options / include_options
InstanceOptions: merge_options / validate_options
}

class "CommandResult" as CommandResult <<core>> {
+stdout
+stderr
+exit_status
}

class "File" as File <<abstract>> {
DATA_FIELDS = exist? mode owner group
uid gid content mtime size selinux_label path
+to_json / type / directory? / symlink?
+md5sum / sha256sum
}

class "AuditLog" as AuditLog <<core>> {
+self.create(opts) : Logger
+info(entry)
}

Train --> Plugins : registry lookup
Plugins --> Transport : plugin(1) base
Transport ..> Options : Options.attach(self)
Transport --> BaseConnection : #connection returns
BaseConnection ..> Options : include InstanceOptions
BaseConnection --> CommandResult : run_command returns
BaseConnection --> File : file returns
BaseConnection --> AuditLog : logs when enabled

note right of BaseConnection
run_command dispatches on the
arity of run_command_via_connection
(1 vs 2) for plugin back-compat.
Caching: file=on, command/api_call=off.
end note

@enduml
1 change: 1 addition & 0 deletions context/local/architecture/diagrams/02-core-classes.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
68 changes: 68 additions & 0 deletions context/local/architecture/diagrams/03-transport-hierarchy.puml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
@startuml 03-transport-hierarchy
!include _style.puml
title Train — Transport / Connection Hierarchy (two archetypes)

abstract class "BaseConnection" as Base <<abstract>> {
+run_command / file / platform
#run_command_via_connection {abstract}
#file_via_connection {abstract}
}

package "OS-command transports\n(implement run_command_via_connection + file_via_connection,\nreal platform detection)" <<Rectangle>> {
class "Local::Connection" as Local <<oscmd>>
class "SSH::Connection" as Ssh <<oscmd>>
class "TrainPlugins::Winrm::Connection" as Winrm <<oscmd>>
}

package "OS-command by history — should be API\n(archetype mismatch: shell out to a CLI that fronts an API)" <<Rectangle>> {
class "Docker::Connection" as Docker <<historical>>
class "Podman::Connection" as Podman <<historical>>
class "TrainPlugins::Kubernetes::Connection" as K8s <<historical>>
}

package "API transports\n(force_platform!, bespoke client surface,\nno file/command primitives)" <<Rectangle>> {
class "Aws::Connection" as Aws <<api>>
class "Azure::Connection" as Azure <<api>>
class "Gcp::Connection" as Gcp <<api>>
class "VMware::Connection" as Vmware <<api>>
class "TrainPlugins::Rest::Connection" as Rest <<api>>
}

Base <|-- Local
Base <|-- Ssh
Base <|-- Docker
Base <|-- Podman
Base <|-- Winrm
Base <|-- K8s
Base <|-- Aws
Base <|-- Azure
Base <|-- Gcp
Base <|-- Vmware
Base <|-- Rest

note right of Aws
API archetype exposes clients like
aws_client(klass) / azure_client;
callers must know the transport type.
Impl anti-pattern observed here:
- ENV export of AWS creds
end note

note left of Local
OS-command archetype fulfills the
universal contract: run_command + file
work against any resource unchanged.
Correctly archetyped: these target a
real OS shell (local/ssh/winrm).
end note

note bottom of K8s
Mis-archetyped: docker/podman/kubernetes
target API-first systems but shell out to
docker/podman/kubectl exec. Should be API
transports. train-kubernetes also calls
exit() on connect error and swallows ENOENT.
Two axes: archetype fit + impl quality.
end note

@enduml
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
82 changes: 82 additions & 0 deletions context/local/architecture/diagrams/04-platform-detection.puml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
@startuml 04-platform-detection
!include _style.puml
title Train — Platform Detection Subsystem

class "Train::Platforms" as Platforms <<core>> {
+self.name(name, cond={}) : Platform
+self.family(name, cond={}) : Family
+self.list / families
+self.top_platforms
+self.export
}

class "Detect" as Detect <<core>> {
+self.scan(backend) : Platform
}

class "Detect::Scanner" as Scanner <<core>> {
+initialize(backend)
+scan : Platform
-scan_children(parent)
-scan_family_children(plat)
-check_condition(condition)
-get_platform(plat)
}

class "Detect::Specifications::OS" as SpecOS <<core>> {
+self.load
}
class "Detect::Specifications::Api" as SpecApi <<core>> {
+self.load
}

class "Platform" as Platform <<core>> {
+name / title / family
+backend / platform : Hash
+family_hierarchy
+find_family_hierarchy
+add_platform_methods
+uuid
}

class "Family" as Family <<core>> {
+name / title
+children / families
}

class "Common" as Common <<core>> {
+detect(&block)
+family / families(*)
+platform(name)
}

class "Detect::UUID" as UUID <<core>> {
+find_or_create_uuid
}

Detect --> Scanner : new(backend).scan
Scanner --> Platforms : top_platforms
Scanner --> Platform : get_platform / add_platform_methods
Platforms --> Platform : creates / registry (list)
Platforms --> Family : creates / registry (families)
Platform ..|> Common
Family ..|> Common
SpecOS ..> Platforms : declares family/platform tree
SpecApi ..> Platforms : declares API platforms
Platform --> UUID : lazy uuid

note bottom of Scanner
scan() instance_evals each platform's
detect block against the backend, walking
top_platforms -> children -> families.
Probes run via run_command + file on the
backend (OSCommon helpers).
end note

note right of SpecApi
API transports register here and are
selected through force_platform!,
bypassing the command/file probing.
end note

@enduml
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading