diff --git a/api/v1alpha1/condition.go b/api/v1alpha1/condition.go new file mode 100644 index 0000000..c3a6dd4 --- /dev/null +++ b/api/v1alpha1/condition.go @@ -0,0 +1,45 @@ +package v1alpha1 + +import ( + "github.com/redhat-cop/operator-utils/pkg/util/apis" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type Conditions struct { + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` +} + +func getOpt(idx int, opts ...string) string { + if len(opts) < idx+1 { + return "" + } + + return opts[idx] +} + +func (s *Conditions) UpdateCondition(conditionType string, conditionStatus metav1.ConditionStatus, opts ...string) { + s.Conditions = apis.AddOrReplaceCondition(metav1.Condition{ + Type: conditionType, + Status: conditionStatus, + LastTransitionTime: metav1.Now(), + Reason: getOpt(0, opts...), + Message: getOpt(1, opts...), + }, s.Conditions) +} + +func (s *Conditions) SetReady(conditionStatus metav1.ConditionStatus, msg ...string) { + s.UpdateCondition(apis.ReconcileSuccess, conditionStatus, apis.ReconcileSuccessReason, getOpt(0, msg...)) +} + +func (s *Conditions) IsReady() bool { + c, exists := apis.GetCondition(apis.ReconcileSuccess, s.Conditions) + if !exists { + return false + } + + return c.Status == metav1.ConditionTrue +} diff --git a/api/v1alpha1/keycloak_types.go b/api/v1alpha1/keycloak_types.go index f55c38b..4624532 100644 --- a/api/v1alpha1/keycloak_types.go +++ b/api/v1alpha1/keycloak_types.go @@ -92,11 +92,7 @@ type SecretOption struct { // KeycloakStatus defines the observed state of Keycloak type KeycloakStatus struct { - // +patchMergeKey=type - // +patchStrategy=merge - // +listType=map - // +listMapKey=type - Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` + Conditions `json:",inline"` } //+kubebuilder:object:root=true diff --git a/api/v1alpha1/keycloakimport_types.go b/api/v1alpha1/keycloakimport_types.go index ec2cf0a..70b3c10 100644 --- a/api/v1alpha1/keycloakimport_types.go +++ b/api/v1alpha1/keycloakimport_types.go @@ -41,14 +41,29 @@ type KeycloakImportSpec struct { OverrideIfExists bool `json:"overrideIfExists,omitempty"` } +func (ki *KeycloakImportSpec) HasSecretReference(secretName string) bool { + for _, substitution := range ki.Substitutions { + if substitution.Secret.Name == secretName { + return true + } + } + + return false +} + type KeycloakInstance struct { Namespace string `json:"namespace"` Name string `json:"name"` } +const ( + RHBKReadiness string = "RHBKIsReady" +) + // KeycloakImportStatus defines the observed state of KeycloakImport type KeycloakImportStatus struct { - Version VersionedStatus `json:"version,omitempty"` + Version VersionedStatus `json:"version,omitempty"` + Conditions `json:",inline"` } //+kubebuilder:object:root=true diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index a4e1c19..c1b5ea8 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -21,8 +21,8 @@ limitations under the License. package v1alpha1 import ( - "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -43,6 +43,28 @@ func (in *AdminUser) DeepCopy() *AdminUser { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Conditions) DeepCopyInto(out *Conditions) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Conditions. +func (in *Conditions) DeepCopy() *Conditions { + if in == nil { + return nil + } + out := new(Conditions) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Features) DeepCopyInto(out *Features) { *out = *in @@ -181,6 +203,7 @@ func (in *KeycloakImportSpec) DeepCopy() *KeycloakImportSpec { func (in *KeycloakImportStatus) DeepCopyInto(out *KeycloakImportStatus) { *out = *in in.Version.DeepCopyInto(&out.Version) + in.Conditions.DeepCopyInto(&out.Conditions) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KeycloakImportStatus. @@ -295,13 +318,7 @@ func (in *KeycloakSpec) DeepCopy() *KeycloakSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *KeycloakStatus) DeepCopyInto(out *KeycloakStatus) { *out = *in - if in.Conditions != nil { - in, out := &in.Conditions, &out.Conditions - *out = make([]metav1.Condition, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } + in.Conditions.DeepCopyInto(&out.Conditions) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KeycloakStatus. @@ -354,7 +371,7 @@ func (in *SecretOption) DeepCopyInto(out *SecretOption) { *out = *in if in.Secret != nil { in, out := &in.Secret, &out.Secret - *out = new(v1.SecretKeySelector) + *out = new(corev1.SecretKeySelector) (*in).DeepCopyInto(*out) } } @@ -374,7 +391,7 @@ func (in *SecretOptionVar) DeepCopyInto(out *SecretOptionVar) { *out = *in if in.Secret != nil { in, out := &in.Secret, &out.Secret - *out = new(v1.SecretKeySelector) + *out = new(corev1.SecretKeySelector) (*in).DeepCopyInto(*out) } } @@ -395,7 +412,7 @@ func (in *Truststore) DeepCopyInto(out *Truststore) { in.File.DeepCopyInto(&out.File) if in.Password != nil { in, out := &in.Password, &out.Password - *out = new(v1.SecretKeySelector) + *out = new(corev1.SecretKeySelector) (*in).DeepCopyInto(*out) } } diff --git a/config/crd/bases/sso.stakater.com_keycloakimports.yaml b/config/crd/bases/sso.stakater.com_keycloakimports.yaml index d146d7e..0082a9e 100644 --- a/config/crd/bases/sso.stakater.com_keycloakimports.yaml +++ b/config/crd/bases/sso.stakater.com_keycloakimports.yaml @@ -100,6 +100,78 @@ spec: status: description: KeycloakImportStatus defines the observed state of KeycloakImport properties: + conditions: + items: + description: "Condition contains details for one aspect of the current + state of this API Resource.\n---\nThis struct is intended for + direct use as an array at the field path .status.conditions. For + example,\n\n\n\ttype FooStatus struct{\n\t // Represents the + observations of a foo's current state.\n\t // Known .status.conditions.type + are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // + +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t + \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t + \ // other fields\n\t}" + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: |- + type of condition in CamelCase or in foo.example.com/CamelCase. + --- + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + useful (see .node.status.conditions), the ability to deconflict is important. + The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map version: properties: resourceVersions: diff --git a/config/samples/sso_v1alpha1_keycloakimport.yaml b/config/samples/sso_v1alpha1_keycloakimport.yaml index 37b76f1..9293ee8 100644 --- a/config/samples/sso_v1alpha1_keycloakimport.yaml +++ b/config/samples/sso_v1alpha1_keycloakimport.yaml @@ -8,15 +8,15 @@ metadata: spec: keycloakInstance: name: keycloak-sample - namespace: rhsso + namespace: rhbk substitutions: - name: DISPLAY_NAME secret: - name: test-realm-secret - key: displayName + name: realm-secret + key: secretKey1 overrideIfExists: true json: | { "realm": "test-realm", - "displayName": "%.DISPLAY_NAME% changed again" + "displayName": "%.DISPLAY_NAME%" } diff --git a/internal/constants/labels.go b/internal/constants/labels.go index 69ae84d..a9f77a5 100644 --- a/internal/constants/labels.go +++ b/internal/constants/labels.go @@ -2,3 +2,4 @@ package constants const RHBKWatchedResourceLabel = "sso.stakater.com/watched" const RHBKRealmImportLabel = "realm.stakater.com/import" +const RHBKRealmImportRevisionLabel = "realm.stakater.com/import-rev" diff --git a/internal/controller/keycloak_controller.go b/internal/controller/keycloak_controller.go index 374facf..7588c58 100644 --- a/internal/controller/keycloak_controller.go +++ b/internal/controller/keycloak_controller.go @@ -18,7 +18,6 @@ package controller import ( "context" - v12 "github.com/openshift/api/route/v1" "github.com/redhat-cop/operator-utils/pkg/util/apis" ssov1alpha1 "github.com/stakater/rhbk-operator/api/v1alpha1" @@ -31,6 +30,7 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/kustomize/kstatus/status" @@ -68,7 +68,7 @@ func (r *KeycloakReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c } err = serviceResource.CreateOrUpdate(ctx, r.Client) if err != nil { - return ctrl.Result{Requeue: true}, err + return ctrl.Result{}, err } routeResource := resources.RHBKRoute{ @@ -78,7 +78,7 @@ func (r *KeycloakReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c err = routeResource.CreateOrUpdate(ctx, r.Client) if err != nil { - return ctrl.Result{Requeue: true}, err + return ctrl.Result{}, err } discoveryServiceResource := resources.RHBKDiscoveryService{ @@ -87,7 +87,7 @@ func (r *KeycloakReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c } err = discoveryServiceResource.CreateOrUpdate(ctx, r.Client) if err != nil { - return ctrl.Result{Requeue: true}, err + return ctrl.Result{}, err } statefulSetResource := &resources.RHBKStatefulSet{ @@ -96,37 +96,18 @@ func (r *KeycloakReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c Scheme: r.Scheme, } err = statefulSetResource.CreateOrUpdate(ctx, r.Client) - if err != nil { - return ctrl.Result{Requeue: true}, err - } - if err != nil { return ctrl.Result{}, err } - println(resources.CheckStatus(statefulSetResource.Resource)) - + originalCR := cr.DeepCopy() if resources.CheckStatus(statefulSetResource.Resource) == status.CurrentStatus { - cr.Status.Conditions = resources.AddOrReplaceCondition(v14.Condition{ - Type: apis.ReconcileSuccess, - Status: v14.ConditionTrue, - ObservedGeneration: statefulSetResource.Resource.Generation, - LastTransitionTime: v14.Now(), - Reason: apis.ReconcileSuccessReason, - Message: "All resources are ready", - }, cr.Status.Conditions) + cr.Status.Conditions.UpdateCondition(apis.ReconcileSuccess, v14.ConditionTrue, apis.ReconcileSuccessReason, "All resources are ready") } else { - cr.Status.Conditions = resources.AddOrReplaceCondition(v14.Condition{ - Type: apis.ReconcileSuccess, - Status: v14.ConditionFalse, - ObservedGeneration: statefulSetResource.Resource.Generation, - LastTransitionTime: v14.Now(), - Reason: "Reconciling", - Message: "Waiting for resources to be ready", - }, cr.Status.Conditions) + cr.Status.Conditions.UpdateCondition(apis.ReconcileSuccess, v14.ConditionFalse, "Reconciling", "Waiting for resources to be ready") } - return ctrl.Result{}, r.Status().Update(ctx, cr) + return ctrl.Result{}, r.Status().Patch(ctx, cr, client.MergeFrom(originalCR)) } // SetupWithManager sets up the controller with the Manager. @@ -135,6 +116,17 @@ func (r *KeycloakReconciler) SetupWithManager(mgr ctrl.Manager) error { For(&ssov1alpha1.Keycloak{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). Owns(&v1.Service{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). Owns(&v12.Route{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). - Owns(&v13.StatefulSet{}). + Owns(&v13.StatefulSet{}, builder.WithPredicates(predicate.Funcs{ + CreateFunc: func(e event.TypedCreateEvent[client.Object]) bool { + return false + }, + DeleteFunc: func(e event.TypedDeleteEvent[client.Object]) bool { + return true + }, + UpdateFunc: func(e event.TypedUpdateEvent[client.Object]) bool { + current := e.ObjectNew.(*v13.StatefulSet) + return resources.CheckStatus(current) == status.CurrentStatus + }, + })). Complete(r) } diff --git a/internal/controller/keycloakimport_controller.go b/internal/controller/keycloakimport_controller.go index ad4e488..871cea1 100644 --- a/internal/controller/keycloakimport_controller.go +++ b/internal/controller/keycloakimport_controller.go @@ -18,14 +18,16 @@ package controller import ( "context" - "time" - + "github.com/go-logr/logr" ssov1alpha1 "github.com/stakater/rhbk-operator/api/v1alpha1" + "github.com/stakater/rhbk-operator/internal/constants" "github.com/stakater/rhbk-operator/internal/resources" v1 "k8s.io/api/apps/v1" - v12 "k8s.io/api/batch/v1" + v14 "k8s.io/api/batch/v1" v13 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" + v12 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" @@ -34,12 +36,14 @@ import ( "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" ) // KeycloakImportReconciler reconciles a KeycloakImport object type KeycloakImportReconciler struct { client.Client Scheme *runtime.Scheme + logger logr.Logger } //+kubebuilder:rbac:groups=sso.stakater.com,resources=keycloakimports,verbs=get;list;watch;create;update;patch;delete @@ -50,10 +54,11 @@ type KeycloakImportReconciler struct { //+kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch;create;update func (r *KeycloakImportReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - logger := log.FromContext(ctx) + r.logger = log.FromContext(ctx) + r.logger.Info("realm import...") - realmCR := &ssov1alpha1.KeycloakImport{} - err := r.Get(ctx, req.NamespacedName, realmCR) + cr := &ssov1alpha1.KeycloakImport{} + err := r.Get(ctx, req.NamespacedName, cr) if errors.IsNotFound(err) { return ctrl.Result{}, nil @@ -61,15 +66,23 @@ func (r *KeycloakImportReconciler) Reconcile(ctx context.Context, req ctrl.Reque return ctrl.Result{}, err } + original := cr.DeepCopy() + keycloak := &ssov1alpha1.Keycloak{} err = r.Get(ctx, client.ObjectKey{ - Namespace: realmCR.Spec.KeycloakInstance.Namespace, - Name: realmCR.Spec.KeycloakInstance.Name, + Namespace: cr.Spec.KeycloakInstance.Namespace, + Name: cr.Spec.KeycloakInstance.Name, }, keycloak) if err != nil { - logger.Info("RHBK resource is does not exists") - return ctrl.Result{RequeueAfter: 10 * time.Second}, nil + cr.Status.Conditions.SetReady(v12.ConditionFalse, "Failed to fetch RHBK instance. "+err.Error()) + return ctrl.Result{}, r.Status().Patch(ctx, cr, client.MergeFrom(original)) + } + + // Don't do anything if rhbk instance is not ready + if !keycloak.Status.Conditions.IsReady() { + cr.Status.Conditions.SetReady(v12.ConditionFalse, "RHBK instance not ready") + return ctrl.Result{}, r.Status().Patch(ctx, cr, client.MergeFrom(original)) } statefulSet := &v1.StatefulSet{} @@ -79,111 +92,154 @@ func (r *KeycloakImportReconciler) Reconcile(ctx context.Context, req ctrl.Reque }, statefulSet) if err != nil { - logger.Info("RHBK deployment is not yet ready") - return ctrl.Result{RequeueAfter: 10 * time.Second}, nil + cr.Status.Conditions.SetReady(v12.ConditionFalse, "RHBK deployment not ready. "+err.Error()) + return ctrl.Result{}, r.Status().Patch(ctx, cr, client.MergeFrom(original)) } - // Fetch substitutions - substitutions := make(map[string]string) - for _, s := range realmCR.Spec.Substitutions { - secret := &v13.Secret{} - err = r.Get(ctx, client.ObjectKey{ - Name: s.Secret.Name, - Namespace: realmCR.Namespace, - }, secret) - - if err != nil { - logger.Error(err, "error fetching secret") - return ctrl.Result{}, err - } - - substitutions[s.Name] = string(secret.Data[s.Secret.Key]) + importSecret := &resources.ImportRealmSecret{ + ImportCR: cr, + Scheme: r.Scheme, + } + err = importSecret.CreateOrUpdate(ctx, r.Client) + if err != nil { + cr.Status.Conditions.SetReady(v12.ConditionFalse, "Realm secret not ready. "+err.Error()) + return ctrl.Result{}, r.Status().Patch(ctx, cr, client.MergeFrom(original)) } - realmSecret := &v13.Secret{} - err = r.Get(ctx, client.ObjectKey{ - Name: resources.GetImportJobSecretName(realmCR), - Namespace: statefulSet.Namespace, - }, realmSecret) + jobs := &v14.JobList{} + err = r.List(ctx, jobs, client.InNamespace(statefulSet.Namespace), client.MatchingLabelsSelector{ + Selector: labels.SelectorFromSet(map[string]string{ + constants.RHBKRealmImportLabel: cr.Name, + }), + }) if err != nil { - if errors.IsNotFound(err) { - sr := &resources.ImportRealmSecret{ - ImportCR: realmCR, - Scheme: r.Scheme, - } - - err = sr.Build(substitutions) - if err != nil { - return ctrl.Result{}, err - } + cr.Status.Conditions.SetReady(v12.ConditionFalse, "Failed to fetch import job. "+err.Error()) + return ctrl.Result{}, r.Status().Patch(ctx, cr, client.MergeFrom(original)) + } - err = r.Create(ctx, sr.Resource) + var found *v14.Job + for _, job := range jobs.Items { + if job.Labels[constants.RHBKRealmImportRevisionLabel] == importSecret.Resource.ResourceVersion { + found = &job + break + } else { + err = r.Delete(ctx, &job, []client.DeleteOption{ + client.PropagationPolicy(v12.DeletePropagationForeground), + }...) if err != nil { - return ctrl.Result{}, err + cr.Status.Conditions.SetReady(v12.ConditionFalse, "Failed to delete old job. "+err.Error()) + return ctrl.Result{Requeue: true}, r.Status().Patch(ctx, cr, client.MergeFrom(original)) } } - - return ctrl.Result{}, err } - job := &v12.Job{} - err = r.Get(ctx, client.ObjectKey{ - Name: resources.GetImportJobName(realmCR), - Namespace: statefulSet.Namespace, - }, job) - - if err != nil { - if errors.IsNotFound(err) { - job := &resources.ImportJob{ - ImportCR: realmCR, - Scheme: r.Scheme, - StatefulSet: statefulSet, - } - - err := job.Build() - if err != nil { - return ctrl.Result{}, err - } + if found == nil { + importJob := &v14.Job{} + importJob, err = resources.Build(cr, statefulSet, importSecret.Resource.ResourceVersion, r.Scheme) + if err != nil { + cr.Status.Conditions.SetReady(v12.ConditionFalse, "Failed to build import job. "+err.Error()) + return ctrl.Result{}, r.Status().Patch(ctx, cr, client.MergeFrom(original)) + } - err = r.Create(ctx, job.Job) - if err != nil { - return ctrl.Result{}, err - } - } else { - return ctrl.Result{}, err + err = r.Create(ctx, importJob) + if err != nil { + cr.Status.Conditions.SetReady(v12.ConditionFalse, "Failed to create import job. "+err.Error()) + return ctrl.Result{}, r.Status().Patch(ctx, cr, client.MergeFrom(original)) } + + return ctrl.Result{}, nil } - if resources.IsJobCompleted(job) { - return ctrl.Result{}, r.RolloutChanges(ctx, statefulSet) + if !resources.MatchSet(statefulSet.Spec.Template.Annotations, map[string]string{ + "statefulset.kubernetes.io/rollout": importSecret.Resource.ResourceVersion, + }) && resources.IsJobCompleted(found) { + return ctrl.Result{}, r.RolloutChanges(ctx, statefulSet, importSecret.Resource.ResourceVersion) } - return ctrl.Result{}, nil + cr.Status.Conditions.SetReady(v12.ConditionTrue) + return ctrl.Result{}, r.Status().Patch(ctx, cr, client.MergeFrom(original)) } -func (r *KeycloakImportReconciler) RolloutChanges(ctx context.Context, statefulSet *v1.StatefulSet) error { +func (r *KeycloakImportReconciler) RolloutChanges(ctx context.Context, statefulSet *v1.StatefulSet, revision string) error { + original := statefulSet.DeepCopy() if statefulSet.Spec.Template.Annotations == nil { statefulSet.Spec.Template.Annotations = make(map[string]string) } - statefulSet.Spec.Template.Annotations["statefulset.kubernetes.io/rollout"] = time.Now().Format(time.RFC3339) - return r.Update(ctx, statefulSet) + statefulSet.Spec.Template.Annotations["statefulset.kubernetes.io/rollout"] = revision + + return r.Patch(ctx, statefulSet, client.MergeFrom(original)) } // SetupWithManager sets up the controller with the Manager. func (r *KeycloakImportReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). - For(&ssov1alpha1.KeycloakImport{}). - Owns(&v13.Secret{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). - Owns(&v12.Job{}, builder.WithPredicates(predicate.Funcs{ + For(&ssov1alpha1.KeycloakImport{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). + Owns(&v14.Job{}, builder.WithPredicates(predicate.Funcs{ + CreateFunc: func(e event.TypedCreateEvent[client.Object]) bool { + return false + }, + DeleteFunc: func(e event.TypedDeleteEvent[client.Object]) bool { + return false + }, UpdateFunc: func(e event.TypedUpdateEvent[client.Object]) bool { - old := e.ObjectOld.(*v12.Job) - recent := e.ObjectNew.(*v12.Job) + old := e.ObjectOld.(*v14.Job) + current := e.ObjectNew.(*v14.Job) - return !resources.IsJobCompleted(old) && resources.IsJobCompleted(recent) + return !resources.IsJobCompleted(old) && resources.IsJobCompleted(current) }, })). - Watches(&ssov1alpha1.Keycloak{}, &handler.EnqueueRequestForObject{}, - builder.WithPredicates(predicate.GenerationChangedPredicate{})). + Watches(&ssov1alpha1.Keycloak{}, handler.EnqueueRequestsFromMapFunc(r.handleRHBKChanged)). + Watches(&v13.Secret{}, handler.EnqueueRequestsFromMapFunc(r.handleSecretChanged)). Complete(r) } + +func (r *KeycloakImportReconciler) handleSecretChanged(ctx context.Context, object client.Object) []reconcile.Request { + secret := object.(*v13.Secret) + imports := &ssov1alpha1.KeycloakImportList{} + err := r.List(ctx, imports) + if err != nil { + r.logger.Error(err, "unable to list RHBK instances") + return nil + } + + var requests []reconcile.Request + for _, cr := range imports.Items { + if resources.MatchSet(secret.GetLabels(), map[string]string{ + constants.RHBKRealmImportLabel: cr.Name, + }) || cr.Spec.HasSecretReference(secret.Name) { + requests = append(requests, reconcile.Request{ + NamespacedName: client.ObjectKey{ + Namespace: cr.Namespace, + Name: cr.Name, + }, + }) + } + } + + return requests +} + +func (r *KeycloakImportReconciler) handleRHBKChanged(ctx context.Context, object client.Object) []reconcile.Request { + rhbk := object.(*ssov1alpha1.Keycloak) + imports := &ssov1alpha1.KeycloakImportList{} + err := r.List(ctx, imports) + if err != nil { + r.logger.Error(err, "unable to list RHBK instances") + return nil + } + + var requests []reconcile.Request + for _, cr := range imports.Items { + if cr.Spec.KeycloakInstance.Name == rhbk.Name && cr.Spec.KeycloakInstance.Namespace == rhbk.Namespace { + requests = append(requests, reconcile.Request{ + NamespacedName: client.ObjectKey{ + Namespace: cr.Namespace, + Name: cr.Name, + }, + }) + } + } + + return requests +} diff --git a/internal/resources/realm_import_job.go b/internal/resources/realm_import_job.go index 2cc1bf2..2196af7 100644 --- a/internal/resources/realm_import_job.go +++ b/internal/resources/realm_import_job.go @@ -2,24 +2,17 @@ package resources import ( "fmt" - "github.com/stakater/rhbk-operator/api/v1alpha1" "github.com/stakater/rhbk-operator/internal/constants" v1 "k8s.io/api/apps/v1" v12 "k8s.io/api/batch/v1" v14 "k8s.io/api/core/v1" v13 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) -type ImportJob struct { - ImportCR *v1alpha1.KeycloakImport - Scheme *runtime.Scheme - StatefulSet *v1.StatefulSet - Job *v12.Job -} - func GetImportJobName(cr *v1alpha1.KeycloakImport) string { return fmt.Sprintf("%s-import", cr.Name) } @@ -32,8 +25,15 @@ func GetRealmMountPath(cr *v1alpha1.KeycloakImport) string { return fmt.Sprintf("/mnt/realm-import/%s-realm.json", cr.Name) } -func (j *ImportJob) Build() error { - template := j.StatefulSet.Spec.Template.DeepCopy() +func GetImportJobSelectorLabel(importCrName string, secretRevision string) labels.Selector { + return labels.SelectorFromSet(map[string]string{ + constants.RHBKRealmImportLabel: importCrName, + constants.RHBKRealmImportRevisionLabel: secretRevision, + }) +} + +func Build(cr *v1alpha1.KeycloakImport, sts *v1.StatefulSet, revision string, scheme *runtime.Scheme) (*v12.Job, error) { + template := sts.Spec.Template.DeepCopy() template.Labels["app"] = "realm-import-job" kcContainer := &template.Spec.Containers[0] @@ -61,7 +61,7 @@ func (j *ImportJob) Build() error { } // Setup ENV replacement - for _, substitution := range j.ImportCR.Spec.Substitutions { + for _, substitution := range cr.Spec.Substitutions { next = append(next, v14.EnvVar{ Name: substitution.Name, ValueFrom: &v14.EnvVarSource{ @@ -72,16 +72,16 @@ func (j *ImportJob) Build() error { // Setup volume for mounting realm JSON template.Spec.Volumes = append(template.Spec.Volumes, v14.Volume{ - Name: GetImportJobSecretVolumeName(j.ImportCR), + Name: GetImportJobSecretVolumeName(cr), VolumeSource: v14.VolumeSource{ Secret: &v14.SecretVolumeSource{ - SecretName: GetImportJobSecretName(j.ImportCR), + SecretName: GetImportJobSecretName(cr), }, }, }) kcContainer.VolumeMounts = append(kcContainer.VolumeMounts, v14.VolumeMount{ - Name: GetImportJobSecretVolumeName(j.ImportCR), + Name: GetImportJobSecretVolumeName(cr), ReadOnly: true, MountPath: "/mnt/realm-import", }) @@ -102,20 +102,21 @@ func (j *ImportJob) Build() error { "-c", fmt.Sprintf(`%s/opt/keycloak/bin/kc.sh --verbose import --optimized --file='%s' --override=%t`, buildProviders, - GetRealmMountPath(j.ImportCR), - j.ImportCR.Spec.OverrideIfExists), + GetRealmMountPath(cr), + cr.Spec.OverrideIfExists), } kcContainer.Command = cmd kcContainer.Args = args template.Spec.RestartPolicy = v14.RestartPolicyNever - j.Job = &v12.Job{ + job := &v12.Job{ ObjectMeta: v13.ObjectMeta{ - Name: GetImportJobName(j.ImportCR), - Namespace: j.StatefulSet.Namespace, + Name: GetImportJobName(cr), + Namespace: sts.Namespace, Labels: map[string]string{ - constants.RHBKRealmImportLabel: j.ImportCR.Name, + constants.RHBKRealmImportLabel: cr.Name, + constants.RHBKRealmImportRevisionLabel: revision, }, }, Spec: v12.JobSpec{ @@ -124,10 +125,10 @@ func (j *ImportJob) Build() error { }, } - err := controllerutil.SetControllerReference(j.ImportCR, j.Job, j.Scheme) + err := controllerutil.SetControllerReference(cr, job, scheme) if err != nil { - return err + return nil, err } - return nil + return job, nil } diff --git a/internal/resources/realm_import_secret.go b/internal/resources/realm_import_secret.go index 11d52fa..f8af59b 100644 --- a/internal/resources/realm_import_secret.go +++ b/internal/resources/realm_import_secret.go @@ -2,22 +2,25 @@ package resources import ( "bytes" + "context" "fmt" + "github.com/stakater/rhbk-operator/internal/constants" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" "strconv" "text/template" "github.com/stakater/rhbk-operator/api/v1alpha1" - "github.com/stakater/rhbk-operator/internal/constants" v1 "k8s.io/api/core/v1" v12 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) type ImportRealmSecret struct { - ImportCR *v1alpha1.KeycloakImport - Scheme *runtime.Scheme - Resource *v1.Secret + ImportCR *v1alpha1.KeycloakImport + Resource *v1.Secret + Scheme *runtime.Scheme + substitutions map[string]string } func GetImportJobSecretName(cr *v1alpha1.KeycloakImport) string { @@ -28,32 +31,49 @@ func GetImportJobSecretRealmName(cr *v1alpha1.KeycloakImport) string { return fmt.Sprintf("%s-realm.json", cr.Name) } -func (s *ImportRealmSecret) Build(substitutions map[string]string) error { - realm, err := expandTemplate(s.ImportCR.Spec.JSON, substitutions) - if err != nil { - return err - } - +func (s *ImportRealmSecret) CreateOrUpdate(ctx context.Context, c client.Client) error { s.Resource = &v1.Secret{ ObjectMeta: v12.ObjectMeta{ Name: GetImportJobSecretName(s.ImportCR), Namespace: s.ImportCR.Namespace, - Labels: map[string]string{ - constants.RHBKRealmImportLabel: s.ImportCR.Name, - constants.RHBKWatchedResourceLabel: strconv.FormatBool(true), - }, - }, - Data: map[string][]byte{ - GetImportJobSecretRealmName(s.ImportCR): realm, }, } - err = controllerutil.SetControllerReference(s.ImportCR, s.Resource, s.Scheme) + // Fetch substitutions + s.substitutions = make(map[string]string) + for _, sub := range s.ImportCR.Spec.Substitutions { + secret := &v1.Secret{} + err := c.Get(ctx, client.ObjectKey{ + Name: sub.Secret.Name, + Namespace: s.ImportCR.Namespace, + }, secret) + + if err != nil { + return err + } + + s.substitutions[sub.Name] = string(secret.Data[sub.Secret.Key]) + } + + _, err := controllerutil.CreateOrUpdate(ctx, c, s.Resource, s.MutateFn) + return err +} + +func (s *ImportRealmSecret) MutateFn() error { + realm, err := expandTemplate(s.ImportCR.Spec.JSON, s.substitutions) if err != nil { return err } - return nil + s.Resource.Labels = map[string]string{ + constants.RHBKWatchedResourceLabel: strconv.FormatBool(true), + } + + s.Resource.Data = map[string][]byte{ + GetImportJobSecretRealmName(s.ImportCR): realm, + } + + return controllerutil.SetControllerReference(s.ImportCR, s.Resource, s.Scheme) } func expandTemplate(t string, substitutions map[string]string) ([]byte, error) { diff --git a/internal/resources/statefulset.go b/internal/resources/statefulset.go index 37ae327..c8e9f57 100644 --- a/internal/resources/statefulset.go +++ b/internal/resources/statefulset.go @@ -104,7 +104,8 @@ func (ks *RHBKStatefulSet) Build() error { }, Template: v12.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ - Labels: labels, + Labels: labels, + Annotations: ks.Resource.Spec.Template.Annotations, }, Spec: v12.PodSpec{ InitContainers: GetInitContainer(ks.Keycloak), diff --git a/internal/resources/utils.go b/internal/resources/utils.go index 7b8a8bb..8070cd7 100644 --- a/internal/resources/utils.go +++ b/internal/resources/utils.go @@ -3,11 +3,13 @@ package resources import ( "encoding/json" "fmt" + "hash/fnv" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" "strconv" "github.com/stakater/rhbk-operator/internal/constants" v1 "k8s.io/api/batch/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/kustomize/kstatus/status" @@ -74,3 +76,17 @@ func CheckStatus(obj runtime.Object) status.Status { return compute.Status } + +func MatchSet(set1 map[string]string, set2 map[string]string) bool { + selector := labels.SelectorFromSet(set2) + return selector.Matches(labels.Set(set1)) +} + +func GetHash(s string) (uint32, error) { + hasher := fnv.New32a() + _, err := hasher.Write([]byte(s)) + if err != nil { + return 0, err + } + return hasher.Sum32(), nil +}