manager.go (191 lines of code) (raw):
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package resource
import (
"context"
"fmt"
"reflect"
)
// Provider is the interface implemented by providers.
type Provider interface {
}
// Facter is the interface implemented by facters.
// Facters provide, facts, with information about the execution context, they
// can be queried through the manager.
type Facter interface {
// Fact returns the value of a fact for a given name and true if it is found.
// It not found, it returns an empty string and false.
Fact(name string) (value string, found bool)
}
// StaticFacter is a facter implemented as map.
type StaticFacter map[string]string
// Fact returns the value of a fact for a given name and true if it is found.
// It not found, it returns an empty string and false.
func (f StaticFacter) Fact(name string) (value string, found bool) {
if f == nil {
return "", false
}
value, found = f[name]
return
}
// Resource implements management for a resource.
type Resource interface {
// Get gets the current state of a resource. An error is returned if the state couldn't
// be determined. An error here interrupts execution.
Get(context.Context, Scope) (current ResourceState, err error)
// Create implements the creation of the resource. It can return an error, that is reported
// as part of the execution result.
Create(context.Context, Scope) error
// Update implements the upodate of an existing resource. Ir can return an error, that
// is reported as part of the execution result.
Update(context.Context, Scope) error
}
// ResourceState is the state of a resource.
type ResourceState interface {
// Found returns true if the resource exists.
Found(context.Context) bool
// NeedsUpdate returns true if the resource needs update when compared with the given
// resource definition.
NeedsUpdate(ctx context.Context, definition Resource) (bool, error)
}
// Resources is a collection of resources.
type Resources []Resource
// Actions reported on results when applying resources.
const (
// ActionUnknown is used to indicate a failure happening before determining the required action.
ActionUnknown = "unkwnown"
// ActionCreate refers to an action that creates a resource.
ActionCreate = "create"
// ActionUpdate refers to an action that affects an existing resource.
ActionUpdate = "update"
)
// ApplyResult is the result of applying a resource.
type ApplyResult struct {
action string
resource Resource
err error
}
// Err returns an error if the application of a resource failed.
func (r ApplyResult) Err() error {
return r.err
}
// String returns the string representation of the result of applying a resource.
func (r ApplyResult) String() string {
if r.err != nil {
return fmt.Sprintf("{%s: %s, failed: %v}", r.action, r.resource, r.err)
} else {
return fmt.Sprintf("{%s: %s}", r.action, r.resource)
}
}
// ApplyResults is the colection of results when applying a collection of resources.
type ApplyResults []ApplyResult
// Scope contains the information available to resources when applying them.
type Scope interface {
// Provider obtains a provider and sets it in the target.
// The target must be a pointer to a provider type.
// It returns false, and doesn't set the target if no provider is found with
// the given name and target type.
Provider(name string, target any) (found bool)
// Fact returns the value of a fact for a given name and true if it is found.
// It not found, it returns an empty string and false.
Fact(name string) (value string, found bool)
}
// Manager manages application of resources, it contains references to providers and
// facters.
type Manager struct {
providers map[string]Provider
facters []Facter
// TBD: pending to confirm migrating API
migrator *Migrator
}
// NewManager instantiates a new empty manager.
func NewManager() *Manager {
return &Manager{
providers: make(map[string]Provider),
}
}
// Register provider registers a provider in the Manager.
func (m *Manager) RegisterProvider(name string, provider Provider) {
m.providers[name] = provider
}
// withMigrator sets a migrator in the manager.
// TBD: not exposed, pending to confirm migrating API
func (m *Manager) withMigrator(migrator *Migrator) {
m.migrator = migrator
}
// Provider obtains a provider from the context, and sets it in the target.
// The target must be a pointer to a provider type.
// It returns false, and doesn't set the target if no provider is found with
// the given name and target type.
func (m *Manager) Provider(name string, target any) bool {
if target == nil {
panic("target provider shound not be nil")
}
p, found := m.providers[name]
if !found {
return false
}
val := reflect.ValueOf(target)
if !reflect.TypeOf(p).AssignableTo(val.Elem().Type()) {
return false
}
val.Elem().Set(reflect.ValueOf(p))
return true
}
// Apply applies a collection of resources. Depending on their current state,
// resources are created or updated.
func (m *Manager) Apply(resources Resources) (ApplyResults, error) {
return m.ApplyCtx(context.Background(), resources)
}
// ApplyCtx applies a collection of resources with a context that is passed to resource
// operations.
// Depending on their current state, resources are created or updated.
func (m *Manager) ApplyCtx(ctx context.Context, resources Resources) (ApplyResults, error) {
results, err := m.applyMigrations()
if err != nil {
return results, fmt.Errorf("migrator failed: %w", err)
}
resourceResults, err := m.applyResources(ctx, resources)
results = append(results, resourceResults...)
return results, err
}
// applyMigrations applies the configured migrations.
func (m *Manager) applyMigrations() (ApplyResults, error) {
if m.migrator == nil {
return nil, nil
}
// Avoid infinite loops.
managerWithoutMigrator := &Manager{
providers: m.providers,
facters: m.facters,
}
return m.migrator.RunMigrations(managerWithoutMigrator)
}
// applyResources applies a collection of resources. Depending on their current
// state, resources are created or updated.
func (m *Manager) applyResources(ctx context.Context, resources Resources) (ApplyResults, error) {
var results ApplyResults
var errors []error
for _, resource := range resources {
if err := ctx.Err(); err != nil {
errors = append(errors, fmt.Errorf("apply interrupted: %w", err))
break
}
result := m.applyResource(ctx, resource)
if result == nil {
continue
}
if result.err != nil {
errors = append(errors, result.err)
}
results = append(results, *result)
}
return results, newApplyError(errors)
}
// applyResource is a helper function that applies a single resource.
func (m *Manager) applyResource(ctx context.Context, resource Resource) *ApplyResult {
current, err := resource.Get(ctx, m)
if err != nil {
return &ApplyResult{
action: ActionUnknown,
resource: resource,
err: err,
}
}
if !current.Found(ctx) {
err := resource.Create(ctx, m)
return &ApplyResult{
action: ActionCreate,
resource: resource,
err: err,
}
}
needsUpdate, err := current.NeedsUpdate(ctx, resource)
if err != nil {
return &ApplyResult{
action: ActionUnknown,
resource: resource,
err: err,
}
}
if needsUpdate {
err := resource.Update(ctx, m)
return &ApplyResult{
action: ActionUpdate,
resource: resource,
err: err,
}
}
// No action applied to this resource.
return nil
}
// AddFacter adds a facter to the manager. Facters added later have precedence.
func (m *Manager) AddFacter(facter Facter) {
m.facters = append([]Facter{facter}, m.facters...)
}
// Fact returns the value of a fact for a given name and true if it is found.
// It not found, it returns an empty string and false.
// If a fact is available in multiple facters, the value in the last added facter
// is returned.
func (m *Manager) Fact(name string) (string, bool) {
for _, facter := range m.facters {
v, found := facter.Fact(name)
if found {
return v, true
}
}
return "", false
}
// applyError wraps all the errors happened while applying a set of resources.
// Errors can be unwrapped with `Unwrap() []error`.
type applyError struct {
errors []error
}
// newApplyError returns an error wrapping all the given errors, or nil if
// there were no error.
func newApplyError(errors []error) error {
if len(errors) == 0 {
return nil
}
return &applyError{errors: errors}
}
// Error implements the error interface.
func (e *applyError) Error() string {
if len(e.errors) == 1 {
return fmt.Sprintf("there was an apply error: %s", e.errors[0].Error())
}
return fmt.Sprintf("there were %d errors", len(e.errors))
}
// Unwrap allows to access wrapped errors.
func (e *applyError) Unwrap() []error {
return e.errors
}