Shep Task Domain Types Plan
Not logged in

Overview

Tasks are the fundamental unit of work in Ansible. A task calls a module with arguments to achieve a desired state. Tasks appear in playbooks, task files, handlers, and blocks.

This document plans the domain types for tasks. Unlike inventory and playbook which have dedicated parser plugins, task parsing is embedded in the playbook plugin. However, tasks need their own domain schema because they're referenced by playbooks, handlers, and potentially standalone task files.

Reference: docs/ansible-schema/tasks.json


Decisions to Make

Question Options
Separate plugin? No - task parsing is part of playbook plugin
Standalone task CLI? Maybe - shep task validate -f tasks/main.yml
Module args format Key-value pairs as [Var] (same as inventory vars)
Block support Yes - blocks are task containers with rescue/always

Domain Analysis

Task Structure (from schema)

A task can have:

Core:

Control flow:

Privilege:

Delegation:

Error handling:

Notification:

Execution:

Metadata:

Environment:

Block Structure

A block groups tasks with error handling:

Blocks can be nested and have most task attributes (when, become, etc.).


FlatBuffers Schema Design

Domain types (flatbuffers/domain/task.fbs):

namespace shep.domain;

// Reuse Var from inventory.fbs for key-value pairs
// include "domain/inventory.fbs";

// A single task invocation
table Task {
  name:string;

  // Module invocation (exactly one should be set)
  module:string;         // module name (e.g., "ansible.builtin.apt")
  args:[Var];            // module arguments as key-value pairs
  action:string;         // alternative: "module: args" format

  // Control flow
  when:string;           // condition (may be complex, stored as string)
  loop:string;           // loop expression (stored as string/JSON)
  loop_control:string;   // loop control settings (JSON)
  register:string;       // variable name to store result
  until:string;          // retry condition
  retries:int;
  delay:int;
  changed_when:string;
  failed_when:string;

  // Privilege escalation
  become:bool;
  become_user:string;
  become_method:string;
  become_exe:string;
  become_flags:string;

  // Delegation
  delegate_to:string;
  delegate_facts:bool;

  // Error handling
  ignore_errors:bool;
  ignore_unreachable:bool;
  any_errors_fatal:bool;

  // Notification
  notify:[string];       // handler names to trigger
  listen:[string];       // topics to listen for (handlers only)

  // Execution
  async:int;
  poll:int;
  throttle:int;
  timeout:int;
  run_once:bool;

  // Metadata
  tags:[string];
  no_log:bool;
  diff:bool;
  check_mode:bool;

  // Environment
  environment:[Var];
  vars:[Var];

  // Connection (overrides)
  connection:string;
  remote_user:string;
  port:int;
}

// A block groups tasks with error handling
table Block {
  name:string;

  // Task lists
  block:[TaskOrBlock];   // main tasks (requires union or separate messages)
  rescue:[TaskOrBlock];  // on failure
  always:[TaskOrBlock];  // always run

  // Block-level attributes (same as task)
  when:string;
  become:bool;
  become_user:string;
  become_method:string;
  tags:[string];
  vars:[Var];
  environment:[Var];
  delegate_to:string;
  ignore_errors:bool;
  ignore_unreachable:bool;
  any_errors_fatal:bool;
  throttle:int;
  timeout:int;
  run_once:bool;
  no_log:bool;
  diff:bool;
  check_mode:bool;
  connection:string;
  remote_user:string;
  port:int;
}

// Union to represent either a task or a block
// Note: FlatBuffers unions require careful handling
union TaskOrBlockUnion { Task, Block }

table TaskOrBlock {
  item:TaskOrBlockUnion;
}

// A list of tasks (used in playbooks, handlers, etc.)
table TaskList {
  items:[TaskOrBlock];
}

Alternative: Simpler design without unions:

namespace shep.domain;

// Task with optional nested block
table Task {
  name:string;
  module:string;
  args:[Var];

  // ... other fields ...

  // If this is a block, these are set instead of module
  is_block:bool;
  block_tasks:[Task];    // self-referential for blocks
  rescue_tasks:[Task];
  always_tasks:[Task];
}

The simpler design avoids FlatBuffers unions at the cost of a less clean schema.


Implementation Notes

Module Detection

Ansible tasks specify modules in multiple ways:

# Direct module name
- apt:
    name: nginx

# With args
- name: Install nginx
  apt:
    name: nginx
    state: present

# Action syntax
- name: Install nginx
  action: apt name=nginx state=present

# Free-form
- shell: echo hello

# FQCN
- ansible.builtin.apt:
    name: nginx

The parser must:

  1. Identify which key is the module (not a known task attribute)
  2. Handle FQCN and short names
  3. Parse both dict args and free-form string args

Conditional Complexity

The when field can be:

Store as string (JSON-encoded for lists). The host evaluates at runtime.

Loop Complexity

Loops can use:

Store the loop type and expression. The host expands at runtime.


File Checklist

New Files

flatbuffers/domain/task.fbs              # Task, Block, TaskList

Modified Files

flatbuffers/domain/playbook.fbs          # References task.fbs
Makefile                                  # Add task.fbs compilation

Generated Files

internal/fbs/shep/domain/Task.go
internal/fbs/shep/domain/Block.go
internal/fbs/shep/domain/TaskOrBlock.go  # if using unions
internal/fbs/shep/domain/TaskList.go

Milestones

  1. [ ] M1: Schema design finalized — Decide on union vs simple approach
  2. [ ] M2: FlatBuffers compile — task.fbs generates Go code
  3. [ ] M3: Integrated with playbook — Playbook parser creates Tasks
  4. [ ] M4: Block support — Blocks with rescue/always work
  5. [ ] M5: Tests passing — Round-trip serialization tests

Out of Scope


Resolved Questions

  1. Union vs flat structure?

    • Flat with is_block flag
  2. How to handle free-form module args?

    • shell: echo hello has args as a string
    • add free_form_args:string field?
  3. Handler-specific fields?

    • listen only applies to handlers
    • Include in Task and add a is_handler flag in addition to listen (since listen can be empty for handlers and for tasks)

Recommended Implementation Order

Since tasks are a dependency of playbooks:

  1. First: Implement flatbuffers/domain/task.fbs
  2. Then: Implement flatbuffers/domain/playbook.fbs (imports task.fbs)
  3. Then: Implement playbook plugin (parses both)
  4. Finally: CLI commands for playbooks

Task parsing code lives in the playbook plugin since standalone task files are just arrays of tasks - the same structure as a play's task list.