Hi folks,

I've written down the design in README format now. This should make for a
easier to ingest from a module developer's perspective. Meanwhile I've also
had a shot at a hardcoded implementation of this, and it is technically
possible to implement this too.

-----------
Resource API

A *resource* is the basic thing that is managed by puppet. Each resource
has a set of attributes describing its current state. Some of the
attributes can be changed throughout the life-time of the resource, some
attributes only report back, but cannot be changed (see read_only)others
can only be set once during initial creation (see init_only). To gather
information about those resources, and to enact changes in the real world,
puppet requires a piece of code to *implement* this interaction. The
implementation can have parameters that influence its mode of operation
(see operational_parameters). To describe all these parts to the
infrastructure, and the consumers, the resource *Definition* (f.k.a.
'type') contains definitions for all of them. The *Implementation* (f.k.a.
'provider') contains code to *get* and *set* the system state.
<https://github.com/puppetlabs/pick-playground/tree/master/tnp-design#resource-definition>Resource
Definition

Puppet::ResourceDefinition.register(
    name: 'apt_key',
    docs: <<-EOS,      This type provides Puppet with the capabilities
to manage GPG keys needed      by apt to perform package validation.
Apt has it's own GPG keyring that can      be manipulated through the
`apt-key` command.      apt_key {
'6F6B15509CF8E59E6E469F327F438280EF8D349F':        source =>
'http://apt.puppetlabs.com/pubkey.gpg'      }      **Autorequires**:
   If Puppet is given the location of a key file which looks like an
absolute      path this type will autorequire that file.    EOS
    attributes:   {
        ensure:      {
            type: 'Enum[present, absent]',
            docs: 'Whether this apt key should be present or absent on
the target system.'
        },
        id:          {
            type:    'Variant[Pattern[/\A(0x)?[0-9a-fA-F]{8}\Z/],
Pattern[/\A(0x)?[0-9a-fA-F]{16}\Z/],
Pattern[/\A(0x)?[0-9a-fA-F]{40}\Z/]]',
            docs:    'The ID of the key you want to manage.',
            namevar: true,
        },
        # ...
        created:     {
            type:      'String',
            docs:      'Date the key was created, in ISO format.',
            read_only: true,
        },
    },
    autorequires: {
        file:    '$source', # will evaluate to the value of the
`source` attribute
        package: 'apt',
    },
)

The Puppet::ResourceDefinition.register(options) function takes a Hash with
the following top-level keys:

   - name: the name of the resource. For autoloading to work, the whole
   function call needs to go into lib/puppet/type/<name>.rb.
   - docs: a doc string that describes the overall working of the type,
   gives examples, and explains pre-requisites as well as known issues.
   - attributes: an hash mapping attribute names to their details. Each
   attribute is described by a hash containing the puppet 4 data type, a
   docs string, and whether the attribute is the namevar, read_only, and/or
   init_only.
   - operational_parameters: a hash mapping parameter names to puppet data
   types and docs strings.
   - autorequires, autobefore, autosubscribe, and autonotify: a Hash
   mapping resource types to titles. Currently the titles must either be
   constants, or, if the value starts with a dollar sign, a reference to the
   value of an attribute.

<https://github.com/puppetlabs/pick-playground/tree/master/tnp-design#resource-implementation>Resource
Implementation

At runtime the current and intended system states for a single resource
instance are always represented as ruby Hashes of the resource's
attributes, and applicable operational parameters.

The two fundamental operations to manage resources are reading and writing
system state. These operations are implemented in the ResourceImplementation
as get and set:

Puppet::ResourceImplementation.register('apt_key') do
  def get
    [
      'title': {
        name: 'title',
        # ...
      },
    ]
  end

  def set(current_state, target_state, noop: false)
    target_state.each do |title, resource|
      # ...
    end
  endend

The get method returns a Hash of all resources currently available, keyed
by their title. If the get method raises an exception, the implementation
is marked as unavailable during the current run, and all resources of its
type will fail. The error message will be reported to the user.

The set method updates resources to the state defined in target_state. For
convenience, current_state contains the last available system state from a
prior get call. When noop is set to true, the implementation must not
change the system state, but only report what it would change.
<https://github.com/puppetlabs/pick-playground/tree/master/tnp-design#runtime-environment>Runtime
Environment

The primary runtime environment for the implementation is the puppet agent,
a long-running daemon process. The implementation can also be used in the
puppet apply command, a one-shot version of the agent, or the puppet
resource command, a short-lived CLI process for listing or managing a
single resource type. Other callers who want to access the implementation
will have to emulate those environments. In any case the registered block
will be surfaced in a clean class which will be instantiated once for each
transaction. The implementation can define any number of helper methods to
support itself. To allow for a transaction to set up the prerequisites for
an implementation, and use it immediately, the provider is instantiated as
late as possible. A transaction will usually call get once, and may call
set any number of times to effect change. The host object can be used to
cache ephemeral state during execution. The implementation should not try
to cache state beyond the transaction, to avoid interfering with the agent
daemon. In many other cases caching beyond the transaction won't help
anyways, as the hosting process will only manage a single transaction.
<https://github.com/puppetlabs/pick-playground/tree/master/tnp-design#utilities>
Utilities

The runtime environment provides some utilities to make the
implementation's life easier, and provide a uniform experience for its
users.
<https://github.com/puppetlabs/pick-playground/tree/master/tnp-design#commands>
Commands

To use CLI commands in a safe and comfortable manner, the implementation
can use the commands method to access shell commands. You can either use a
full path, or a bare command name. In the latter case puppet will use the
system PATH setting to search for the command. If the commands are not
available, an error will be raised and the resources will fail in this run.
The commands are aware of whether noop is in effect or not, and will skip
the actual execution if necessary.

Puppet::ResourceImplementation.register('apt_key') do
  commands apt_key: '/usr/bin/apt-key'
  commands gpg: 'gpg'

This will create methods called apt_get, and gpg, which will take CLI
arguments without any escaping, and run them in a safe environment (clean
working directory, clean environment). For example to call apt-key to
delete a specific key by id:

apt_key 'del', key_id

By default the stdout of the command is logged to debug, while the stderr
is logged to warning. To access the stdout in the implementation, use the
command name with _lines appended, and process it through the returned
Enumerable <http://ruby-doc.org/core/Enumerable.html> line-by-line. For
example, to process the list of all apt keys:

apt_key_lines(%w{adv --list-keys --with-colons --fingerprint
--fixed-list-mode}).collect do |line|
  # process each line here, and return a resultend

Note: the output of the command is streamed through the Enumerable. If the
implementation requires the exit value of the command before processing, or
wants to cache the output, use to_a to read the complete stream in one go.

If the command returns a non-zero exit code, an error is signalled to
puppet. If this happens during get, all managed resources of this type will
fail. If this happens during a set, all resources that have been scheduled
for processing in this call, but not yet have been marked as a success will
be marked as failed. To avoid this behaviour, call the try_ prefix variant.
In this (hypothetical) example, apt-key signals already deleted keys with
an exit code of 1, which is OK when the implementation is trying to delete
the key:

try_apt_key 'del', key_id
if [0, 1].contains $?.exitstatus
  # success, or already deletedelse
  # failend

The exit code is signalled through the ruby standard variable $? as a
Process::Status object <https://ruby-doc.org/core/Process/Status.html>
<https://github.com/puppetlabs/pick-playground/tree/master/tnp-design#logging-and-reporting>Logging
and Reporting

The implementation needs to signal changes, successes and failures to the
runtime environment. The logger provides a structured way to do so.
<https://github.com/puppetlabs/pick-playground/tree/master/tnp-design#general-messages>General
messages

To provide feedback about the overall operation of the implementation, the
logger provides the usual set of loglevel
<https://docs.puppet.com/puppet/latest/metaparameter.html#loglevel> methods
that take a string, and pass that up to puppet's logging infrastructure:

logger.warning("Unexpected state detected, continuing in degraded mode.")

will result in the following message:

Warning: apt_key: Unexpected state detected, continuing in degraded mode.


   - debug: detailed messages to understand everything that is happening at
   runtime; only shown on request
   - info: high level progress messages; especially useful before
   long-running operations, or before operations that can fail, to provide
   context to interactive users
   - notice: use this loglevel to indicate state changes and similar events
   of notice from the regular operations of the implementation
   - warning: signal error conditions that do not (yet) prohibit execution
   of the main part of the implementation; for example deprecation warnings,
   temporary errors.
   - err: signal error conditions that have caused normal operations to fail
   - critical/alert/emerg: should not be used by resource implementations

See wikipedia <https://en.wikipedia.org/wiki/Syslog#Severity_level> and
RFC424 <https://tools.ietf.org/html/rfc5424> for more details.
<https://github.com/puppetlabs/pick-playground/tree/master/tnp-design#logging-contexts>Logging
contexts

Most of an implementation's messages are expected to be relative to a
specific resource instance, and a specific operation on that instance. For
example, to report the change of an attribute:

logger.attribute_changed(title:, attribute:, old_value:, new_value:,
message: "Changed #{attribute} from #{old_value.inspect} to
#{newvalue.inspect}")

To enable detailed logging without repeating key arguments, and provide
consistent error logging, the logger provides *logging context* methods
that capture the current action and resource instance.

logger.updating(title: title) do
  if key_not_found
    logger.warning('Original key not found')
  end

  # Update the key by calling CLI tool
  apt_key(...)

  logger.attribute_changed(
    attribute: 'content',
    old_value: nil,
    new_value: content_hash,
    message: "Created with content hash #{content_hash}")end

will result in the following messages (of course, with the #{} sequences
replaced by the true values):

Debug: Apt_key[#{title}]: Started updating
Warning: Apt_key[#{title}]: Updating: Original key not found
Debug: Apt_key[#{title}]: Executing 'apt-key ...'
Debug: Apt_key[#{title}]: Successfully executed 'apt-key ...'
Notice: Apt_key[#{title}]: Updating content: Created with content hash
#{content_hash}
Notice: Apt_key[#{title}]: Successfully updated
# TODO: update messages to match current log message formats for
resource messages

In the case of an exception escaping the block, the error is logged
appropriately:

Debug: Apt_key[#{title}]: Started updating
Warning: Apt_key[#{title}]: Updating: Original key not found
Error: Apt_key[#{title}]: Updating failed: #{exception message}
# TODO: update messages to match current log message formats for
resource messages

Logging contexts process all exceptions. StandardErrors
<https://ruby-doc.org/core/StandardError.html> are assumed to be regular
failures in handling a resources, and they are swallowed after logging.
Everything else is assumed to be a fatal application-level issue, and is
passed up the stack, ending execution. See the ruby documentation
<https://ruby-doc.org/core/Exception.html> for details on which exceptions
are not StandardErrors.

The equivalent long-hand form with manual error handling:

logger.updating(title: title)begin
  if key_not_found
    logger.warning(title: title, message: 'Original key not found')
  end

  # Update the key by calling CLI tool
  try_apt_key(...)

  if $?.exitstatus != 0
    logger.error(title: title, "Failed executing apt-key #{...}")
  else
    logger.attribute_changed(
      title:     title,
      attribute: 'content',
      old_value: nil,
      new_value: content_hash,
      message:   "Created with content hash #{content_hash}")
  end
  logger.changed(title: title)rescue StandardError => e
  logger.error(title: title, exception: e, message: 'Updating failed')
  raise unless e.is_a? StandardErrorend

This example is only for demonstration purposes. In the normal course of
operations, implementations should always use the utility functions.
<https://github.com/puppetlabs/pick-playground/tree/master/tnp-design#logging-reference>Logging
reference

The following action/context methods are available:

   - creating(title:, message: 'Creating', &block)
   - updating(title:, message: 'Updating', &block)
   - deleting(title:, message: 'Deleting', &block)
   -

   attribute_changed(title:, attribute:, old_value:, new_value:, message:
   nil)
   -

   created(title:, message: 'Created')
   - updated(title:, message: 'Updated')
   -

   deleted(title:, message: 'Deleted')
   -

   fail(title:, message:) - abort the current context with an error


--------------------



Regards, David

-- 
You received this message because you are subscribed to the Google Groups 
"Puppet Users" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to puppet-users+unsubscr...@googlegroups.com.
To view this discussion on the web visit 
https://groups.google.com/d/msgid/puppet-users/CALF7fHZ-PYgrqboexxDJzzLPgbxidu%2BZs%3DRYHyc85LBOnr4EfA%40mail.gmail.com.
For more options, visit https://groups.google.com/d/optout.

Reply via email to