Files
mgmt/engine/world.go
James Shubin 045b29291e engine, lang: Modern exported resources
I've been waiting to write this patch for a long time. I firmly believe
that the idea of "exported resources" was truly a brilliant one, but
which was never even properly understood by its original inventors! This
patch set aims to show how it should have been done.

The main differences are:

* Real-time modelling, since "once per run" makes no sense.
* Filter with code/functions not language syntax.
* Directed exporting to limit the intended recipients.

The next step is to add more "World" reading and filtering functions to
make it easy and expressive to make your selection of resources to
collect!
2025-04-05 12:45:23 -04:00

239 lines
9.8 KiB
Go

// Mgmt
// Copyright (C) James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
// Additional permission under GNU GPL version 3 section 7
//
// If you modify this program, or any covered work, by linking or combining it
// with embedded mcl code and modules (and that the embedded mcl code and
// modules which link with this program, contain a copy of their source code in
// the authoritative form) containing parts covered by the terms of any other
// license, the licensors of this program grant you additional permission to
// convey the resulting work. Furthermore, the licensors of this program grant
// the original author, James Shubin, additional permission to update this
// additional permission if he deems it necessary to achieve the goals of this
// additional permission.
package engine
import (
"context"
"fmt"
"github.com/purpleidea/mgmt/etcd/interfaces"
"github.com/purpleidea/mgmt/etcd/scheduler"
)
// WorldInit is some data passed in when starting the World interface.
// TODO: This is a lousy struct name, feel free to change it.
type WorldInit struct {
// Hostname is the UUID we use to represent ourselves to everyone else.
Hostname string
// Debug represents if we're running in debug mode or not.
Debug bool
// Logf is a logger which should be used.
Logf func(format string, v ...interface{})
}
// World is an interface to the rest of the different graph state. It allows the
// GAPI to store state and exchange information throughout the cluster. It is
// the interface each machine uses to communicate with the rest of the world.
type World interface { // TODO: is there a better name for this interface?
// Init sets things up and is called once before any other methods.
Init(*WorldInit) error
// Close does some cleanup and is the last method that is ever called.
Close() error
FsWorld
DeployWorld
StrWorld
ResWorld
}
// FsWorld is a world interface for dealing with the core deploy filesystem
// stuff.
type FsWorld interface {
// URI returns the current FS URI.
// TODO: Can we improve this API or deprecate it entirely?
URI() string
// Fs takes a URI and returns the filesystem that corresponds to that.
// This is a way to turn a unique string handle into an appropriate
// filesystem object that we can interact with.
Fs(uri string) (Fs, error)
}
// DeployWorld is a world interface with all of the deploy functions.
type DeployWorld interface {
WatchDeploy(context.Context) (chan error, error)
// TODO: currently unused, but already implemented
//GetDeploys(ctx context.Context) (map[uint64]string, error)
GetDeploy(ctx context.Context, id uint64) (string, error)
GetMaxDeployID(ctx context.Context) (uint64, error)
// TODO: This could be split out to a sub-interface?
AddDeploy(ctx context.Context, id uint64, hash, pHash string, data *string) error
}
// StrWorld is a world interface which is useful for reading, writing, and
// watching strings in a shared, distributed database. It is likely that much of
// the functionality is built upon these primitives.
// XXX: We should consider improving this API if possible.
type StrWorld interface {
StrWatch(ctx context.Context, namespace string) (chan error, error)
StrIsNotExist(error) bool
StrGet(ctx context.Context, namespace string) (string, error)
StrSet(ctx context.Context, namespace, value string) error
StrDel(ctx context.Context, namespace string) error
// XXX: add the exchange primitives in here directly?
StrMapWatch(ctx context.Context, namespace string) (chan error, error)
StrMapGet(ctx context.Context, namespace string) (map[string]string, error)
StrMapSet(ctx context.Context, namespace, value string) error
StrMapDel(ctx context.Context, namespace string) error
}
// ResWorld is a world interface that lets us store, pull and watch resources in
// a distributed database.
// XXX: These API's are likely to change.
// XXX: Add optional TTL's to these API's, maybe use WithTTL(...) type options.
type ResWorld interface {
// ResWatch returns a channel which produces a new value once on startup
// as soon as it is successfully connected, and once for every time it
// sees that a resource that has been exported for this hostname is
// added, deleted, or modified. If kind is specified, the watch will
// attempt to only send events relating to that resource kind. We always
// intended to only show events for resources which the watching host is
// allowed to see.
ResWatch(ctx context.Context, kind string) (chan error, error)
// ResCollect does a lookup for resource entries that have previously
// been stored for us. It returns a subset of these based on the input
// filter. It does not return a Res, since while that would be useful,
// and logical, it turns out we usually want to transport the Res data
// onwards through the function graph, and using a native string is what
// is already supported. (A native res type would just be encoded as a
// string anyways.) While it might be more "correct" to do the work to
// decode the string into a Res, the user of this function would just
// encode it back to a string anyways, and this is not very efficient.
ResCollect(ctx context.Context, filters []*ResFilter) ([]*ResOutput, error)
// ResExport stores a number of resources in the world storage system.
// The individual records should not be updated if they are identical to
// what is already present. (This is to prevent unnecessary events.) If
// this makes no changes, it returns (true, nil). If it makes a change,
// then it returns (false, nil). On any error we return (false, err).
ResExport(ctx context.Context, resourceExports []*ResExport) (bool, error)
// ResDelete deletes a number of resources in the world storage system.
// If this doesn't delete, it returns (true, nil). If it makes a delete,
// then it returns (false, nil). On any error we return (false, err).
ResDelete(ctx context.Context, resourceDeletes []*ResDelete) (bool, error)
}
// ResFilter specifies that we want to match an item with this three tuple. If
// any of these are empty, then it means to match an item with any value for
// that field.
// TODO: Future secure implementations must verify that the exported made a
// value available to that hostname. It's not enough for a host to request it.
// We can enforce this with public key encryption eventually.
type ResFilter struct {
Kind string
Name string
Host string // from this host
}
// Match returns nil on a successful match.
func (obj *ResFilter) Match(kind, name, host string) error {
if obj.Kind != "" && obj.Kind != kind {
return fmt.Errorf("kind did not match")
}
if obj.Name != "" && obj.Name != name {
return fmt.Errorf("name did not match")
}
if obj.Host != "" && obj.Host != host {
return fmt.Errorf("host did not match")
}
return nil // match!
}
// ResOutput represents a record of exported resource data which we have read
// out from the world storage system. The Data field contains an encoded version
// of the resource, and even though decoding it will get you a Kind and Name, we
// still store those values here in duplicate for them to be available before
// decoding.
type ResOutput struct {
Kind string
Name string
Host string // from this host
Data string // encoded res data
}
// ResExport represents a record of exported resource data which we want to save
// to the world storage system. The Data field contains an encoded version of
// the resource, and even though decoding it will get you a Kind and Name, we
// still store those values here in duplicate for them to be available before
// decoding. If Host is specified, then only the node with that hostname may
// access this resource. If it's empty than it may be collected by anyone. If we
// want to export to only three hosts, then we duplicate this entry three times.
// It's true that this is not an efficient use of storage space, but it maps
// logically to a future data structure where data is encrypted to the public
// key of that specific host where we wouldn't be able to de-duplicate anyways.
type ResExport struct {
Kind string
Name string
Host string // to/for this host
Data string // encoded res data
}
// ResDelete represents the uniqueness key for stored resources. As a result,
// this triple is a useful map key in various locations.
type ResDelete struct {
Kind string
Name string
Host string // to/for this host
}
// SchedulerWorld is an interface that has to do with distributed scheduling.
// XXX: This should be abstracted to remove the etcd specific types if possible.
type SchedulerWorld interface {
// Scheduler runs a distributed scheduler.
Scheduler(namespace string, opts ...scheduler.Option) (*scheduler.Result, error)
}
// EtcdWorld is a world interface that should be implemented if the world
// backend is implementing etcd, and if it supports dynamically resizing things.
// TODO: In theory we could generalize this to support other backends, but lets
// assume it's specific to etcd only for now.
type EtcdWorld interface {
IdealClusterSizeWatch(context.Context) (chan error, error)
IdealClusterSizeGet(context.Context) (uint16, error)
IdealClusterSizeSet(context.Context, uint16) (bool, error)
// WatchMembers returns a channel of changing members in the cluster.
WatchMembers(context.Context) (<-chan *interfaces.MembersResult, error)
}