// Mgmt // Copyright (C) James Shubin and the project contributors // Written by James Shubin 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 . // // 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! } // MatchFilters is a simple helper function to avoid duplicating this loop here. // If any filter matches, it returns nil. Otherwise we error. func MatchFilters(filters []*ResFilter, kind, name, host string) error { // TODO: I'd love to avoid this O(N^2) matching if possible... for _, filter := range filters { if err := filter.Match(kind, name, host); err == nil { return nil } } return fmt.Errorf("not matches found") // did not 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) }