File Type

This document describes the design of the file resource type for managing files and directories.

Overview

The file resource manages files and directories with three aspects:

  • Existence: Whether the file/directory exists or is absent
  • Content: The contents of a file (from inline content or source file)
  • Attributes: Owner, group, and permissions

Provider Interface

File providers must implement the FileProvider interface:

type FileProvider interface {
    model.Provider

    CreateDirectory(ctx context.Context, dir string, owner string, group string, mode string) error
    Store(ctx context.Context, file string, contents []byte, source string, owner string, group string, mode string) error
    Status(ctx context.Context, file string) (*model.FileState, error)
}

Method Responsibilities

MethodResponsibility
StatusQuery current file state (existence, type, content hash, attributes)
StoreCreate or update a file with content and attributes
CreateDirectoryCreate a directory with attributes

Status Response

The Status method returns a FileState containing:

type FileState struct {
    CommonResourceState
    Metadata *FileMetadata
}

type FileMetadata struct {
    Name     string         // File path
    Checksum string         // SHA256 hash of contents (files only)
    Owner    string         // Owner username
    Group    string         // Group name
    Mode     string         // Permissions in octal (e.g., "0644")
    Provider string         // Provider name (e.g., "posix")
    MTime    time.Time      // Modification time
    Size     int64          // File size in bytes
    Extended map[string]any // Provider-specific metadata
}

The Ensure field in CommonResourceState is set to:

  • present if a regular file exists
  • directory if a directory exists
  • absent if the path does not exist

Available Providers

ProviderPlatformDocumentation
posixUnix/LinuxPosix

Ensure States

ValueDescription
presentPath must be a regular file with specified content
absentPath must not exist
directoryPath must be a directory

Content Sources

Files can receive content from two mutually exclusive sources:

PropertyDescription
contentsInline string content (template-resolved)
sourcePath to local file to copy from
# Inline content with template
- file:
    - /etc/motd:
        ensure: present
        content: |
          Welcome to {{ lookup('facts.hostname') }}
          Managed by CCM
        owner: root
        group: root
        mode: "0644"

# Copy from source file
- file:
    - /etc/app/config.yaml:
        ensure: present
        source: files/config.yaml
        owner: app
        group: app
        mode: "0640"

When using source, the path is relative to the manifest’s working directory if one is set.

Required Properties

Unlike some resources, file resources require explicit attributes:

PropertyRequiredDescription
ownerYesUsername that owns the file
groupYesGroup that owns the file
modeYesPermissions in octal notation

This prevents accidental creation of files with default or inherited permissions.

Apply Logic

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Get current state via Status()          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                  β”‚
                  β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Is current state desired state?         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              Yes β”‚         No
                  β–Ό         β”‚
          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”‚
          β”‚ No change β”‚     β”‚
          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β”‚
                            β–Ό
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              β”‚ What is desired ensure? β”‚
              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                            β”‚
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚ absent                β”‚ directory             β”‚ present
    β–Ό                       β–Ό                       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Remove     β”‚      β”‚ CreateDir     β”‚      β”‚ Store         β”‚
β”‚ (os.Remove)β”‚      β”‚               β”‚      β”‚               β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Idempotency

The file resource checks multiple attributes for idempotency:

State Checks (in order)

  1. Ensure match: Current type matches desired (present/absent/directory)
  2. Content match: SHA256 checksum of contents matches (for ensure: present)
  3. Owner match: Current owner matches desired
  4. Group match: Current group matches desired
  5. Mode match: Current permissions match desired

Decision Table

DesiredCurrent StateAction
absentabsentNone
absentpresent/directoryRemove
directorydirectory + matching attrsNone
directoryabsent/presentCreateDirectory
directorydirectory + wrong attrsCreateDirectory (updates attrs)
presentpresent + matching allNone
presentabsentStore
presentpresent + wrong contentStore
presentpresent + wrong attrsStore

Content Comparison

Content is compared using SHA256 checksums:

SourceChecksum Method
contents propertySha256HashBytes([]byte(contents))
source propertySha256HashFile(adjustedPath)
Existing fileSha256HashFile(filePath)

Mode Validation

File modes are validated during resource creation:

Valid Formats:

  • "0644" - Standard octal
  • "644" - Without leading zero
  • "0o755" - With 0o prefix
  • "0O700" - With 0O prefix

Validation Rules:

  • Must be valid octal number (digits 0-7)
  • Must be ≀ 0777 (no setuid/setgid/sticky via mode)

Path Validation

File paths must be:

  • Absolute (start with /)
  • Clean (no . or .. components, filepath.Clean(path) == path)
if filepath.Clean(p.Name) != p.Name {
    return fmt.Errorf("file path must be absolute")
}

Working Directory

When a manifest has a working directory (e.g., extracted from an archive), the source property is resolved relative to it:

if properties.Source != "" && mgr.WorkingDirectory() != "" {
    source = filepath.Join(mgr.WorkingDirectory(), properties.Source)
}

This allows manifests bundled with their source files to use relative paths.

Noop Mode

In noop mode, the file type:

  1. Queries current state normally
  2. Computes content checksums
  3. Logs what actions would be taken
  4. Sets appropriate NoopMessage:
    • “Would have created the file”
    • “Would have created directory”
    • “Would have removed the file”
  5. Reports Changed: true if changes would occur
  6. Does not call provider Store/CreateDirectory methods
  7. Does not remove files

Desired State Validation

After applying changes (in non-noop mode), the type verifies the file reached the desired state by calling Status() again and checking all attributes match. If validation fails, ErrDesiredStateFailed is returned.

Subsections of File Type

Posix Provider

This document describes the implementation details of the Posix file provider for managing files and directories on Unix-like systems.

Provider Selection

The Posix provider is the default and only file provider. It is always available and returns priority 1 for all file resources.

Operations

Store (Create/Update File)

Process:

  1. Verify parent directory exists
  2. Parse file mode from octal string
  3. Open source file if source property is set
  4. Create temporary file in the same directory as target
  5. Set file permissions on temp file
  6. Write content (from source file or contents property)
  7. Set ownership (chown) on temp file
  8. Close temp file
  9. Atomic rename temp file to target path

Atomic Write Pattern:

[parent dir]/<basename>.* (temp file)
    ↓ chmod (set permissions)
    ↓ write content
    ↓ chown (set owner/group)
    ↓ close
    ↓ rename
[parent dir]/<basename> (final file)

The temp file is created in the same directory as the target to ensure os.Rename() is atomic (same filesystem).

Content Sources:

PropertyBehavior
contentsWrite string directly to file (template-resolved)
sourceCopy from local file path (adjusted for working directory)

If both are empty, an empty file is created.

Error Handling:

ConditionBehavior
Parent directory doesn’t existReturn error: “is not a directory”
Invalid mode formatReturn error from strconv.ParseUint
Source file not foundReturn error from os.Open
Permission deniedReturn error from underlying syscall
Rename failureReturn error: “could not rename temporary file”

CreateDirectory

Process:

  1. Parse file mode from octal string
  2. Create directory and parents via os.MkdirAll()
  3. Lookup numeric UID/GID from owner/group names
  4. Set permissions via os.Chmod() (ensures correct mode even if umask affected MkdirAll)
  5. Set ownership via os.Chown()

Command Sequence:

os.MkdirAll(dir, parsedMode)    // Create directory tree
os.Chmod(dir, parsedMode)        // Ensure correct permissions
os.Chown(dir, uid, gid)          // Set ownership

The explicit Chmod after MkdirAll is necessary because MkdirAll applies the process umask to the mode.

Status

Process:

  1. Initialize state with default metadata
  2. Call os.Stat() on file path
  3. Based on result, populate state accordingly

State Detection:

os.Stat() ResultEnsure ValueMetadata
File existspresentSize, mtime, owner, group, mode, checksum
Directory existsdirectorySize, mtime, owner, group, mode
os.ErrNotExistabsentNone
os.ErrPermissionabsentNone (logged as warning)
Other error(unchanged)None (logged as warning)

Metadata Collection:

FieldSource
NameFrom properties
Provider“posix”
SizeFileInfo.Size()
MTimeFileInfo.ModTime()
Ownerutil.GetFileOwner() - resolves UID to username
Grouputil.GetFileOwner() - resolves GID to group name
Modeutil.GetFileOwner() - octal string (e.g., “0644”)
Checksumutil.Sha256HashFile() - SHA256 hash (files only)

Note: Checksum is only calculated for regular files, not directories.

Idempotency

The file resource achieves idempotency by comparing current state against desired state:

State Checks

The isDesiredState() function checks (in order):

  1. Ensure value matches - present, absent, or directory
  2. Content checksum matches - SHA256 of contents vs existing file (for files only)
  3. Owner matches - Username comparison
  4. Group matches - Group name comparison
  5. Mode matches - Octal permission string comparison

Decision Flow

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ What is the desired ensure state?       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                  β”‚
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚ absent      β”‚ directory   β”‚ present     β”‚
    β–Ό             β–Ό             β–Ό             β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ File      β”‚ β”‚ Directory β”‚ β”‚ File exists?  β”‚ β”‚
β”‚ exists?   β”‚ β”‚ exists?   β”‚ β”‚ Content match?β”‚ β”‚
β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β”‚ Owner match?  β”‚ β”‚
      β”‚             β”‚       β”‚ Group match?  β”‚ β”‚
  Yes β”‚ No      Yes β”‚ No    β”‚ Mode match?   β”‚ β”‚
      β–Ό             β–Ό       β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”           β”‚         β”‚
β”‚ Remove  β”‚ β”‚ Stable    β”‚     All Yesβ”‚    No   β”‚
β”‚ file    β”‚ β”‚           β”‚           β–Ό         β–Ό
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                            β”‚ Stable    β”‚ β”‚ Store     β”‚
                            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Content Comparison

For files with ensure: present:

Content SourceChecksum Calculation
contents propertySha256HashBytes([]byte(contents))
source propertySha256HashFile(adjustedSourcePath)

The source path is adjusted based on the manager’s working directory when set.

Mode Validation

File modes are validated during resource creation:

  1. Strip optional 0o or 0O prefix
  2. Parse as octal number (base 8)
  3. Validate range: must be ≀ 0777

Valid Mode Examples:

InputParsed Value
"0644"0o644
"644"0o644
"0o755"0o755
"0O700"0o700

Invalid Mode Examples:

InputError
"0888"Invalid octal digit
"1777"Exceeds maximum (setuid/setgid not supported via mode)
"rw-r--r--"Not octal format

Ownership Resolution

Owner and group names are resolved to numeric UID/GID via:

uid, gid, err := util.LookupOwnerGroup(owner, group)

This uses the system’s user database (/etc/passwd, /etc/group, or equivalent):

  • user.Lookup(owner) β†’ UID
  • user.LookupGroup(group) β†’ GID

Error Handling:

ConditionBehavior
User not foundReturn error: “could not lookup user”
Group not foundReturn error: “could not lookup group”
Invalid UID/GID formatReturn error from strconv.Atoi

Working Directory Support

When a manager has a working directory set (e.g., from extracted manifest), the source property path is adjusted:

func (t *Type) adjustedSource(properties *model.FileResourceProperties) string {
    source := properties.Source
    if properties.Source != "" && t.mgr.WorkingDirectory() != "" {
        source = filepath.Join(t.mgr.WorkingDirectory(), properties.Source)
    }
    return source
}

This allows manifests to use relative paths for source files bundled with the manifest.

Platform Support

The Posix provider uses Unix-specific system calls:

OperationSystem Call
Get file owner/groupsyscall.Stat_t (UID/GID from stat)
Set ownershipos.Chown() β†’ chown(2)
Set permissionsos.Chmod() β†’ chmod(2)

The provider has separate implementations for Unix and Windows (file_unix.go, file_windows.go in internal/util), with Windows returning errors for ownership operations.

Security Considerations

Atomic Writes

Files are written atomically via temp file + rename. This prevents:

  • Partial file reads during write
  • Corruption if process is interrupted
  • Race conditions with concurrent readers

Permission Ordering

Permissions and ownership are set on the temp file before rename:

  1. Chmod - Set permissions
  2. Write content
  3. Chown - Set ownership
  4. Rename to target

This ensures the file never exists at the target path with incorrect permissions.

Path Validation

File paths must be absolute and clean (no . or .. components):

if filepath.Clean(p.Name) != p.Name {
    return fmt.Errorf("file path must be absolute")
}

Required Properties

Owner, group, and mode are required properties and cannot be empty, preventing accidental creation of files with default/inherited permissions.