From 1d3aa6060e37e646d53319a05897ea4434d26f7e Mon Sep 17 00:00:00 2001 From: Dylan Murray Date: Mon, 4 Feb 2019 16:41:13 -0500 Subject: [PATCH] pkg/ansible; Add support to watch cluster-scoped resources (#924) (#1031) * pkg/ansible; Add support to watch cluster-scoped resources= * Update proxy command in images/scorecard-proxy --- doc/ansible/dev/advanced_options.md | 1 + images/scorecard-proxy/cmd/proxy/main.go | 3 +- pkg/ansible/controller/controller.go | 15 +- pkg/ansible/operator/operator.go | 11 +- .../proxy/controllermap/controllermap.go | 140 +++++++++++++++++ pkg/ansible/proxy/kubeconfig/kubeconfig.go | 8 +- pkg/ansible/proxy/proxy.go | 148 +++++++++--------- pkg/ansible/proxy/proxy_test.go | 3 +- pkg/ansible/run.go | 3 +- pkg/ansible/runner/fake/runner.go | 14 +- pkg/ansible/runner/runner.go | 74 +++++---- pkg/ansible/runner/runner_test.go | 29 +++- pkg/ansible/runner/testdata/valid.yaml.tmpl | 6 + 13 files changed, 328 insertions(+), 127 deletions(-) create mode 100644 pkg/ansible/proxy/controllermap/controllermap.go diff --git a/doc/ansible/dev/advanced_options.md b/doc/ansible/dev/advanced_options.md index a116bb5f1a..3d741c47cb 100644 --- a/doc/ansible/dev/advanced_options.md +++ b/doc/ansible/dev/advanced_options.md @@ -14,6 +14,7 @@ Some features can be overridden per resource via an annotation on that CR. The o | Reconcile Period | `reconcilePeriod` | time between reconcile runs for a particular CR | ansbile.operator-sdk/reconcile-period | 1m | | Manage Status | `manageStatus` | Allows the ansible operator to manage the conditions section of each resource's status section. | | true | | Watching Dependent Resources | `watchDependentResources` | Allows the ansible operator to dynamically watch resources that are created by ansible | | true | +| Watching Cluster-Scoped Resources | `watchClusterScopedResources` | Allows the ansible operator to watch cluster-scoped resources that are created by ansible | | false | | Max Runner Artifacts | `maxRunnerArtifacts` | Manages the number of [artifact directories](https://ansible-runner.readthedocs.io/en/latest/intro.html#runner-artifacts-directory-hierarchy) that ansible runner will keep in the operator container for each individual resource. | ansible.operator-sdk/max-runner-artifacts | 20 | diff --git a/images/scorecard-proxy/cmd/proxy/main.go b/images/scorecard-proxy/cmd/proxy/main.go index 90162e3796..6c4ddd662f 100644 --- a/images/scorecard-proxy/cmd/proxy/main.go +++ b/images/scorecard-proxy/cmd/proxy/main.go @@ -19,6 +19,7 @@ import ( "os" proxy "github.com/operator-framework/operator-sdk/pkg/ansible/proxy" + "github.com/operator-framework/operator-sdk/pkg/ansible/proxy/controllermap" "github.com/operator-framework/operator-sdk/pkg/k8sutil" log "github.com/sirupsen/logrus" @@ -47,7 +48,7 @@ func main() { } done := make(chan error) - cMap := proxy.NewControllerMap() + cMap := controllermap.NewControllerMap() // start the proxy err = proxy.Run(done, proxy.Options{ diff --git a/pkg/ansible/controller/controller.go b/pkg/ansible/controller/controller.go index 548f1c0d63..3c164fda53 100644 --- a/pkg/ansible/controller/controller.go +++ b/pkg/ansible/controller/controller.go @@ -40,13 +40,14 @@ var log = logf.Log.WithName("ansible-controller") // Options - options for your controller type Options struct { - EventHandlers []events.EventHandler - LoggingLevel events.LogLevel - Runner runner.Runner - GVK schema.GroupVersionKind - ReconcilePeriod time.Duration - ManageStatus bool - WatchDependentResources bool + EventHandlers []events.EventHandler + LoggingLevel events.LogLevel + Runner runner.Runner + GVK schema.GroupVersionKind + ReconcilePeriod time.Duration + ManageStatus bool + WatchDependentResources bool + WatchClusterScopedResources bool } // Add - Creates a new ansible operator controller and adds it to the manager diff --git a/pkg/ansible/operator/operator.go b/pkg/ansible/operator/operator.go index 3e0b27a9b2..5f550cf54a 100644 --- a/pkg/ansible/operator/operator.go +++ b/pkg/ansible/operator/operator.go @@ -21,7 +21,7 @@ import ( "github.com/operator-framework/operator-sdk/pkg/ansible/controller" "github.com/operator-framework/operator-sdk/pkg/ansible/flags" - "github.com/operator-framework/operator-sdk/pkg/ansible/proxy" + "github.com/operator-framework/operator-sdk/pkg/ansible/proxy/controllermap" "github.com/operator-framework/operator-sdk/pkg/ansible/runner" "sigs.k8s.io/controller-runtime/pkg/manager" @@ -32,7 +32,7 @@ import ( // Run - A blocking function which starts a controller-runtime manager // It starts an Operator by reading in the values in `./watches.yaml`, adds a controller // to the manager, and finally running the manager. -func Run(done chan error, mgr manager.Manager, f *flags.AnsibleOperatorFlags, cMap *proxy.ControllerMap) { +func Run(done chan error, mgr manager.Manager, f *flags.AnsibleOperatorFlags, cMap *controllermap.ControllerMap) { watches, err := runner.NewFromWatches(f.WatchesFile) if err != nil { logf.Log.WithName("manager").Error(err, "Failed to get watches") @@ -57,7 +57,12 @@ func Run(done chan error, mgr manager.Manager, f *flags.AnsibleOperatorFlags, cM done <- errors.New("failed to add controller") return } - cMap.Store(o.GVK, *ctr, runner.GetWatchDependentResources()) + cMap.Store(o.GVK, &controllermap.ControllerMapContents{Controller: *ctr, + WatchDependentResources: runner.GetWatchDependentResources(), + WatchClusterScopedResources: runner.GetWatchClusterScopedResources(), + WatchMap: controllermap.NewWatchMap(), + UIDMap: controllermap.NewUIDMap(), + }) } done <- mgr.Start(c) } diff --git a/pkg/ansible/proxy/controllermap/controllermap.go b/pkg/ansible/proxy/controllermap/controllermap.go new file mode 100644 index 0000000000..d6a12166f2 --- /dev/null +++ b/pkg/ansible/proxy/controllermap/controllermap.go @@ -0,0 +1,140 @@ +// Copyright 2018 The Operator-SDK Authors +// +// Licensed 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 controllermap + +import ( + "sync" + + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/controller" +) + +// ControllerMap - map of GVK to ControllerMapContents +type ControllerMap struct { + mutex sync.RWMutex + internal map[schema.GroupVersionKind]*ControllerMapContents +} + +// UIDMap - map of UID to namespaced name of owner +type UIDMap struct { + mutex sync.RWMutex + internal map[types.UID]types.NamespacedName +} + +// WatchMap - map of GVK to interface. Determines if resource is being watched already +type WatchMap struct { + mutex sync.RWMutex + internal map[schema.GroupVersionKind]interface{} +} + +// ControllerMapContents- Contains internal data associated with each controller +type ControllerMapContents struct { + Controller controller.Controller + WatchDependentResources bool + WatchClusterScopedResources bool + WatchMap *WatchMap + UIDMap *UIDMap +} + +// NewControllerMap returns a new object that contains a mapping between GVK +// and ControllerMapContents object +func NewControllerMap() *ControllerMap { + return &ControllerMap{ + internal: make(map[schema.GroupVersionKind]*ControllerMapContents), + } +} + +// NewWatchMap - returns a new object that maps GVK to interface to determine +// if resource is being watched +func NewWatchMap() *WatchMap { + return &WatchMap{ + internal: make(map[schema.GroupVersionKind]interface{}), + } +} + +// NewUIDMap - returns a new object that maps UID to namespaced name of owner +func NewUIDMap() *UIDMap { + return &UIDMap{ + internal: make(map[types.UID]types.NamespacedName), + } +} + +// Get - Returns a ControllerMapContents given a GVK as the key. `ok` +// determines if the key exists +func (cm *ControllerMap) Get(key schema.GroupVersionKind) (value *ControllerMapContents, ok bool) { + cm.mutex.RLock() + defer cm.mutex.RUnlock() + value, ok = cm.internal[key] + return value, ok +} + +// Delete - Deletes associated GVK to controller mapping from the ControllerMap +func (cm *ControllerMap) Delete(key schema.GroupVersionKind) { + cm.mutex.Lock() + defer cm.mutex.Unlock() + delete(cm.internal, key) +} + +// Store - Adds a new GVK to controller mapping +func (cm *ControllerMap) Store(key schema.GroupVersionKind, value *ControllerMapContents) { + cm.mutex.Lock() + defer cm.mutex.Unlock() + cm.internal[key] = value +} + +// Get - Checks if GVK is already watched +func (wm *WatchMap) Get(key schema.GroupVersionKind) (value interface{}, ok bool) { + wm.mutex.RLock() + defer wm.mutex.RUnlock() + value, ok = wm.internal[key] + return value, ok +} + +// Delete - Deletes associated watches for a specific GVK +func (wm *WatchMap) Delete(key schema.GroupVersionKind) { + wm.mutex.Lock() + defer wm.mutex.Unlock() + delete(wm.internal, key) +} + +// Store - Adds a new GVK to be watched +func (wm *WatchMap) Store(key schema.GroupVersionKind) { + wm.mutex.Lock() + defer wm.mutex.Unlock() + wm.internal[key] = nil +} + +// Get - Returns a NamespacedName of the owner given a UID +func (um *UIDMap) Get(key types.UID) (value types.NamespacedName, ok bool) { + um.mutex.RLock() + defer um.mutex.RUnlock() + value, ok = um.internal[key] + return value, ok +} + +// Delete - Deletes associated UID to NamespacedName mapping +func (um *UIDMap) Delete(key types.UID) { + um.mutex.Lock() + defer um.mutex.Unlock() + delete(um.internal, key) +} + +// Store - Adds a new UID to NamespacedName mapping +func (um *UIDMap) Store(key types.UID, value types.NamespacedName) { + um.mutex.Lock() + defer um.mutex.Unlock() + um.internal[key] = value +} diff --git a/pkg/ansible/proxy/kubeconfig/kubeconfig.go b/pkg/ansible/proxy/kubeconfig/kubeconfig.go index 1aee677ed7..1f049cc4f5 100644 --- a/pkg/ansible/proxy/kubeconfig/kubeconfig.go +++ b/pkg/ansible/proxy/kubeconfig/kubeconfig.go @@ -64,13 +64,19 @@ type values struct { Namespace string } +type NamespacedOwnerReference struct { + metav1.OwnerReference + Namespace string +} + // Create renders a kubeconfig template and writes it to disk func Create(ownerRef metav1.OwnerReference, proxyURL string, namespace string) (*os.File, error) { + nsOwnerRef := NamespacedOwnerReference{OwnerReference: ownerRef, Namespace: namespace} parsedURL, err := url.Parse(proxyURL) if err != nil { return nil, err } - ownerRefJSON, err := json.Marshal(ownerRef) + ownerRefJSON, err := json.Marshal(nsOwnerRef) if err != nil { return nil, err } diff --git a/pkg/ansible/proxy/proxy.go b/pkg/ansible/proxy/proxy.go index 80b71d79e0..ecd4f37436 100644 --- a/pkg/ansible/proxy/proxy.go +++ b/pkg/ansible/proxy/proxy.go @@ -28,30 +28,25 @@ import ( "net/http" "net/http/httputil" "strings" - "sync" + "github.com/operator-framework/operator-sdk/pkg/ansible/proxy/controllermap" + "github.com/operator-framework/operator-sdk/pkg/ansible/proxy/kubeconfig" k8sRequest "github.com/operator-framework/operator-sdk/pkg/ansible/proxy/requestfactory" "k8s.io/apimachinery/pkg/api/meta" metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" ) -// ControllerMap - map of GVK to controller -type ControllerMap struct { - sync.RWMutex - internal map[schema.GroupVersionKind]controller.Controller - watch map[schema.GroupVersionKind]bool -} - type marshaler interface { MarshalJSON() ([]byte, error) } @@ -189,7 +184,7 @@ func CacheResponseHandler(h http.Handler, informerCache cache.Cache, restMapper // InjectOwnerReferenceHandler will handle proxied requests and inject the // owner refernece found in the authorization header. The Authorization is // then deleted so that the proxy can re-set with the correct authorization. -func InjectOwnerReferenceHandler(h http.Handler, cMap *ControllerMap, restMapper meta.RESTMapper) http.Handler { +func InjectOwnerReferenceHandler(h http.Handler, cMap *controllermap.ControllerMap, restMapper meta.RESTMapper) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { switch req.Method { case http.MethodPost: @@ -223,14 +218,18 @@ func InjectOwnerReferenceHandler(h http.Handler, cMap *ControllerMap, restMapper http.Error(w, m, http.StatusBadRequest) return } - owner := metav1.OwnerReference{} + // Set owner to NamespacedOwnerReference, which has metav1.OwnerReference + // as a subset along with the Namespace of the owner. Please see the + // kubeconfig.NamespacedOwnerReference type for more information. The + // namespace is required when creating the reconcile requests. + owner := kubeconfig.NamespacedOwnerReference{} + json.Unmarshal(authString, &owner) if err := json.Unmarshal(authString, &owner); err != nil { m := "Could not unmarshal auth string" log.Error(err, m) http.Error(w, m, http.StatusInternalServerError) return } - log.Info(fmt.Sprintf("Owner: %#v", owner)) body, err := ioutil.ReadAll(req.Body) if err != nil { @@ -247,7 +246,7 @@ func InjectOwnerReferenceHandler(h http.Handler, cMap *ControllerMap, restMapper http.Error(w, m, http.StatusBadRequest) return } - data.SetOwnerReferences(append(data.GetOwnerReferences(), owner)) + data.SetOwnerReferences(append(data.GetOwnerReferences(), owner.OwnerReference)) newBody, err := json.Marshal(data.Object) if err != nil { m := "Could not serialize body" @@ -277,17 +276,16 @@ func InjectOwnerReferenceHandler(h http.Handler, cMap *ControllerMap, restMapper return } - dataClusterScoped := dataMapping.Scope.Name() != meta.RESTScopeNameRoot - ownerClusterScoped := ownerMapping.Scope.Name() != meta.RESTScopeNameRoot - if !ownerClusterScoped || dataClusterScoped { - // add watch for resource - err = addWatchToController(owner, cMap, data) - if err != nil { - m := "could not add watch to controller" - log.Error(err, m) - http.Error(w, m, http.StatusInternalServerError) - return - } + dataNamespaceScoped := dataMapping.Scope.Name() != meta.RESTScopeNameRoot + ownerNamespaceScoped := ownerMapping.Scope.Name() != meta.RESTScopeNameRoot + useOwnerReference := !ownerNamespaceScoped || dataNamespaceScoped + // add watch for resource + err = addWatchToController(owner, cMap, data, useOwnerReference) + if err != nil { + m := "could not add watch to controller" + log.Error(err, m) + http.Error(w, m, http.StatusInternalServerError) + return } } // Removing the authorization so that the proxy can set the correct authorization. @@ -328,7 +326,7 @@ type Options struct { KubeConfig *rest.Config Cache cache.Cache RESTMapper meta.RESTMapper - ControllerMap *ControllerMap + ControllerMap *controllermap.ControllerMap WatchedNamespaces []string DisableCache bool } @@ -401,7 +399,7 @@ func Run(done chan error, o Options) error { return nil } -func addWatchToController(owner metav1.OwnerReference, cMap *ControllerMap, resource *unstructured.Unstructured) error { +func addWatchToController(owner kubeconfig.NamespacedOwnerReference, cMap *controllermap.ControllerMap, resource *unstructured.Unstructured, useOwnerReference bool) error { gv, err := schema.ParseGroupVersion(owner.APIVersion) if err != nil { return err @@ -411,60 +409,64 @@ func addWatchToController(owner metav1.OwnerReference, cMap *ControllerMap, reso Version: gv.Version, Kind: owner.Kind, } - c, watch, ok := cMap.Get(gvk) + contents, ok := cMap.Get(gvk) if !ok { return errors.New("failed to find controller in map") } + wMap := contents.WatchMap + uMap := contents.UIDMap + // Store UID + uMap.Store(owner.UID, types.NamespacedName{ + Name: owner.Name, + Namespace: owner.Namespace, + }) u := &unstructured.Unstructured{} u.SetGroupVersionKind(gvk) // Add a watch to controller - if watch { - log.Info("Watching child resource", "kind", resource.GroupVersionKind(), "enqueue_kind", u.GroupVersionKind()) - err = c.Watch(&source.Kind{Type: resource}, &handler.EnqueueRequestForOwner{OwnerType: u}) - if err != nil { - return err + if contents.WatchDependentResources { + // Use EnqueueRequestForOwner unless user has configured watching cluster scoped resources + if useOwnerReference && !contents.WatchClusterScopedResources { + _, exists := wMap.Get(resource.GroupVersionKind()) + // If already watching resource no need to add a new watch + if exists { + return nil + } + log.Info("Watching child resource", "kind", resource.GroupVersionKind(), "enqueue_kind", u.GroupVersionKind()) + // Store watch in map + wMap.Store(resource.GroupVersionKind()) + err = contents.Controller.Watch(&source.Kind{Type: resource}, &handler.EnqueueRequestForOwner{OwnerType: u}) + } else if contents.WatchClusterScopedResources { + // Use Map func since EnqueuRequestForOwner won't work + // Check if resource is already watched + _, exists := wMap.Get(resource.GroupVersionKind()) + if exists { + return nil + } + log.Info("Watching child resource which can be cluster-scoped", "kind", resource.GroupVersionKind(), "enqueue_kind", u.GroupVersionKind()) + // Store watch in map + wMap.Store(resource.GroupVersionKind()) + // Add watch + err = contents.Controller.Watch( + &source.Kind{Type: resource}, + &handler.EnqueueRequestsFromMapFunc{ToRequests: handler.ToRequestsFunc(func(a handler.MapObject) []reconcile.Request { + log.V(2).Info("Creating reconcile request from object", "gvk", gvk, "name", a.Meta.GetName()) + ownRefs := a.Meta.GetOwnerReferences() + for _, ref := range ownRefs { + nn, exists := uMap.Get(ref.UID) + if !exists { + continue + } + return []reconcile.Request{ + {NamespacedName: nn}, + } + } + return nil + })}, + ) + if err != nil { + return err + } } } return nil } - -// NewControllerMap returns a new object that contains a mapping between GVK -// and controller -func NewControllerMap() *ControllerMap { - return &ControllerMap{ - internal: make(map[schema.GroupVersionKind]controller.Controller), - watch: make(map[schema.GroupVersionKind]bool), - } -} - -// Get - Returns a controller given a GVK as the key. `watch` in the return -// specifies whether or not the operator will watch dependent resources for -// this controller. `ok` returns whether the query was successful. `controller` -// is the associated controller-runtime controller object. -func (cm *ControllerMap) Get(key schema.GroupVersionKind) (controller controller.Controller, watch, ok bool) { - cm.RLock() - defer cm.RUnlock() - result, ok := cm.internal[key] - if !ok { - return result, false, ok - } - watch, ok = cm.watch[key] - return result, watch, ok -} - -// Delete - Deletes associated GVK to controller mapping from the ControllerMap -func (cm *ControllerMap) Delete(key schema.GroupVersionKind) { - cm.Lock() - defer cm.Unlock() - delete(cm.internal, key) -} - -// Store - Adds a new GVK to controller mapping. Also creates a mapping between -// GVK and a boolean `watch` that specifies whether this controller is watching -// dependent resources. -func (cm *ControllerMap) Store(key schema.GroupVersionKind, value controller.Controller, watch bool) { - cm.Lock() - defer cm.Unlock() - cm.internal[key] = value - cm.watch[key] = watch -} diff --git a/pkg/ansible/proxy/proxy_test.go b/pkg/ansible/proxy/proxy_test.go index 566b4566e2..92733b2d6e 100644 --- a/pkg/ansible/proxy/proxy_test.go +++ b/pkg/ansible/proxy/proxy_test.go @@ -22,6 +22,7 @@ import ( "testing" "github.com/operator-framework/operator-sdk/internal/util/fileutil" + "github.com/operator-framework/operator-sdk/pkg/ansible/proxy/controllermap" kcorev1 "k8s.io/api/core/v1" kmetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -38,7 +39,7 @@ func TestHandler(t *testing.T) { t.Fatalf("Failed to instantiate manager: %v", err) } done := make(chan error) - cMap := NewControllerMap() + cMap := controllermap.NewControllerMap() err = Run(done, Options{ Address: "localhost", Port: 8888, diff --git a/pkg/ansible/run.go b/pkg/ansible/run.go index fa5cf8ccd1..c693895ec6 100644 --- a/pkg/ansible/run.go +++ b/pkg/ansible/run.go @@ -22,6 +22,7 @@ import ( aoflags "github.com/operator-framework/operator-sdk/pkg/ansible/flags" "github.com/operator-framework/operator-sdk/pkg/ansible/operator" proxy "github.com/operator-framework/operator-sdk/pkg/ansible/proxy" + "github.com/operator-framework/operator-sdk/pkg/ansible/proxy/controllermap" "github.com/operator-framework/operator-sdk/pkg/k8sutil" "github.com/operator-framework/operator-sdk/pkg/leader" sdkVersion "github.com/operator-framework/operator-sdk/version" @@ -74,7 +75,7 @@ func Run(flags *aoflags.AnsibleOperatorFlags) { } done := make(chan error) - cMap := proxy.NewControllerMap() + cMap := controllermap.NewControllerMap() // start the proxy err = proxy.Run(done, proxy.Options{ diff --git a/pkg/ansible/runner/fake/runner.go b/pkg/ansible/runner/fake/runner.go index 2200d561f5..ad0260eca5 100644 --- a/pkg/ansible/runner/fake/runner.go +++ b/pkg/ansible/runner/fake/runner.go @@ -25,10 +25,11 @@ import ( // Runner - implements the Runner interface for a GVK that's being watched. type Runner struct { - Finalizer string - ReconcilePeriod time.Duration - ManageStatus bool - WatchDependentResources bool + Finalizer string + ReconcilePeriod time.Duration + ManageStatus bool + WatchDependentResources bool + WatchClusterScopedResources bool // Used to send error if Run should fail. Error error // Job Events that will be sent back from the runs channel @@ -83,6 +84,11 @@ func (r *Runner) GetWatchDependentResources() bool { return r.WatchDependentResources } +// GetWatchClusterScopedResources - get watchClusterScopedResources. +func (r *Runner) GetWatchClusterScopedResources() bool { + return r.WatchClusterScopedResources +} + // GetFinalizer - gets the fake finalizer. func (r *Runner) GetFinalizer() (string, bool) { return r.Finalizer, r.Finalizer != "" diff --git a/pkg/ansible/runner/runner.go b/pkg/ansible/runner/runner.go index a5a3356884..71f5746d55 100644 --- a/pkg/ansible/runner/runner.go +++ b/pkg/ansible/runner/runner.go @@ -54,21 +54,23 @@ type Runner interface { GetReconcilePeriod() (time.Duration, bool) GetManageStatus() bool GetWatchDependentResources() bool + GetWatchClusterScopedResources() bool } // watch holds data used to create a mapping of GVK to ansible playbook or role. // The mapping is used to compose an ansible operator. type watch struct { - Version string `yaml:"version"` - Group string `yaml:"group"` - Kind string `yaml:"kind"` - Playbook string `yaml:"playbook"` - Role string `yaml:"role"` - ReconcilePeriod string `yaml:"reconcilePeriod"` - ManageStatus bool `yaml:"manageStatus"` - WatchDependentResources bool `yaml:"watchDependentResources"` - MaxRunnerArtifacts int `yaml:"maxRunnerArtifacts"` - Finalizer *Finalizer `yaml:"finalizer"` + MaxRunnerArtifacts int `yaml:"maxRunnerArtifacts"` + Version string `yaml:"version"` + Group string `yaml:"group"` + Kind string `yaml:"kind"` + Playbook string `yaml:"playbook"` + Role string `yaml:"role"` + ReconcilePeriod string `yaml:"reconcilePeriod"` + ManageStatus bool `yaml:"manageStatus"` + WatchDependentResources bool `yaml:"watchDependentResources"` + WatchClusterScopedResources bool `yaml:"watchClusterScopedResources"` + Finalizer *Finalizer `yaml:"finalizer"` } // Finalizer - Expose finalizer to be used by a user. @@ -82,9 +84,11 @@ type Finalizer struct { // UnmarshalYaml - implements the yaml.Unmarshaler interface func (w *watch) UnmarshalYAML(unmarshal func(interface{}) error) error { // by default, the operator will manage status and watch dependent resources + // The operator will not manage cluster scoped resources by default. w.ManageStatus = true w.WatchDependentResources = true w.MaxRunnerArtifacts = 20 + w.WatchClusterScopedResources = false // hide watch data in plain struct to prevent unmarshal from calling // UnmarshalYAML again @@ -129,13 +133,13 @@ func NewFromWatches(path string) (map[schema.GroupVersionKind]Runner, error) { } switch { case w.Playbook != "": - r, err := NewForPlaybook(w.Playbook, s, w.Finalizer, reconcilePeriod, w.ManageStatus, w.WatchDependentResources, w.MaxRunnerArtifacts) + r, err := NewForPlaybook(w.Playbook, s, w.Finalizer, reconcilePeriod, w.ManageStatus, w.WatchDependentResources, w.WatchClusterScopedResources, w.MaxRunnerArtifacts) if err != nil { return nil, err } m[s] = r case w.Role != "": - r, err := NewForRole(w.Role, s, w.Finalizer, reconcilePeriod, w.ManageStatus, w.WatchDependentResources, w.MaxRunnerArtifacts) + r, err := NewForRole(w.Role, s, w.Finalizer, reconcilePeriod, w.ManageStatus, w.WatchDependentResources, w.WatchClusterScopedResources, w.MaxRunnerArtifacts) if err != nil { return nil, err } @@ -148,7 +152,7 @@ func NewFromWatches(path string) (map[schema.GroupVersionKind]Runner, error) { } // NewForPlaybook returns a new Runner based on the path to an ansible playbook. -func NewForPlaybook(path string, gvk schema.GroupVersionKind, finalizer *Finalizer, reconcilePeriod *time.Duration, manageStatus, dependentResources bool, maxArtifacts int) (Runner, error) { +func NewForPlaybook(path string, gvk schema.GroupVersionKind, finalizer *Finalizer, reconcilePeriod *time.Duration, manageStatus, dependentResources, clusterScopedResources bool, maxArtifacts int) (Runner, error) { if !filepath.IsAbs(path) { return nil, fmt.Errorf("playbook path must be absolute for %v", gvk) } @@ -161,10 +165,11 @@ func NewForPlaybook(path string, gvk schema.GroupVersionKind, finalizer *Finaliz cmdFunc: func(ident, inputDirPath string, maxArtifacts int) *exec.Cmd { return exec.Command("ansible-runner", "-vv", "--rotate-artifacts", fmt.Sprintf("%v", maxArtifacts), "-p", path, "-i", ident, "run", inputDirPath) }, - reconcilePeriod: reconcilePeriod, - manageStatus: manageStatus, - watchDependentResources: dependentResources, - maxRunnerArtifacts: maxArtifacts, + maxRunnerArtifacts: maxArtifacts, + reconcilePeriod: reconcilePeriod, + manageStatus: manageStatus, + watchDependentResources: dependentResources, + watchClusterScopedResources: clusterScopedResources, } err := r.addFinalizer(finalizer) if err != nil { @@ -174,7 +179,7 @@ func NewForPlaybook(path string, gvk schema.GroupVersionKind, finalizer *Finaliz } // NewForRole returns a new Runner based on the path to an ansible role. -func NewForRole(path string, gvk schema.GroupVersionKind, finalizer *Finalizer, reconcilePeriod *time.Duration, manageStatus, dependentResources bool, maxArtifacts int) (Runner, error) { +func NewForRole(path string, gvk schema.GroupVersionKind, finalizer *Finalizer, reconcilePeriod *time.Duration, manageStatus, dependentResources, clusterScopedResources bool, maxArtifacts int) (Runner, error) { if !filepath.IsAbs(path) { return nil, fmt.Errorf("role path must be absolute for %v", gvk) } @@ -189,10 +194,11 @@ func NewForRole(path string, gvk schema.GroupVersionKind, finalizer *Finalizer, rolePath, roleName := filepath.Split(path) return exec.Command("ansible-runner", "-vv", "--rotate-artifacts", fmt.Sprintf("%v", maxArtifacts), "--role", roleName, "--roles-path", rolePath, "--hosts", "localhost", "-i", ident, "run", inputDirPath) }, - reconcilePeriod: reconcilePeriod, - manageStatus: manageStatus, - watchDependentResources: dependentResources, - maxRunnerArtifacts: maxArtifacts, + maxRunnerArtifacts: maxArtifacts, + reconcilePeriod: reconcilePeriod, + manageStatus: manageStatus, + watchDependentResources: dependentResources, + watchClusterScopedResources: clusterScopedResources, } err := r.addFinalizer(finalizer) if err != nil { @@ -203,15 +209,16 @@ func NewForRole(path string, gvk schema.GroupVersionKind, finalizer *Finalizer, // runner - implements the Runner interface for a GVK that's being watched. type runner struct { - Path string // path on disk to a playbook or role depending on what cmdFunc expects - GVK schema.GroupVersionKind // GVK being watched that corresponds to the Path - Finalizer *Finalizer - cmdFunc func(ident, inputDirPath string, maxArtifacts int) *exec.Cmd // returns a Cmd that runs ansible-runner - finalizerCmdFunc func(ident, inputDirPath string, maxArtifacts int) *exec.Cmd - reconcilePeriod *time.Duration - manageStatus bool - watchDependentResources bool - maxRunnerArtifacts int + maxRunnerArtifacts int + Path string // path on disk to a playbook or role depending on what cmdFunc expects + GVK schema.GroupVersionKind // GVK being watched that corresponds to the Path + Finalizer *Finalizer + cmdFunc func(ident, inputDirPath string, maxArtifacts int) *exec.Cmd // returns a Cmd that runs ansible-runner + finalizerCmdFunc func(ident, inputDirPath string, maxArtifacts int) *exec.Cmd + reconcilePeriod *time.Duration + manageStatus bool + watchDependentResources bool + watchClusterScopedResources bool } func (r *runner) Run(ident string, u *unstructured.Unstructured, kubeconfig string) (RunResult, error) { @@ -316,6 +323,11 @@ func (r *runner) GetWatchDependentResources() bool { return r.watchDependentResources } +// GetWatchClusterScopedResources - get the watch cluster scoped resources value +func (r *runner) GetWatchClusterScopedResources() bool { + return r.watchClusterScopedResources +} + func (r *runner) GetFinalizer() (string, bool) { if r.Finalizer != nil { return r.Finalizer.Name, true diff --git a/pkg/ansible/runner/runner_test.go b/pkg/ansible/runner/runner_test.go index 3b14a8b4e8..814d5893f9 100644 --- a/pkg/ansible/runner/runner_test.go +++ b/pkg/ansible/runner/runner_test.go @@ -120,9 +120,11 @@ func TestNewFromWatches(t *testing.T) { Group: "app.example.com", Kind: "NoFinalizer", }, - Path: validTemplate.ValidPlaybook, - manageStatus: true, - reconcilePeriod: &twoSeconds, + Path: validTemplate.ValidPlaybook, + manageStatus: true, + reconcilePeriod: &twoSeconds, + watchDependentResources: true, + watchClusterScopedResources: false, }, schema.GroupVersionKind{ Version: "v1alpha1", @@ -134,14 +136,31 @@ func TestNewFromWatches(t *testing.T) { Group: "app.example.com", Kind: "Playbook", }, - Path: validTemplate.ValidPlaybook, - manageStatus: true, + Path: validTemplate.ValidPlaybook, + manageStatus: true, + watchDependentResources: true, + watchClusterScopedResources: false, Finalizer: &Finalizer{ Name: "finalizer.app.example.com", Role: validTemplate.ValidRole, Vars: map[string]interface{}{"sentinel": "finalizer_running"}, }, }, + schema.GroupVersionKind{ + Version: "v1alpha1", + Group: "app.example.com", + Kind: "WatchClusterScoped", + }: runner{ + GVK: schema.GroupVersionKind{ + Version: "v1alpha1", + Group: "app.example.com", + Kind: "WatchClusterScoped", + }, + Path: validTemplate.ValidPlaybook, + manageStatus: true, + watchDependentResources: true, + watchClusterScopedResources: true, + }, schema.GroupVersionKind{ Version: "v1alpha1", Group: "app.example.com", diff --git a/pkg/ansible/runner/testdata/valid.yaml.tmpl b/pkg/ansible/runner/testdata/valid.yaml.tmpl index ec3ec9f64c..f2a7f8c582 100644 --- a/pkg/ansible/runner/testdata/valid.yaml.tmpl +++ b/pkg/ansible/runner/testdata/valid.yaml.tmpl @@ -13,6 +13,12 @@ role: {{ .ValidRole }} vars: sentinel: finalizer_running +- version: v1alpha1 + group: app.example.com + kind: WatchClusterScoped + playbook: {{ .ValidPlaybook }} + reconcilePeriod: 2s + watchClusterScopedResources: true - version: v1alpha1 group: app.example.com kind: NoReconcile