Files
mgmt/engine/world.go
James Shubin 748f05732a engine, etcd: Watch on star pattern for all hostnames
We forgot to watch on star hostname matches.
2025-04-05 15:45:44 -04:00

240 lines
9.9 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.
// XXX: Add a WithStar(true) option to add in the * hostname matching.
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)
}