From 08d6e528dab1d7d1f82ff6e1f50414f20f64cc36 Mon Sep 17 00:00:00 2001 From: Chris Doherty Date: Tue, 21 Feb 2023 10:09:32 -0600 Subject: [PATCH] Add tinkerbell CRD refactor design Signed-off-by: Chris Doherty --- design/14_tinkerbell_crd_refactor/proposal.md | 462 ++++++++++++++++++ .../workflow_state.puml | 13 + .../workflow_state_machine.png | Bin 0 -> 14834 bytes 3 files changed, 475 insertions(+) create mode 100644 design/14_tinkerbell_crd_refactor/proposal.md create mode 100644 design/14_tinkerbell_crd_refactor/workflow_state.puml create mode 100644 design/14_tinkerbell_crd_refactor/workflow_state_machine.png diff --git a/design/14_tinkerbell_crd_refactor/proposal.md b/design/14_tinkerbell_crd_refactor/proposal.md new file mode 100644 index 0000000..a5671c5 --- /dev/null +++ b/design/14_tinkerbell_crd_refactor/proposal.md @@ -0,0 +1,462 @@ +# Tink CRD Refactor + +## Table of contents + +- [Tink CRD Refactor](#tink-crd-refactor) + - [Table of contents](#table-of-contents) + - [Overview](#overview) + - [Context](#context) + - [Goals/Non-goals](#goalsnon-goals) + - [Proposal](#proposal) + - [Custom Resource Definitions](#custom-resource-definitions) + - [`Hardware`](#hardware) + - [`OSIE`](#osie) + - [`Template`](#template) + - [`Workflow`](#workflow) + - [Workflow and Action state transition](#workflow-and-action-state-transition) + - [Webhooks](#webhooks) + - [Data and functions available during template rendering](#data-and-functions-available-during-template-rendering) + - [Hegel Changes](#hegel-changes) + - [Migrating from v1alpha1 to v1alpha2](#migrating-from-v1alpha1-to-v1alpha2) + - [Rationale](#rationale) + - [Comparison with existing resources](#comparison-with-existing-resources) + - [Implementation Plan](#implementation-plan) + - [Future Work](#future-work) + +## Overview + +Tinkerbell's backend is rooted in 3 Custom Resource Definitions (CRDs): Hardware, Workflow and Template. The CRDs were developed as part of the KRM proposal that introduced a Kubernetes backend option to the Tinkerbell stack and mirrored the Postgres database schema (now deprecated and removed). As the CRDs were a reflection of the Postgres schema, they inherited the schemas flaws. This proposal attempts to remediate the flaws by refactoring the CRDs. + +## Context + +When users interact with Tinkerbell the primary interface is `kubectl` and Tinkerbell's CRDs: Hardware, Workflow and Template. The CRDs are hard to understand because they contain duplicate fields, obsolete fields, unclear semantics and, consequently, unclear system behavior expectations. + +Some specific issues with the CRDs are summarized as: + +**Hardware** + +1. Network information is specified in both `.Spec.Interfaces` and `.Spec.Metadata.Instance.Ips`. Only `.Spec.Interfaces` is used. +1. Disk information is specified in both `.Spec.Disks` and `.Spec.Metadata.Instance.Storage`. `.Spec.Metadata.Instance.Storage` is unused. +1. Userdata can be specified in both `.Spec.UserData` and `.Spec.Instance.Userdata`. `.Spec.Instance.Userdata` is unused. +1. `.Spec.TinkVersion` has no functional use. +1. `.Spec.Resources` was intended for use in CAPT as part of its Hardware selection algorithm but is yet to be implemented. +1. `.Spec.Interfaces[].Netboot.{AllowPXE,AllowWorkflow}` and `.Spec.Metadata.Instance.AlwaysPxe` are seemingly related but reside on different objects and how they impact eachother is unclear. +1. `.Spec.Metadata.Custom` defines specific fields that are related to other parts of Hardware. +1. `.Status.State`, `.Spec.Metadata.Instance.State` and `.Spec.Metadata.State` have unclear semantics. The `.Spec` state fields impact the machine provisioning process but nothing in the core Tinkerbell stack sets their values. `.Status.State` is unused. + +**Template** + +1. Template defines a single field, `.Spec.Data`. The format of `.Spec.Data` is entirely ambiguous requiring the user to understand implementation detail. In summary, `.Spec.Data` is composed of a list of tasks that can run on different machines; a task is composed of a list of actions that perform a function such as streaming a raw image. The multi-machine capability has no known use-cases. +1. The Template objects `.Status` field is unused. + +**Workflow** + +1. The `.Spec.HardwareMap` historically defines a template value used to render a tasks `WorkerAddr`. The `WorkerAddr` should be the MAC of the machine that should run the task. This creates a hard to understand relationship between Workflow and Template that users must understand to successfully execute Workflows. +1. The `.Spec.GlobalTimeout` is unused and its origin is unclear (status fields are typically populated by Kubernetes controllers to build understanding of the current object state). +1. Actions leverage the `WorkflowState` type that is intended to describe the overall state of the Workflow. + +Users resort to Q&A in the Tinkerbell Slack to determine what fields are required and how tweaking them impacts the system. The CRDs should be simple enough and sufficiently documented to aid users in understanding how they can manipulate the system. + +## Goals/Non-goals + +**Goals** + +- To de-duplicate Tink custom resource definition fields and data structures. +- To provide clear behavioral expectations when manipulating custom resources. +- To remove obsolete fields and data structures. + +**Non-goals** + +- To change the existing relationship between Tink and Rufio. +- To introduce additional object status data typically found on the `.Status` field. +- To support new technologies such as IPv6. + +## Proposal + +### Custom Resource Definitions + +The new set of CRDs will be defined as part of a `v1alpha2` API. + +#### `Hardware` + +```go +// Hardware is a logical representation of a machine that can execute Workflows. +type Hardware struct { + HardwareSpec +} + +type HardwareSpec struct { + // NetworkInterfaces defines the desired DHCP and netboot configuration for a network interface. + // It is necessary to specify at least one NetworkInterface. + NetworkInterfaces NetworkInterfaces + + // IPXE provides iPXE script override fields. + // Optional. + IPXE IPXE + + // OSIE describes the Operating System Installation Environment to be netbooted. + OSIE OSIE + + // Instance describes instance specific data that is generally unused by Tinkerbell core. + Instance Instance + + // StorageDevices is a list of storage devices that will be available in the OSIE. + // Optional. + StorageDevices []StorageDevice + + // BMCRef references a Rufio Machine object. It exists in the current API and will not be changed + // with this proposal. + BMCRef LocalObjectReference +} + +// NetworkInterface is the desired configuration for a particular network interface. +type NetworkInterface struct { + // DHCP is the basic network information for serving DHCP requests. + DHCP DHCP + + // DisableDHCP disables DHCP for this interface. Implies DisableNetboot. + // Default false. + DisableDHCP bool + + // DisableNetboot disables netbooting for this interface. The interface will still receive + // network information speified on by DHCP. + // Default false. + DisableNetboot bool +} + +// DHCP describes basic network configuration to be served in DHCP offers. +type DHCP struct { + IP string + Netmask string + + // Optional. + Gateway string + + // Optional. + Hostname string + + // Optional. + VLANID int + + // Optional. + Nameservers []string + + // Optional. + Timeservers []string + + // Defaults to max allowed DHCP lease time. + LeaseTime int32 +} + +// OSIE describes an OSIE to be used with a Hardware. The environment data +// is dependent on the OSIE being used and should be updated with the OSIE reference object. +type Netboot struct { + // OSIERef is a reference to an OSIE object. + OSIERef LocalObjectReference + + // KernelParams passed to the kernel when launching the OSIE. Parameters are joined with a + // space. + // Optional. + KernelParams []KernelParam +} + +type IPXE struct { + // Inline is an inline iPXE script that will be served as specified on this property. + Inline *string + + // URL is a URL to an hosted iPXE script. + URL *string +} + +// Instance describes instance specific data. Instance specific data is typically dependent on the +// permanent OS that a piece of hardware runs. This data is often served by an instance metadata +// service such as Tinkerbell's Hegel. The core Tinkerbell stack does not leverage this data. +type Instance struct { + // Userdata is data with a structure understood by the producer and consumer of the data. + Userdata string + + // Vendordata is data with a structure understood by the producer and consumer of the data. + Vendordata string +} + +// NetworkInterfaces maps a MAC address to a NetworkInterface. +type NetworkInterfaces map[MAC]NetworkInterface + +// MAC is a Media Access Control address. +type MAC string + +// StorageDevice describes a storage device path that will be present in the OSIE. +type StorageDevice string + +// KernelParam defines an atomic kernel parameter that will be passed to the OSIE. +type KernelParam string +``` + +#### `OSIE` + +`OSIE` is a new CRD. It exists to ensure OSIE URLs can be re-used and easily updated across `Hardware` instances. + +```go +// OSIE describes and Operating System Initialization Environment. It is used by Tinkerbell +// to provision machines and should launch the Tink Worker component. +type OSIE struct { + Spec OSIESpec +} + +type OSIESpec struct { + // KernelURL is a URL to a kernel image. + KernelURL string + + // InitrdURL is a URL to an initrd image. + InitrdURL string +} +``` + +#### `Template` + +```go +// Template defines a set of actions to be run on a target machine. The template is rendered +// prior to execution where it is exposed to Hardware and user defined data. All fields within +// TemplateSpec may contain template values. See https://pkg.go.dev/text/template for more details. +type Template struct { + Spec TemplateSpec +} + +type TemplateSpec struct { + // Actions defines the set of actions to be run on a target machine. Actions are run sequentially + // in the order they are specified. At least 1 action must be specified. Names of actions + // must be unique within a Template. + Actions []Action + + // Volumes to be mounted on all actions. If an action specifies the same volume it will take + // precedence. + // Optional. + Volumes []Volume + + // Env defines environment variables to be available in all actions. If an action specifies + // the same environment variable it will take precedence. + // Optional. + Env map[string]string +} + +// Action defines an individual action to be run on a target machine. +type Action struct { + // Name is a unique name for the action. + Name ActionName + + // Image is an OCI image. + Image string + + // Command defines the command to use when launching the image. + // Optional. + Command string + + // Args are a set of arguments to be passed to the container on launch. + Args []string + + // Env defines environment variables used when launching the container. + // Optional. + Env map[string]string + + // Volumes defines the volumes to mount into the container. + // Optional. + Volumes []Volume + + // NetworkNamespace defines the network namespace to run the container in. This enables access + // to the host network namespace. + // See https://man7.org/linux/man-pages/man7/namespaces.7.html. + NetworkNamespace string +} + +// Volume is a specification for mounting a volume in an action. Volumes take the form +// {SRC-VOLUME-NAME | SRC-HOST-DIR}:TGT-CONTAINER-DIR:OPTIONS. When specifying a VOLUME-NAME that +// does not exist it will be created for you. +// +// Examples +// +// Read-only bind mount bound to /data +// /etc/data:/data:ro +// +// Writable volume name bound to /data +// shared_volume:/data +// +// See https://docs.docker.com/storage/volumes/ for additional details +type Volume string + +// ActionName is unique name within the context of a workflow. +type ActionName string +``` + +#### `Workflow` + +```go +// Workflow describes a set of actions to be run on a specific Hardware. Workflows execute +// once and should be considered ephemeral. +type Workflow struct { + Spec WorkflowSpec + Status WorkflowStatus +} + +type WorkflowSpec struct { + // HardwareRef is a reference to a Hardware resource this workflow will execute on. + // If no namespace is specified the Workflow's namespace is assumed. + HardwareRef LocalObjectReference + + // TemplateRef is a reference to a Template resource used to render workflow actions. + // If no namespace is specified the Workflow's namespace is assumed. + TemplateRef LocalObjectReference + + // TemplateData is user defined data that is injected during template rendering. The data + // structure should be marshalable. + // Optional. + TemplateData map[string]any + + // Timeout defines the time the workflow has to complete. The timer begins when the first action + // is requested. When set to 0, no timeout is applied. + // Default 0. + Timeout int32 +} + +type WorkflowStatus struct { + // Actions is the list of rendered actions and their status. + Actions RenderedActions + + // StartedAt is the time the first action was requested. Nil indicates the workflow has not + // started. + StartedAt *metav1.Time + + // State describes the current state of the workflow. This fields represents a summation of + // action states. Specifically, if all actions succeeded, the workflow will succeed. If one + // action fails, the workflow fails irrespective of previous action status'. + State State + + // Reason describes the reason for failure. It is only relevant when Result is ResultFailed. + // It is propogated from the failed action. + Reason Reason +} + +// RenderedActions is a map of action name to RenderedAction. +type RenderedActions map[ActionName]ActionStatus + +// ActionStatus describes status information about an action. +type ActionStatus struct { + // Rendered is the rendered action. + Rendered Action + + // StartedAt is the time the action was requested. Nil indicates the action has not started. + StartedAt *metav1.Time + + // State describes the current state of the action. + State State + + // Reason describes the reason for failure. It is only relevant when Result is ResultFailed. + Reason Reason + + // Message is a freeform user friendly message describing the specific issue that caused the + // failure. It is only relevant when Result is ResultFailed. + Message string +} + +// State describes the point in time state of a workflow or action. +type State string + +const ( + StatePending State = "Pending" + StateRunning State = "Running" + StateSucceeded State = "Succeeded" + StateFailed State = "Failed" +) + +// Reason is a one-word TitleCase string indicating why a failure occurred. It is not restricted +// to the values defined with this API. +type Reason string + +const ( + ReasonUnknown Reason = "Unknown" + ReasonTimeout Reason = "Timeout" +) +``` + +### Workflow state transition + +![Workflow state transitions](https://raw.githubusercontent.com/tinkerbell/roadmap/b7b2362997c01ffa52758e10259b8f55e81f7447/design/14_tinkerbell_crd_refactor/workflow_state_machine.png) + +Actions follow a similar state transition model. + +### Webhooks + +We will introduce a set of basic webhooks for validating each CRD. The webhooks will only validate the `v1alpha2` API version. They will be implemented as part of the `tink-controller` component. + +### Data and functions available during template rendering + +Templates will have access to a subset of `Hardware` data when they are rendered. Injecting `Hardware` data was outlined in a [previous proposal](https://github.com/tinkerbell/proposals/tree/e24b19a628c6b1ecaafd566667155ca5d6fd6f70/proposals/0028). The existing implementation only injects disk that will, based on this proposal, be sources from the `.Hardware.StorageDevices` list. + +The previous proposal did not outline a set of custom functions injected into templates. The custom functions are detailed in [`template_funcs.go`](https://github.com/tinkerbell/tink/blob/main/internal/workflow/template_funcs.go) and include: + +* `contains`: determine if a string contains a substring. +* `hasPrefix`: determine if a string has a prefix. +* `hasSuffix`: determine if a string has a suffix. +* `formatPartition`: formats a string representing a device path with a specific partition. For example, `{{ formatPartition "/dev/sda" 1 }}` will result in `/dev/sda1`. + +These functions will continue to be available during template rendering. + +### Hegel Changes + +Hegel will undergo a reduction in the endpoints it serves because the data is no longer available in the `Hardware` resource. Minimally, Hegel will serve the following endpoints: + +* `/2009-04-04/meta-data/instance-id` +* `/2009-04-04/user-data` + +## Migrating from v1alpha1 to v1alpha2 + +Given the relative immaturity of the Tinkerbell API we will not provide any migration tooling from `v1alpha1` to `v1alpha2`. Users will be required to manually convert `Hardware` and `Template` resources. `Workflow` resources are considered ephemeral and can generally be deleted. + +## Rationale + +### Comparison with existing resources + +**Hardware** + +The features available to toggle DHCP behavior have been reduced to 2 options specified on the `NetworkInterface` data structure: `DisableNetboot` and `DisableDHCP`. This is in contrast to the existing `Hardware` that defines 4 different properties across various data structures including the `State` fields that require specific, undocumented string values to alter behavior. + +We rationalized that a machine can only boot a single OSIE at a time because operators do not regularly change the OSIE they want to use, and that there are weak use-cases for multi-OSIE support. Consequently, the ability to support different OSIEs per network interface has been removed in favor of supporting a single OSIE definition. + +Numerous fields served by Hegel have been removed with only the fields necessary for honoring cloud-init remaining under the `Instance` field. This significantly reduces the data available for Hegel to serve and _will_ break some Tinkerbell maintained actions that leverage Hegel data to configure disks. A separate proposal will address user defined metadata and how Hegel will serve that data. + +**Template** + +Templates are explicitly defined as part of the CRD in contrast to the free-form string that required a specific, largely undocumented, format in the existing definition. This will signficantly improve the user experience as it establishes clear expectations and feature support. + +[Tasks](https://github.com/tinkerbell/tink/blob/main/internal/workflow/types.go#L13) used for multi-worker workflows have been removed. Multi-worker workflows have been unsupported since the transition to the Kubernetes backend. This, subsequently, removes the need to specify device IDs (device IDs are specified on the `Workflow`) when rendering templates simplifying the `Workflow` and `Template` relationship. + +The `PID` field has been removed in favor of a `Network` field. This facilitates known use-cases that require the container to run in the host network namespace. + +`On*` fields have been removed. The `On*` fields provided rudimentary event based mechanisms for executing arbitrary logic. The semantics were unclear and use-cases unknown. + +All fields on the `TemplateSpec` will support templatable values in accordance with the [`test/template` Go standard library package](https://pkg.go.dev/text/template). + +**Workflow** + +Tasks no longer feature on `WorkflowStatus` as they pertain to multi-worker workflows that are no longer supported. + +Reasons for failures have been introduced as a core concept, via the `Reason` field, around status reporting of actions. This enables programmatic failure reason identification and comparison. A future proposal will address how users can provide customized machine comparable reasons for failed actions. + +The `Workflow` provides a `TemplateData` field that can be used to inject arbitrary data into templates. This facilitates users wanting to model variable data on `Template`s that has per-`Workflow` values. + +## Implementation Plan + +1. All services that have unreleased changes will be released under a new version. This provides a baseline of functional services that can be used in the Tinkerbell Helm charts. + +2. Develop the code to leverage the `v1alpha2` API. + +3. Hard cut over to the new API version. This means versions beyond those specified during (1) will no longer support the `v1alpha1` API. + +## Future Work + +Introducing mechanisms to propagate reasons and user readable messages for action failure. As these mechanisms do not exist currently and will not be realized through this proposal, the `.Workflow.Status.Reason` and `.Workflow.Status.Message` will have minimal benefit to the end user. The separate proposal will address the contract between actions and Tink Worker that can be used to propogate a reason and message to workflows. + +Maintainers have informally discussed inverting the relationship between `Hardware` and Rufio CRDs. This is necessary for defining a precedent on how to extend Tinkerbell functionality without expanding the scope of core Tinkerbell CRDs. + +Introduction of user defined metadata to be served by Hegel that could facilitate user defined actions. Similarly, injection of additional `Hardware` data when templates are rendered will be addressed on a separate ad-hoc basis. + +Separation of `Hardware.Instance` data into a separate CRD possibly owned by Hegel. Given the instance data is unused by the core Tinkerbell stack it diff --git a/design/14_tinkerbell_crd_refactor/workflow_state.puml b/design/14_tinkerbell_crd_refactor/workflow_state.puml new file mode 100644 index 0000000..3b8c2f5 --- /dev/null +++ b/design/14_tinkerbell_crd_refactor/workflow_state.puml @@ -0,0 +1,13 @@ +@startuml Workflow State Machine + +[*] --> Pending : Workflow reconciled +Pending --> Running : First action requested +Running --> Succeeded +Running --> Failed +Succeeded --> [*] +Failed --> [*] + +Succeeded : All actions completed\nsuccessfully +Failed : An action failed + +@enduml \ No newline at end of file diff --git a/design/14_tinkerbell_crd_refactor/workflow_state_machine.png b/design/14_tinkerbell_crd_refactor/workflow_state_machine.png new file mode 100644 index 0000000000000000000000000000000000000000..2470709d63cc8dc17030b66532a2c0b1aef03c4e GIT binary patch literal 14834 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