otto/rfc/0011-step-library-format.adoc

403 lines
12 KiB
Plaintext

= RFC-0011: Step Library Packaging Format
:toc: preamble
:toclevels: 3
ifdef::env-github[]
:tip-caption: :bulb:
:note-caption: :information_source:
:important-caption: :heavy_exclamation_mark:
:caution-caption: :fire:
:warning-caption: :warning:
endif::[]
.**RFC Template**
.Metadata
[cols="1h,1"]
|===
| RFC
| 0011
| Title
| Step Library Packaging Format
| Sponsor
| link:https://github.com/rtyler[R Tyler Croy]
| Status
| Draft :speech_balloon:
| Type
| Standards
| Created
| 2020-10-17
|===
== Abstract
In order to provide for a simple and extensible way to implement steps, the
step library packaging format allows for native tools and scripts to be
distributed and loaded by agents.
== Specification
Each step effectively has an `entrypoint` which is a binary executable. The
agents in Otto will execute this file with an <<invocation-file>> containing
all the necessary configuration and parameters.
[[manifest-file]]
=== Manifest file
The `manifest.yml` file is the step library package description. It contains
all the information on how the step can be invokoed, but also details about how
the build tooling should package the artifact for use within Otto.
.Example manifest file
[source,yaml]
----
# This manifest captures the basic functionality of the Jenkins Pipeline `sh`
# step
---
# The symbol defines how this step should present in the pipeline
symbol: sh
# Description is help text
description: |
The `sh` step executes a shell script within the given execution context
# Instructs the agent to create a local cache for this step, this will default
# to false as _most_ steps do not need their own caching directory.
#
# Agents are expected to construct the caches in their
# workdir/caches/<step-symbol> and to pass that into the step via the "cache"
# invocation file parameter
cache: true
# List all the files/globs to include in the packaged artifact
includes:
# Paths are treated as relative from wherever osp is invoked from
- name: target/release/sh-step
# Steps the entire prefix of the file name, placing the file in the root of
# the artifact
flatten: true
# A name starting with ./ is treated to be relative to the manifest.yml
- name: ./README.adoc
# The entrypoint tells the Otto agent which actual binary to use when
# executing.
entrypoint:
path: sh-step
# Multiarch tells the agent that this should be executed on all platforms. In
# which case case it may be "blindly" invoked.
#
# Non-multiarch steps will be attempt to be invoked with
# `${entrypoint.path}-${arch}-${vendor}-${system}-${abi}` similar to how
# Rust manages target triples: https://doc.rust-lang.org/nightly/rustc/platform-support.html
multiarch: false
# The configuration helps the step self-express the configuration variables it
# requires from Otto in order to execute properly
#
# The names of these variables are to be considered globally flat by default,
# allowing for multiple steps to share the same configuration values. Should a
# step wish to _not_ share its configuration values, it should namespace them
# in the key name with the convention of `{step}.{key}` (e.g.
# `sh.default_shell`)
#
# The configuration variables are also a means of requesting credentials from
# Otto.
configuration:
default_shell:
description: |
The default shell to use for the invocation of `sh` steps
required: false
default: '/bin/sh'
# The parameters array allows for keyword invocation and positional invocation
# of the step
parameters:
- name: script
required: true
type: string
description: |
Runs a Bourne shell script, typically on a Unix node. Multiple lines are accepted.
An interpreter selector may be used, for example: `#!/usr/bin/perl`
Otherwise the system default shell will be run, using the `-xe` flags (you can specify `set +e` and/or `set +x` to disable those).
- name: encoding
description: |
Encoding of the stdout/stderr output, not typically needed as the system will
default to whatever `LC_TYPE` is defined.
type: string
required: false
- name: label
description: |
A label to identify the shell step in a GUI.
type: string
required: false
- name: returnStatus
description: Compatibility support only, doesn't do anything
type: boolean
required: false
- name: returnStdout
description: Compatibility support only, doesn't do anything
type: boolean
required: false
----
[[invocation-file]]
=== Invocation file
The invocation file is a YAML file generated at runtime and made available to
the step binary on the agent. The invocation file should carry all parameters,
environment variables, and internal configuration necessary for the step binary
to execute correctly.
.Example invocation file passed to entrypoint
[source,yaml]
----
---
configuration:
self: 'some-uuid-v4' # <1>
ipc: '/tmp/agent-5171.sock' # <2>
endpoints: # <3>
objects:
url: 'http://localhost:8080'
parameters:
script: 'ls -lah'
----
<1> `self` contains the identifier the step can use to identify itself when interacting with the control socket or other services.
<2> `ipc` will have a path to the agent's control socket, which speaks HTTP.
<3> `endpoints` is a map of endpoints which the step may interact with to perform its functions.
[[control-socket]]
=== Agent Control Socket
Each agent will open up a control socket for the steps it launches to safely
communicate back with the long-lived agent daemon. The agent _may_ create a
single long-lived IPC socket which is open for all steps, or generate a unique
IPC connection for each step. The messages must be JSON structured and steps
should wait for a response before proceeding to their next operation.
For the inter-process communication (IPC) between steps and the agent, the agent
should bind an HTTP service to a local unix socket on platforms which support it.
All requests should then be formed as JSON over HTTP, which is further described
in <<0005-json-over-http.adoc#abstract, RFC #5>>.
Examples of requests are detailed below.
.Response
[source,json]
----
{
"type" : "Received"
}
----
==== Manage pipeline status
The total number of pipeline status is subject of another document, but for example
purposes assume there are: `Success`, `Failure`, `Unstable`, and `Aborted`.
[NOTE]
====
If the step returns a non-zero status, it will automatically set the status to `Failure`.
See the `Status` enum in the agent code for a mapping of exit codes to status.
====
.Change the status
[source,json]
----
{
"type" : "SetPipelineStatus",
"status" : "Unstable"
}
----
.Terminate the pipeline
[source,json]
----
{
"type" : "TerminatePipeline"
}
----
==== Variables
Capturing variables should be pretty straightforward.
.Example step capturing a variable
[source]
----
prompt msg: 'What color should the bike shed be?', into: 'color'
----
.Variable capture message
[source,json]
----
{
"type" : "CaptureVariable",
"name" : "color",
"value" : "blue"
}
----
These can then be accessed in the steps remaining in the scope (e.g. a stage)
via a special environment variable: `VAR_COLOR`
Storing a new variable should replace it, but a `drop` step should also exist, e.g.:
[source]
----
drop name: 'color'
----
.Drop variable message
[source,json]
----
{
"type" : "DropVariable",
"name" : "color"
}
----
== Motivation
Otto requires a means of defining pipeline behavior in a customizable fashion.
This includes a standard set of steps which address common user needs, along
with a pattern to allow for user-defined steps.
== Reasoning
The approach defined in this document intends to address some of the goals
defined at the outset of the Otto project, discussed below.
=== Safe extensibility
[quote]
====
Extensibility must not come at the expense of system integrity. Systems which
allow for administrator, or user-injected code at runtime cannot avoid system
reliability and security problems. Extensibility is an important characteristic
to support, but secondary to system integrity.
====
The process boundaries required by the design of step libraries is the **key
feature** of this design. The extensibility of the system is inherently
process-based which allows for steps to do numerous things which are not
currently known or defined.
=== User-defined extension
[quote]
====
Usage cannot grow across an organization without user-defined extension. The
operators of the system will not be able to provide for every eventual
requirement from users. Some mechanism for extending or consolidating aspects
of a continuous delivery process must exist.
====
Implied, but not defined by this document is the notion of a "standard library"
of steps to cover common cases which many users will have in _most_ of their
pipelines. That said, because step libraries are "just tarballs" that means
that a user should be able to bring their own step libraries and in some cases
even override the step libraries defined by the standard library.
Because the step libraries are dumb tarballs, they also don't require any
specific platform support for users to bring their own implementations of
steps. An `entrypoint` in the <<manifest-file>> is nothing more than an file
executable by the agent. This allows users to write their own custom steps in
Bash, Ruby, Python, Java, C#, etc. So long as the process can read the
<<invocation-file>> and emit the appropriate outputs, any number of steps
implemented in different languages should have no problem co-existing within
the same pipeline.
=== Avoid mixing execution contexts
[quote]
====
Mixing of management and execution contexts causes a myriad of issues. Many
tools allow the management/orchestrator process to run user-defined workloads.
This allows breaches of isolation between user-defined workloads and
administrator configuration and data.
====
The step library approach pushes execution of user-defined code solely to the
agent which is executed the pipeline. In many cases this will be a machine with
few privileges or an ephemeral cloud/container instance. As such there is zero
execution of user-defined workloads in the locations where system execution
occurs, such as in the parser, orchestrator, or other services.
=== Deterministic runtime behavior
[quote]
====
Non-deterministic runtime behavior adds instability. Without being able to
"explain" a set of operations which should occur before runtime, it is
impossible to determine whether or not a given delivery pipeline is correctly
constructed.
====
Execution of steps from a step library can and should be done without an
"interpreter." That is to say the modeling language which sits on top of the
step libraries doesn't need to be Turing-complete
footnote:[https://en.wikipedia.org/wiki/Turing_completeness] and can be
"explained" prior to execution.
This approach _should_ open the door to future enhancements which can perform a
type of static analysis on pipelines to find redundant steps or performance
bottlenecks that can be improved upon at a later date.
== Backwards Compatibility
Since Otto has no previous step subsystem, no backwards compatibility concerns.
== Security
=== Variables
Variables must be stored within the agent per-pipeline, such that pipelines
cannot "pollute" the variable namespace of other pipelines. Current test
implementations require a single agent invocation per pipeline invocation, so
it's not yet possible to even have an agent run multiple successive or
concurrent pipelines. Should that ever become the case, it is expected that
variables will be associated with pipeline which declares them.
=== Credentials
The retrieval and use of credentials by steps is not subject to this document,
and is at this time still under active deliberation.
== Testing
Testing of step libraries is covered in the main source repository. This
includes testing of steps as well, which are generally using
link:https://github.com/kward/shunit2/[shunit2].
== Prototype Implementation
The prototype implementation is found in the
link:https://github.com/rtyler/otto[main branch].
== References