diff --git a/README.md b/README.md index e037f805..4e0b28b9 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Dev teams may of may not be granted permissions to create these objects. In case A `NamespaceConfig` CRD looks as follows: ```yaml -apiVersion: redhat-cop.redhat.io/v1alpha1 +apiVersion: redhatcop.redhat.io/v1alpha1 kind: NamespaceConfig metadata: name: small-size @@ -50,7 +50,7 @@ oc new-project test-namespace-config During the provisioning of the projects to dev teams some, organizations start with T-shirt sized quotas. Here is an example of how this can be done with the Namespace Configuration Controller ```yaml -apiVersion: redhat-cop.redhat.io/v1alpha1 +apiVersion: redhatcop.redhat.io/v1alpha1 kind: NamespaceConfig metadata: name: small-size @@ -68,7 +68,7 @@ spec: requests.cpu: "4" requests.memory: "2Gi" --- -apiVersion: redhat-cop.redhat.io/v1alpha1 +apiVersion: redhatcop.redhat.io/v1alpha1 kind: NamespaceConfig metadata: name: large-size @@ -104,7 +104,7 @@ In most cases isolating one project from other projects is a good way to start. The configuration would look as follows: ```yaml -apiVersion: redhat-cop.redhat.io/v1alpha1 +apiVersion: redhatcop.redhat.io/v1alpha1 kind: NamespaceConfig metadata: name: multitenant @@ -150,7 +150,7 @@ That said limit range can still be useful to define the ratio between request an Here is how it can be done: ```yaml -apiVersion: redhat-cop.redhat.io/v1alpha1 +apiVersion: redhatcop.redhat.io/v1alpha1 kind: NamespaceConfig metadata: name: overcommit-limitrange @@ -184,7 +184,7 @@ oc label namespace overcommit-project overcommit=limited Another scenario is an application needs to talk to the master API and needs to specific permissions to do that. As an example, we are creating a service account with the `registry-viewer` and `registry-editor` accounts. Here is what we can do: ```yaml -apiVersion: redhat-cop.redhat.io/v1alpha1 +apiVersion: redhatcop.redhat.io/v1alpha1 kind: NamespaceConfig metadata: name: special-sa @@ -266,7 +266,7 @@ rules: resourceNames: - forbid-privileged-pods --- -apiVersion: redhat-cop.redhat.io/v1alpha1 +apiVersion: redhatcop.redhat.io/v1alpha1 kind: NamespaceConfig metadata: name: unprivileged-pods diff --git a/deploy/crds/redhatcop_v1alpha1_namespaceconfig_cr.yaml b/deploy/crds/redhatcop_v1alpha1_namespaceconfig_cr.yaml index 3f64850c..f36d8400 100644 --- a/deploy/crds/redhatcop_v1alpha1_namespaceconfig_cr.yaml +++ b/deploy/crds/redhatcop_v1alpha1_namespaceconfig_cr.yaml @@ -1,4 +1,4 @@ -apiVersion: redhat-cop.redhat.io/v1alpha1 +apiVersion: redhatcop.redhat.io/v1alpha1 kind: NamespaceConfig metadata: name: example-namespaceconfig diff --git a/deploy/crds/redhatcop_v1alpha1_namespaceconfig_crd.yaml b/deploy/crds/redhatcop_v1alpha1_namespaceconfig_crd.yaml index 3c002e59..135ef493 100644 --- a/deploy/crds/redhatcop_v1alpha1_namespaceconfig_crd.yaml +++ b/deploy/crds/redhatcop_v1alpha1_namespaceconfig_crd.yaml @@ -10,6 +10,8 @@ spec: plural: namespaceconfigs singular: namespaceconfig scope: Namespaced + subresources: + status: {} validation: openAPIV3Schema: properties: @@ -32,13 +34,20 @@ spec: type: object type: array selector: - description: 'INSERT ADDITIONAL SPEC FIELDS - desired state of cluster - Important: Run "operator-sdk generate k8s" to regenerate code after - modifying this file Add custom validation using kubebuilder tags: - https://book.kubebuilder.io/beyond_basics/generating_crd.html' type: object type: object status: + properties: + lastUpdate: + format: date-time + type: string + reason: + type: string + status: + enum: + - Success + - Failure + type: string type: object version: v1alpha1 versions: diff --git a/examples/multitenant-networkpolicy.yaml b/examples/multitenant-networkpolicy.yaml index 38902950..1e8bacd9 100644 --- a/examples/multitenant-networkpolicy.yaml +++ b/examples/multitenant-networkpolicy.yaml @@ -1,4 +1,4 @@ -apiVersion: redhat-cop.redhat.io/v1alpha1 +apiVersion: redhatcop.redhat.io/v1alpha1 kind: NamespaceConfig metadata: name: multitenant diff --git a/examples/overcommit-limitrange.yaml b/examples/overcommit-limitrange.yaml index c713277a..af7fd8b2 100644 --- a/examples/overcommit-limitrange.yaml +++ b/examples/overcommit-limitrange.yaml @@ -1,4 +1,4 @@ -apiVersion: redhat-cop.redhat.io/v1alpha1 +apiVersion: redhatcop.redhat.io/v1alpha1 kind: NamespaceConfig metadata: name: overcommit-limitrange diff --git a/examples/serviceaccount-permissions.yaml b/examples/serviceaccount-permissions.yaml index 5a624b39..d048a86b 100644 --- a/examples/serviceaccount-permissions.yaml +++ b/examples/serviceaccount-permissions.yaml @@ -1,4 +1,4 @@ -apiVersion: redhat-cop.redhat.io/v1alpha1 +apiVersion: redhatcop.redhat.io/v1alpha1 kind: NamespaceConfig metadata: name: special-sa diff --git a/examples/special-pod.yaml b/examples/special-pod.yaml index 5cd63460..b8f99d28 100644 --- a/examples/special-pod.yaml +++ b/examples/special-pod.yaml @@ -26,7 +26,7 @@ rules: resourceNames: - forbid-privileged-pods --- -apiVersion: redhat-cop.redhat.io/v1alpha1 +apiVersion: redhatcop.redhat.io/v1alpha1 kind: NamespaceConfig metadata: name: unprivileged-pods diff --git a/examples/tshirt-quotas.yaml b/examples/tshirt-quotas.yaml index 7ce8af80..cb047cb4 100644 --- a/examples/tshirt-quotas.yaml +++ b/examples/tshirt-quotas.yaml @@ -1,4 +1,4 @@ -apiVersion: redhat-cop.redhat.io/v1alpha1 +apiVersion: redhatcop.redhat.io/v1alpha1 kind: NamespaceConfig metadata: name: small-size @@ -16,7 +16,7 @@ spec: requests.cpu: "4" requests.memory: "2Gi" --- -apiVersion: redhat-cop.redhat.io/v1alpha1 +apiVersion: redhatcop.redhat.io/v1alpha1 kind: NamespaceConfig metadata: name: large-size diff --git a/pkg/apis/redhatcop/v1alpha1/doc.go b/pkg/apis/redhatcop/v1alpha1/doc.go index d0276e58..1980756f 100644 --- a/pkg/apis/redhatcop/v1alpha1/doc.go +++ b/pkg/apis/redhatcop/v1alpha1/doc.go @@ -1,4 +1,4 @@ // Package v1alpha1 contains API Schema definitions for the redhat-cop v1alpha1 API group // +k8s:deepcopy-gen=package,register -// +groupName=redhat-cop.redhat.io +// +groupName=redhatcop.redhat.io package v1alpha1 diff --git a/pkg/apis/redhatcop/v1alpha1/namespaceconfig_types.go b/pkg/apis/redhatcop/v1alpha1/namespaceconfig_types.go index 8be5460a..7fe60b1a 100644 --- a/pkg/apis/redhatcop/v1alpha1/namespaceconfig_types.go +++ b/pkg/apis/redhatcop/v1alpha1/namespaceconfig_types.go @@ -14,6 +14,7 @@ type NamespaceConfigSpec struct { // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file // Add custom validation using kubebuilder tags: https://book.kubebuilder.io/beyond_basics/generating_crd.html + Selector metav1.LabelSelector `json:"selector,omitempty"` Resources []runtime.RawExtension `json:"resources,omitempty"` } @@ -24,12 +25,18 @@ type NamespaceConfigStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file // Add custom validation using kubebuilder tags: https://book.kubebuilder.io/beyond_basics/generating_crd.html + + // +kubebuilder:validation:Enum=Success,Failure + Status string `json:"status,omitempty"` + LastUpdate metav1.Time `json:"lastUpdate,omitempty"` + Reason string `json:"reason,omitempty"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // NamespaceConfig is the Schema for the namespaceconfigs API // +k8s:openapi-gen=true +// +kubebuilder:subresource:status type NamespaceConfig struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` diff --git a/pkg/apis/redhatcop/v1alpha1/register.go b/pkg/apis/redhatcop/v1alpha1/register.go index 4cbcb1c8..68fd6a9f 100644 --- a/pkg/apis/redhatcop/v1alpha1/register.go +++ b/pkg/apis/redhatcop/v1alpha1/register.go @@ -2,7 +2,7 @@ // Package v1alpha1 contains API Schema definitions for the redhat-cop v1alpha1 API group // +k8s:deepcopy-gen=package,register -// +groupName=redhat-cop.redhat.io +// +groupName=redhatcop.redhat.io package v1alpha1 import ( @@ -12,7 +12,7 @@ import ( var ( // SchemeGroupVersion is group version used to register these objects - SchemeGroupVersion = schema.GroupVersion{Group: "redhat-cop.redhat.io", Version: "v1alpha1"} + SchemeGroupVersion = schema.GroupVersion{Group: "redhatcop.redhat.io", Version: "v1alpha1"} // SchemeBuilder is used to add go types to the GroupVersionKind scheme SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion} diff --git a/pkg/apis/redhatcop/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/redhatcop/v1alpha1/zz_generated.deepcopy.go index a1f8c778..80ad2954 100644 --- a/pkg/apis/redhatcop/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/redhatcop/v1alpha1/zz_generated.deepcopy.go @@ -14,7 +14,7 @@ func (in *NamespaceConfig) DeepCopyInto(out *NamespaceConfig) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) return } @@ -96,6 +96,7 @@ func (in *NamespaceConfigSpec) DeepCopy() *NamespaceConfigSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NamespaceConfigStatus) DeepCopyInto(out *NamespaceConfigStatus) { *out = *in + in.LastUpdate.DeepCopyInto(&out.LastUpdate) return } diff --git a/pkg/apis/redhatcop/v1alpha1/zz_generated.openapi.go b/pkg/apis/redhatcop/v1alpha1/zz_generated.openapi.go index e624544c..cc0dddb2 100644 --- a/pkg/apis/redhatcop/v1alpha1/zz_generated.openapi.go +++ b/pkg/apis/redhatcop/v1alpha1/zz_generated.openapi.go @@ -70,8 +70,7 @@ func schema_pkg_apis_redhatcop_v1alpha1_NamespaceConfigSpec(ref common.Reference Properties: map[string]spec.Schema{ "selector": { SchemaProps: spec.SchemaProps{ - Description: "INSERT ADDITIONAL SPEC FIELDS - desired state of cluster Important: Run \"operator-sdk generate k8s\" to regenerate code after modifying this file Add custom validation using kubebuilder tags: https://book.kubebuilder.io/beyond_basics/generating_crd.html", - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector"), + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector"), }, }, "resources": { @@ -99,9 +98,28 @@ func schema_pkg_apis_redhatcop_v1alpha1_NamespaceConfigStatus(ref common.Referen Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ Description: "NamespaceConfigStatus defines the observed state of NamespaceConfig", - Properties: map[string]spec.Schema{}, + Properties: map[string]spec.Schema{ + "status": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "lastUpdate": { + SchemaProps: spec.SchemaProps{ + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Time"), + }, + }, + "reason": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, }, }, - Dependencies: []string{}, + Dependencies: []string{ + "k8s.io/apimachinery/pkg/apis/meta/v1.Time"}, } } diff --git a/pkg/controller/namespaceconfig/namespaceconfig_controller.go b/pkg/controller/namespaceconfig/namespaceconfig_controller.go index 3f91a463..a1ef553c 100644 --- a/pkg/controller/namespaceconfig/namespaceconfig_controller.go +++ b/pkg/controller/namespaceconfig/namespaceconfig_controller.go @@ -3,24 +3,28 @@ package namespaceconfig import ( "context" "encoding/json" + "math" "strings" + "time" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/client-go/dynamic" - - "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/tools/record" multierror "github.com/hashicorp/go-multierror" + "github.com/operator-framework/operator-sdk/pkg/predicate" redhatcopv1alpha1 "github.com/redhat-cop/namespace-configuration-operator/pkg/apis/redhatcop/v1alpha1" "github.com/redhat-cop/operator-utils/pkg/util" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/discovery" "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/handler" @@ -54,6 +58,7 @@ func newReconciler(mgr manager.Manager) reconcile.Reconciler { ReconcilerBase: util.NewReconcilerBase(mgr.GetClient(), mgr.GetScheme()), DiscoveryClient: *discovery.NewDiscoveryClientForConfigOrDie(mgr.GetConfig()), Config: *mgr.GetConfig(), + recorder: mgr.GetRecorder("namespaceconfiguration-controller"), } } @@ -66,7 +71,7 @@ func add(mgr manager.Manager, r reconcile.Reconciler) error { } // Watch for changes to primary resource NamespaceConfig - err = c.Watch(&source.Kind{Type: &redhatcopv1alpha1.NamespaceConfig{}}, &handler.EnqueueRequestForObject{}) + err = c.Watch(&source.Kind{Type: &redhatcopv1alpha1.NamespaceConfig{}}, &handler.EnqueueRequestForObject{}, predicate.GenerationChangedPredicate{}) if err != nil { return err } @@ -111,6 +116,7 @@ type ReconcileNamespaceConfig struct { util.ReconcilerBase discovery.DiscoveryClient rest.Config + recorder record.EventRecorder } // Reconcile reads that state of the cluster for a NamespaceConfig object and makes changes based on the state read @@ -138,11 +144,26 @@ func (r *ReconcileNamespaceConfig) Reconcile(request reconcile.Request) (reconci return reconcile.Result{}, err } + // if isNotValid(instance) { + // //record event + // //update status + // //return with time + // } + + // if !isInitliazed(instance) { + // err := r.GetClient().Update(context.TODO(), instance) + // if err != nil { + // log.Error(err, "unable to update instance", "instance", instance) + // return reconcile.Result{}, err + // } + // return reconcile.Result{}, nil + // } + //namespaces selected by this instance namespaces, err := r.getSelectedNamespaces(instance) if err != nil { log.Error(err, "unable to retrieve the list of selected namespaces", "selector", instance.Spec.Selector) - return reconcile.Result{}, err + return r.manageError(err, instance) } ownerLabelValue := instance.GetNamespace() + "-" + instance.GetName() ownerSelector := operatorLabel + "=" + ownerLabelValue @@ -165,7 +186,7 @@ func (r *ReconcileNamespaceConfig) Reconcile(request reconcile.Request) (reconci err := r.GetClient().Update(context.TODO(), instance) if err != nil { log.Error(err, "unable to update instance", "instance", instance) - return reconcile.Result{}, err + return r.manageError(err, instance) } return reconcile.Result{}, nil } @@ -176,9 +197,9 @@ func (r *ReconcileNamespaceConfig) Reconcile(request reconcile.Request) (reconci err := r.GetClient().Update(context.TODO(), instance) if err != nil { log.Error(err, "unable to update instance", "instance", instance) - return reconcile.Result{}, err + return r.manageError(err, instance) } - return reconcile.Result{}, err + return reconcile.Result{}, nil } // know types in this cluster @@ -199,7 +220,10 @@ func (r *ReconcileNamespaceConfig) Reconcile(request reconcile.Request) (reconci err1 = multierror.Append(err1, err) } } - return reconcile.Result{}, err1.ErrorOrNil() + if err1.ErrorOrNil() != nil { + return r.manageError(err1.ErrorOrNil(), instance) + } + return r.manageSuccess(instance) } func getObjects(namespaceconfig *redhatcopv1alpha1.NamespaceConfig) ([]unstructured.Unstructured, error) { @@ -475,3 +499,51 @@ func (r *ReconcileNamespaceConfig) getAPIReourceForUnstructured(obj unstructured return res, nil } + +func (r *ReconcileNamespaceConfig) manageError(issue error, instance *redhatcopv1alpha1.NamespaceConfig) (reconcile.Result, error) { + + lastUpdate := instance.Status.LastUpdate.Time + r.recorder.Event(instance, "Warning", "ProcessingError", issue.Error()) + status := redhatcopv1alpha1.NamespaceConfigStatus{ + LastUpdate: metav1.Now(), + Reason: issue.Error(), + Status: "Failure", + } + instance.Status = status + err := r.GetClient().Status().Update(context.Background(), instance) + if err != nil { + log.Error(err, "unable to update status") + return reconcile.Result{ + RequeueAfter: time.Second, + Requeue: true, + }, nil + } + var retryInterval time.Duration + if instance.Status.LastUpdate.IsZero() { + retryInterval = time.Second + } else { + retryInterval = status.LastUpdate.Sub(lastUpdate).Round(time.Second) + } + return reconcile.Result{ + RequeueAfter: time.Duration(math.Min(float64(retryInterval.Nanoseconds()*2), float64(time.Hour.Nanoseconds()*6))), + Requeue: true, + }, nil +} + +func (r *ReconcileNamespaceConfig) manageSuccess(instance *redhatcopv1alpha1.NamespaceConfig) (reconcile.Result, error) { + status := redhatcopv1alpha1.NamespaceConfigStatus{ + LastUpdate: metav1.Now(), + Reason: "", + Status: "Success", + } + instance.Status = status + err := r.GetClient().Status().Update(context.Background(), instance) + if err != nil { + log.Error(err, "unable to update status") + return reconcile.Result{ + RequeueAfter: time.Second, + Requeue: true, + }, nil + } + return reconcile.Result{}, nil +}