From 692e07846f60a17aec213a44d8384555775c763f Mon Sep 17 00:00:00 2001 From: Mickael Stanislas Date: Thu, 3 Oct 2024 10:39:03 +0200 Subject: [PATCH] feat: add base of alert and refacto kubeclient (#24) --- api/v1alpha1/alert.go | 9 + api/v1alpha1/alert_discord_types.go | 50 +++++ api/v1alpha1/value_or_valueFrom.go | 78 ++++++++ api/v1alpha1/zz_generated.deepcopy.go | 180 +++++++++++++++++ cmd/kimup/main.go | 22 +-- cmd/kimup/scheduler.go | 5 +- cmd/operator/main.go | 14 +- cmd/webhook/webhook.go | 4 +- .../kimup.cloudavenue.io_alertdiscords.yaml | 183 ++++++++++++++++++ config/rbac/role.yaml | 3 + .../controller/alert_discord_controller.go | 67 +++++++ internal/controller/image_controller.go | 6 +- internal/kubeclient/alert.go | 93 +++++++++ internal/kubeclient/client.go | 135 ++----------- internal/kubeclient/errors.go | 5 + internal/kubeclient/image.go | 161 +++++++++++++++ internal/kubeclient/watch.go | 45 +---- 17 files changed, 879 insertions(+), 181 deletions(-) create mode 100644 api/v1alpha1/alert.go create mode 100644 api/v1alpha1/alert_discord_types.go create mode 100644 api/v1alpha1/value_or_valueFrom.go create mode 100644 config/crd/bases/kimup.cloudavenue.io_alertdiscords.yaml create mode 100644 internal/controller/alert_discord_controller.go create mode 100644 internal/kubeclient/alert.go create mode 100644 internal/kubeclient/errors.go create mode 100644 internal/kubeclient/image.go diff --git a/api/v1alpha1/alert.go b/api/v1alpha1/alert.go new file mode 100644 index 0000000..a65d8c8 --- /dev/null +++ b/api/v1alpha1/alert.go @@ -0,0 +1,9 @@ +package v1alpha1 + +type ( + AlertCoreSpec struct{} + + AlertCoreStatus struct { + Synced bool `json:"synced"` + } +) diff --git a/api/v1alpha1/alert_discord_types.go b/api/v1alpha1/alert_discord_types.go new file mode 100644 index 0000000..f2fbdbc --- /dev/null +++ b/api/v1alpha1/alert_discord_types.go @@ -0,0 +1,50 @@ +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type ( + // AlertDiscordSpec defines the desired state of AlertDiscord + AlertDiscordSpec struct { + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:example:="123456789012345678" + ChannelIDs []string `json:"channelIDs"` + + // +kubebuilder:validation:Optional + CredentialBotToken *ValueOrValueFrom `json:"credentialBotToken,omitempty"` + + // +kubebuilder:validation:Optional + CredentialOAuth2Token *ValueOrValueFrom `json:"credentialOAuth2,omitempty"` + } + + // AlertDiscordStatus defines the observed state of AlertDiscord + AlertDiscordStatus struct { + AlertCoreStatus `json:",inline"` + } +) + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:path=alertdiscords,scope=Cluster + +type AlertDiscord struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec AlertDiscordSpec `json:"spec,omitempty"` + Status AlertDiscordStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +type AlertDiscordList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []AlertDiscord `json:"items"` +} + +func init() { + // SchemeBuilder.Register(&AlertDiscord{}, &AlertDiscordList{}) +} diff --git a/api/v1alpha1/value_or_valueFrom.go b/api/v1alpha1/value_or_valueFrom.go new file mode 100644 index 0000000..7369c39 --- /dev/null +++ b/api/v1alpha1/value_or_valueFrom.go @@ -0,0 +1,78 @@ +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" +) + +type ( + ValueOrValueFrom struct { + // Value is a string value to assign to the key. + // if ValueFrom is specified, this value is ignored. + // +optional + Value string `json:"value,omitempty"` + + // ValueFrom is a reference to a field in a secret or config map. + // +optional + ValueFrom *ValueFromSource `json:"valueFrom,omitempty"` + } + + // ValueFromSource is a reference to a field in a secret or config map. + ValueFromSource struct { + // SecretKeyRef is a reference to a field in a secret. + // +optional + SecretKeyRef *corev1.SecretKeySelector `json:"secretKeyRef,omitempty"` + + // ConfigMapKeyRef is a reference to a field in a config map. + // +optional + ConfigMapKeyRef *corev1.ConfigMapKeySelector `json:"configMapKeyRef,omitempty"` + } +) + +// func (v *ValueOrValueFrom) GetValue(ctx context.Context, namespace, name string) (string, error) { + +// // If ValueFrom is nil, return the value +// if v.ValueFrom == nil { +// return v.Value, nil +// } + +// if v.ValueFrom.SecretKeyRef == nil && v.ValueFrom.ConfigMapKeyRef == nil { +// return "", fmt.Errorf("ValueFrom is specified but neither SecretKeyRef nor ConfigMapKeyRef is set") +// } + +// // // Read from config map +// // if p.ConfigMapKeyRef != nil { +// // var configMap coreV1.ConfigMap +// // objectKey := types.NamespacedName{Namespace: namespace, Name: p.ConfigMapKeyRef.Name} +// // err := r.GetKubeClient().Get(ctx, objectKey, &configMap) +// // if errors.IsNotFound(err) { +// // return "", err +// // } +// // key := p.ConfigMapKeyRef.Key +// // if value, found := configMap.Data[key]; found { +// // return value, nil +// // } +// // return "", brose_errors.NewMapEntryNotFoundError(key, nil) +// // } + +// // // Read from secret +// // if p.SecretKeyRef != nil { +// // var secret coreV1.Secret +// // objectKey := types.NamespacedName{Namespace: namespace, Name: p.SecretKeyRef.Name} +// // err := r.Get(ctx, objectKey, &secret) +// // if errors.IsNotFound(err) { +// // return "", err +// // } +// // key := p.SecretKeyRef.Key +// // valueBase64, foundBase64 := secret.Data[key] +// // valueString, foundString := secret.StringData[key] +// // if !foundBase64 && !foundString { +// // return "", brose_errors.NewMapEntryNotFoundError(key, nil) +// // } else if foundString { +// // return valueString, nil +// // } +// // return string(valueBase64), nil +// // } +// // return "", brose_errors.NewMissingPropertyValueError(name, nil) + +// return "", nil +// } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 855e3dd..7cb7973 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -25,6 +25,141 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AlertCoreSpec) DeepCopyInto(out *AlertCoreSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AlertCoreSpec. +func (in *AlertCoreSpec) DeepCopy() *AlertCoreSpec { + if in == nil { + return nil + } + out := new(AlertCoreSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AlertCoreStatus) DeepCopyInto(out *AlertCoreStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AlertCoreStatus. +func (in *AlertCoreStatus) DeepCopy() *AlertCoreStatus { + if in == nil { + return nil + } + out := new(AlertCoreStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AlertDiscord) DeepCopyInto(out *AlertDiscord) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AlertDiscord. +func (in *AlertDiscord) DeepCopy() *AlertDiscord { + if in == nil { + return nil + } + out := new(AlertDiscord) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AlertDiscord) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AlertDiscordList) DeepCopyInto(out *AlertDiscordList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]AlertDiscord, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AlertDiscordList. +func (in *AlertDiscordList) DeepCopy() *AlertDiscordList { + if in == nil { + return nil + } + out := new(AlertDiscordList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AlertDiscordList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AlertDiscordSpec) DeepCopyInto(out *AlertDiscordSpec) { + *out = *in + if in.ChannelIDs != nil { + in, out := &in.ChannelIDs, &out.ChannelIDs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.CredentialBotToken != nil { + in, out := &in.CredentialBotToken, &out.CredentialBotToken + *out = new(ValueOrValueFrom) + (*in).DeepCopyInto(*out) + } + if in.CredentialOAuth2Token != nil { + in, out := &in.CredentialOAuth2Token, &out.CredentialOAuth2Token + *out = new(ValueOrValueFrom) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AlertDiscordSpec. +func (in *AlertDiscordSpec) DeepCopy() *AlertDiscordSpec { + if in == nil { + return nil + } + out := new(AlertDiscordSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AlertDiscordStatus) DeepCopyInto(out *AlertDiscordStatus) { + *out = *in + out.AlertCoreStatus = in.AlertCoreStatus +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AlertDiscordStatus. +func (in *AlertDiscordStatus) DeepCopy() *AlertDiscordStatus { + if in == nil { + return nil + } + out := new(AlertDiscordStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Image) DeepCopyInto(out *Image) { *out = *in @@ -180,3 +315,48 @@ func (in *ImageTrigger) DeepCopy() *ImageTrigger { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ValueFromSource) DeepCopyInto(out *ValueFromSource) { + *out = *in + if in.SecretKeyRef != nil { + in, out := &in.SecretKeyRef, &out.SecretKeyRef + *out = new(v1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } + if in.ConfigMapKeyRef != nil { + in, out := &in.ConfigMapKeyRef, &out.ConfigMapKeyRef + *out = new(v1.ConfigMapKeySelector) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ValueFromSource. +func (in *ValueFromSource) DeepCopy() *ValueFromSource { + if in == nil { + return nil + } + out := new(ValueFromSource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ValueOrValueFrom) DeepCopyInto(out *ValueOrValueFrom) { + *out = *in + if in.ValueFrom != nil { + in, out := &in.ValueFrom, &out.ValueFrom + *out = new(ValueFromSource) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ValueOrValueFrom. +func (in *ValueOrValueFrom) DeepCopy() *ValueOrValueFrom { + if in == nil { + return nil + } + out := new(ValueOrValueFrom) + in.DeepCopyInto(out) + return out +} diff --git a/cmd/kimup/main.go b/cmd/kimup/main.go index ae1d66d..1fe951a 100644 --- a/cmd/kimup/main.go +++ b/cmd/kimup/main.go @@ -48,7 +48,7 @@ func main() { initScheduler(ctx, k) go func() { - x, err := k.WatchEventsImage(ctx) + x, err := k.Image().Watch(ctx) if err != nil { log.Panicf("Error watching events: %v", err) } @@ -65,20 +65,20 @@ func main() { ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() - an := annotations.New(ctx, event.Value) + an := annotations.New(ctx, &event.Value) switch event.Type { case "ADDED": // Clean old action an.Remove(annotations.KeyAction) - setupTriggers(event.Value) - refreshIfRequired(an, *event.Value) - if err := setTagIfNotExists(ctx, an, event.Value); err != nil { + setupTriggers(&event.Value) + refreshIfRequired(an, event.Value) + if err := setTagIfNotExists(ctx, an, &event.Value); err != nil { log.Errorf("Error setting tag: %v", err) } - if err := k.SetImage(ctx, *event.Value); err != nil { + if err := k.Image().Update(ctx, event.Value); err != nil { log.Errorf("Error updating image: %v", err) } @@ -90,7 +90,7 @@ func main() { for _, trigger := range event.Value.Spec.Triggers { switch trigger.Type { case triggers.Crontab: - cleanTriggers(event.Value) + cleanTriggers(&event.Value) case triggers.Webhook: log.Info("Webhook trigger not implemented yet") } @@ -100,16 +100,16 @@ func main() { an.Remove(annotations.KeyAction) } - refreshIfRequired(an, *event.Value) + refreshIfRequired(an, event.Value) - if err := k.SetImage(ctx, *event.Value); err != nil { + if err := k.Image().Update(ctx, event.Value); err != nil { log.Errorf("Error updating image: %v", err) } - setupTriggers(event.Value) + setupTriggers(&event.Value) case "DELETED": - cleanTriggers(event.Value) + cleanTriggers(&event.Value) } } } diff --git a/cmd/kimup/scheduler.go b/cmd/kimup/scheduler.go index 293ed58..4c18974 100644 --- a/cmd/kimup/scheduler.go +++ b/cmd/kimup/scheduler.go @@ -35,13 +35,12 @@ func initScheduler(ctx context.Context, k *kubeclient.Client) { defer l[e.Data()["namespace"].(string)+"/"+e.Data()["image"].(string)].Unlock() retryErr := retry.RetryOnConflict(retry.DefaultBackoff, func() error { - // TODO: implement image refresh log.Infof("Refreshing image %s in namespace %s", e.Data()["image"], e.Data()["namespace"]) ctx, cancel := context.WithTimeout(ctx, 60*time.Second) defer cancel() - image, err := k.GetImage(ctx, e.Data()["namespace"].(string), e.Data()["image"].(string)) + image, err := k.Image().Get(ctx, e.Data()["namespace"].(string), e.Data()["image"].(string)) if err != nil { if err := crontab.RemoveJob(crontab.BuildKey(e.Data()["namespace"].(string), e.Data()["image"].(string))); err != nil { return err @@ -127,7 +126,7 @@ func initScheduler(ctx context.Context, k *kubeclient.Client) { } } - if err := k.SetImage(ctx, image); err != nil { + if err := k.Image().Update(ctx, image); err != nil { log.Errorf("Error updating image: %v", err) } diff --git a/cmd/operator/main.go b/cmd/operator/main.go index a98613d..87dd639 100644 --- a/cmd/operator/main.go +++ b/cmd/operator/main.go @@ -33,6 +33,7 @@ import ( kimupv1alpha1 "github.com/orange-cloudavenue/kube-image-updater/api/v1alpha1" "github.com/orange-cloudavenue/kube-image-updater/internal/controller" + "github.com/orange-cloudavenue/kube-image-updater/internal/kubeclient" // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. @@ -143,10 +144,17 @@ func main() { os.Exit(1) } + kubeAPIClient, err := kubeclient.NewFromRestConfig(ctrl.GetConfigOrDie()) + if err != nil { + setupLog.Error(err, "unable to create kubeclient") + os.Exit(1) + } + if err = (&controller.ImageReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - Recorder: mgr.GetEventRecorderFor("kimup-operator"), + Client: mgr.GetClient(), + KubeAPIClient: kubeAPIClient, + Scheme: mgr.GetScheme(), + Recorder: mgr.GetEventRecorderFor("kimup-operator"), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Image") os.Exit(1) diff --git a/cmd/webhook/webhook.go b/cmd/webhook/webhook.go index 675b91e..c953299 100644 --- a/cmd/webhook/webhook.go +++ b/cmd/webhook/webhook.go @@ -146,13 +146,13 @@ func createPatch(ctx context.Context, pod *corev1.Pod) ([]byte, error) { var image v1alpha1.Image if crdName == "" { // find the image associated with the pod - image, err = kubeClient.FindImage(ctx, pod.Namespace, container.Image) + image, err = kubeClient.Image().Find(ctx, pod.Namespace, container.Image) if err != nil { warningLogger.Printf("No associated kind Image found: %v", err) continue } } else { - image, err = kubeClient.GetImage(ctx, pod.Namespace, crdName) + image, err = kubeClient.Image().Get(ctx, pod.Namespace, crdName) if err != nil { warningLogger.Printf("Failed to get kind Image: %v", err) continue diff --git a/config/crd/bases/kimup.cloudavenue.io_alertdiscords.yaml b/config/crd/bases/kimup.cloudavenue.io_alertdiscords.yaml new file mode 100644 index 0000000..c0e2cfc --- /dev/null +++ b/config/crd/bases/kimup.cloudavenue.io_alertdiscords.yaml @@ -0,0 +1,183 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.1 + name: alertdiscords.kimup.cloudavenue.io +spec: + group: kimup.cloudavenue.io + names: + kind: AlertDiscord + listKind: AlertDiscordList + plural: alertdiscords + singular: alertdiscord + scope: Cluster + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: AlertDiscordSpec defines the desired state of AlertDiscord + properties: + channelIDs: + items: + type: string + minItems: 1 + type: array + credentialBotToken: + properties: + value: + description: |- + Value is a string value to assign to the key. + if ValueFrom is specified, this value is ignored. + type: string + valueFrom: + description: ValueFrom is a reference to a field in a secret or + config map. + properties: + configMapKeyRef: + description: ConfigMapKeyRef is a reference to a field in + a config map. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: SecretKeyRef is a reference to a field in a secret. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + type: object + credentialOAuth2: + properties: + value: + description: |- + Value is a string value to assign to the key. + if ValueFrom is specified, this value is ignored. + type: string + valueFrom: + description: ValueFrom is a reference to a field in a secret or + config map. + properties: + configMapKeyRef: + description: ConfigMapKeyRef is a reference to a field in + a config map. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: SecretKeyRef is a reference to a field in a secret. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + type: object + required: + - channelIDs + type: object + status: + description: AlertDiscordStatus defines the observed state of AlertDiscord + properties: + synced: + type: boolean + required: + - synced + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index b5dcb8f..6bec031 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -7,6 +7,7 @@ rules: - apiGroups: - kimup.cloudavenue.io resources: + - alertdiscord - images verbs: - create @@ -19,12 +20,14 @@ rules: - apiGroups: - kimup.cloudavenue.io resources: + - alertdiscord/finalizers - images/finalizers verbs: - update - apiGroups: - kimup.cloudavenue.io resources: + - alertdiscord/status - images/status verbs: - get diff --git a/internal/controller/alert_discord_controller.go b/internal/controller/alert_discord_controller.go new file mode 100644 index 0000000..f5628a5 --- /dev/null +++ b/internal/controller/alert_discord_controller.go @@ -0,0 +1,67 @@ +/* +Copyright 2024. + +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 controller + +import ( + "context" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/orange-cloudavenue/kube-image-updater/api/v1alpha1" +) + +// ImageReconciler reconciles a Image object +type AlertDiscordReconciler struct { + client.Client + Scheme *runtime.Scheme + Recorder record.EventRecorder +} + +// +kubebuilder:rbac:groups=kimup.cloudavenue.io,resources=alertdiscord,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=kimup.cloudavenue.io,resources=alertdiscord/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=kimup.cloudavenue.io,resources=alertdiscord/finalizers,verbs=update + +func (r *AlertDiscordReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + _ = log.FromContext(ctx) + + var aDiscord v1alpha1.AlertDiscord + + if err := r.Client.Get(ctx, req.NamespacedName, &aDiscord); err != nil { + log.Log.Error(err, "unable to fetch AlertDiscord") + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + // * Status + + if err := r.Status().Update(ctx, &aDiscord); err != nil { + log.Log.Error(err, "unable to update AlertDiscord status") + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *AlertDiscordReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&v1alpha1.AlertDiscord{}). + Complete(r) +} diff --git a/internal/controller/image_controller.go b/internal/controller/image_controller.go index ae742c7..db20bc6 100644 --- a/internal/controller/image_controller.go +++ b/internal/controller/image_controller.go @@ -28,13 +28,15 @@ import ( kimupv1alpha1 "github.com/orange-cloudavenue/kube-image-updater/api/v1alpha1" "github.com/orange-cloudavenue/kube-image-updater/internal/annotations" + "github.com/orange-cloudavenue/kube-image-updater/internal/kubeclient" ) // ImageReconciler reconciles a Image object type ImageReconciler struct { client.Client - Scheme *runtime.Scheme - Recorder record.EventRecorder + KubeAPIClient *kubeclient.Client + Scheme *runtime.Scheme + Recorder record.EventRecorder } type ImageEvent string diff --git a/internal/kubeclient/alert.go b/internal/kubeclient/alert.go new file mode 100644 index 0000000..dd01e36 --- /dev/null +++ b/internal/kubeclient/alert.go @@ -0,0 +1,93 @@ +package kubeclient + +import ( + "context" + + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" + + "github.com/orange-cloudavenue/kube-image-updater/api/v1alpha1" +) + +type ( + alert struct { + c *Client + } + + AlertList struct { + alert + } + + AlertObj[Obj, List any] struct { + alert + alertClient dynamic.NamespaceableResourceInterface + } +) + +// Alert() returns an alert object +func (c *Client) Alert() *AlertList { + return &AlertList{alert: alert{ + c: c, + }} +} + +// Discord returns an alert object for Discord +func (a *AlertList) Discord() *AlertObj[v1alpha1.AlertDiscord, v1alpha1.AlertDiscordList] { + return &AlertObj[v1alpha1.AlertDiscord, v1alpha1.AlertDiscordList]{ + alert: a.alert, + alertClient: a.c.d.Resource(schema.GroupVersionResource{ + Group: v1alpha1.GroupVersion.Group, + Version: v1alpha1.GroupVersion.Version, + Resource: "alertdiscords", + }), + } +} + +// Get retrieves an Alert object by its name within the specified namespace. +// It takes a context, the namespace, and the name of the Alert as parameters. +// If the Alert is found, it returns a pointer to the Alert object and a nil error. +// If there is an error during the retrieval process, it returns nil and the error encountered. +func (a *AlertObj[Obj, List]) Get(ctx context.Context, namespace, name string) (Obj, error) { + u, err := a.alertClient.Namespace(namespace).Get(ctx, name, v1.GetOptions{}) + if err != nil { + return *new(Obj), err + } + + return decodeUnstructured[Obj](u) +} + +// List retrieves a list of AlertObj instances from the specified namespace. +// It takes a context, the namespace as a string, and list options. +// Returns a pointer to a List of AlertObj and an error if the operation fails. +func (a *AlertObj[Obj, List]) List(ctx context.Context, namespace string, opts v1.ListOptions) (List, error) { + u, err := a.alertClient.Namespace(namespace).List(ctx, opts) + if err != nil { + return *new(List), err + } + + return decodeUnstructured[List](u) +} + +// Update updates an existing alert in the specified namespace. +// +// Parameters: +// - ctx: The context for the operation, which can be used for cancellation and deadlines. +// - namespace: The namespace where the alert is located. +// - alert: A pointer to the alert object that needs to be updated. +// +// Returns: +// - An error if the update operation fails; otherwise, it returns nil. +func (a *AlertObj[Obj, List]) Update(ctx context.Context, namespace string, alert Obj) error { + u, err := encodeUnstructured(alert) + if err != nil { + return err + } + + _, err = a.alertClient.Namespace(namespace).Update(ctx, u, v1.UpdateOptions{}) + if err != nil { + return err + } + + return nil +} diff --git a/internal/kubeclient/client.go b/internal/kubeclient/client.go index bfa13e6..a925336 100644 --- a/internal/kubeclient/client.go +++ b/internal/kubeclient/client.go @@ -12,14 +12,12 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" "github.com/orange-cloudavenue/kube-image-updater/api/v1alpha1" - "github.com/orange-cloudavenue/kube-image-updater/internal/utils" ) type ( @@ -30,13 +28,30 @@ type ( ) func init() { - flag.String("kubeconfig", "", "path to the kubeconfig file") + if flag.Lookup("kubeconfig") == nil { + flag.String("kubeconfig", "", "path to the kubeconfig file") + } } // New creates a new kubernetes client // kubeConfigPath is the path to the kubeconfig file (empty for in-cluster) func New(kubeConfigPath string) (*Client, error) { - client, dynamicClient, err := newClientK8s(kubeConfigPath) + config, err := getConfig(kubeConfigPath) + if err != nil { + return nil, err + } + + return NewFromRestConfig(config) +} + +// NewFromRestConfig creates a new kubernetes client from a rest config +func NewFromRestConfig(config *rest.Config) (*Client, error) { + client, err := kubernetes.NewForConfig(config) + if err != nil { + return nil, err + } + + dynamicClient, err := dynamic.NewForConfig(config) if err != nil { return nil, err } @@ -54,25 +69,6 @@ func getConfig(kubeConfigPath string) (config *rest.Config, err error) { return rest.InClusterConfig() } -func newClientK8s(kubeConfigPath string) (*kubernetes.Clientset, *dynamic.DynamicClient, error) { - config, err := getConfig(kubeConfigPath) - if err != nil { - return nil, nil, err - } - - c, err := kubernetes.NewForConfig(config) - if err != nil { - return nil, nil, err - } - - d, err := dynamic.NewForConfig(config) - if err != nil { - return nil, nil, err - } - - return c, d, nil -} - // GetKubeClient returns the standard kubernetes client func (c *Client) GetKubeClient() *kubernetes.Clientset { return c.c @@ -83,16 +79,6 @@ func (c *Client) GetDynamicClient() *dynamic.DynamicClient { return c.d } -// ! Images - -func (c *Client) cImage() dynamic.NamespaceableResourceInterface { - return c.d.Resource(schema.GroupVersionResource{ - Group: v1alpha1.GroupVersion.Group, - Version: v1alpha1.GroupVersion.Version, - Resource: "images", - }) -} - type UnstructuredFunc interface { UnstructuredContent() map[string]interface{} } @@ -115,89 +101,6 @@ func encodeUnstructured[T any](t T) (*unstructured.Unstructured, error) { return &unstructured.Unstructured{Object: x}, nil } -func (c *Client) listImages(ctx context.Context, namespace string) (list v1alpha1.ImageList, err error) { - var v *unstructured.UnstructuredList - - if namespace == "" { - v, err = c.cImage().List(ctx, metav1.ListOptions{}) - } else { - v, err = c.cImage().Namespace(namespace).List(ctx, metav1.ListOptions{}) - } - if err != nil { - return list, fmt.Errorf("failed to list resources: %w", err) - } - - list, err = decodeUnstructured[v1alpha1.ImageList](v) - if err != nil { - return list, fmt.Errorf("failed to convert resource: %w", err) - } - - return -} - -// ListAllImages lists all images in all namespaces -func (c *Client) ListAllImages(ctx context.Context) (list v1alpha1.ImageList, err error) { - return c.listImages(ctx, "") -} - -// ListImages lists all images in a namespace -func (c *Client) ListImages(ctx context.Context, namespace string) (list v1alpha1.ImageList, err error) { - return c.listImages(ctx, namespace) -} - -// GetImage gets an image in a namespace -func (c *Client) GetImage(ctx context.Context, namespace, name string) (image v1alpha1.Image, err error) { - if namespace == "" { - return image, fmt.Errorf("namespace is required") - } - - if name == "" { - return image, fmt.Errorf("name is required") - } - - v, err := c.cImage().Namespace(namespace).Get(ctx, name, metav1.GetOptions{}) - if err != nil { - return image, fmt.Errorf("failed to get resource: %w", err) - } - - image, err = decodeUnstructured[v1alpha1.Image](v) - if err != nil { - return image, fmt.Errorf("failed to convert resource: %w", err) - } - - return -} - -// SetImage sets an image in a namespace -func (c *Client) SetImage(ctx context.Context, image v1alpha1.Image) (err error) { - unstructedImage, err := encodeUnstructured(image) - if err != nil { - return fmt.Errorf("failed to convert resource: %w", err) - } - - _, err = c.cImage().Namespace(image.Namespace).Update(ctx, unstructedImage, metav1.UpdateOptions{}) - if err != nil { - return fmt.Errorf("failed to update resource: %w", err) - } - - return -} - -// FindImage finds an image in a namespace -func (c *Client) FindImage(ctx context.Context, namespace, name string) (image v1alpha1.Image, err error) { - l, err := c.listImages(ctx, namespace) - if err != nil { - return image, fmt.Errorf("failed to list images: %w", err) - } - for _, i := range l.Items { - if i.GetImageWithoutTag() == utils.ImageParser(name).GetImageWithoutTag() { - return i, nil - } - } - - return image, fmt.Errorf("image not found") -} - type K8sDockerRegistrySecretData struct { Auths map[string]K8sDockerRegistrySecret `json:"auths"` } diff --git a/internal/kubeclient/errors.go b/internal/kubeclient/errors.go new file mode 100644 index 0000000..e3a433c --- /dev/null +++ b/internal/kubeclient/errors.go @@ -0,0 +1,5 @@ +package kubeclient + +import "errors" + +var ErrNotFound = errors.New("not found") diff --git a/internal/kubeclient/image.go b/internal/kubeclient/image.go new file mode 100644 index 0000000..b3e70f2 --- /dev/null +++ b/internal/kubeclient/image.go @@ -0,0 +1,161 @@ +package kubeclient + +import ( + "context" + "fmt" + + log "github.com/sirupsen/logrus" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" + + "github.com/orange-cloudavenue/kube-image-updater/api/v1alpha1" +) + +type ( + ImageObj struct { + c *Client + imageClient dynamic.NamespaceableResourceInterface + } +) + +// Image returns an image object +func (c *Client) Image() *ImageObj { + return &ImageObj{ + c: c, + imageClient: c.d.Resource(schema.GroupVersionResource{ + Group: v1alpha1.GroupVersion.Group, + Version: v1alpha1.GroupVersion.Version, + Resource: "images", + }), + } +} + +// Get retrieves an Image object by its name within the specified namespace. +// It takes a context, the namespace, and the name of the Image as parameters. +// If the Image is found, it returns a pointer to the Image object and a nil error. +// If there is an error during the retrieval process, it returns nil and the error encountered. +func (i *ImageObj) Get(ctx context.Context, namespace, name string) (v1alpha1.Image, error) { + u, err := i.imageClient.Namespace(namespace).Get(ctx, name, v1.GetOptions{}) + if err != nil { + return v1alpha1.Image{}, err + } + + return decodeUnstructured[v1alpha1.Image](u) +} + +// List retrieves a list of images from the specified namespace. +// It takes a context, the namespace as a string, and list options. +// Returns a pointer to a List of images and an error if the operation fails. +func (i *ImageObj) List(ctx context.Context, namespace string, opts v1.ListOptions) (v1alpha1.ImageList, error) { + return i.listImages(ctx, namespace, opts) +} + +// ListAll retrieves a list of images from all namespaces. +// It takes a context and list options as parameters. +// Returns a pointer to a List of images and an error if the operation fails. +func (i *ImageObj) ListAll(ctx context.Context, opts v1.ListOptions) (v1alpha1.ImageList, error) { + return i.listImages(ctx, "", opts) +} + +// listImages lists all images +// It takes a context and a namespace as parameters. +// if namespace is empty, it lists all images in all namespaces. +// Returns a pointer to a List of images and an error if the operation fails. +func (i *ImageObj) listImages(ctx context.Context, namespace string, opts v1.ListOptions) (v1alpha1.ImageList, error) { + var ( + err error + u *unstructured.UnstructuredList + ) + + if namespace == "" { + u, err = i.imageClient.List(ctx, opts) + } else { + u, err = i.imageClient.Namespace(namespace).List(ctx, opts) + } + if err != nil { + return v1alpha1.ImageList{}, fmt.Errorf("failed to list resources: %w", err) + } + + return decodeUnstructured[v1alpha1.ImageList](u) +} + +// Update the image object in the specified namespace. +// +// Parameters: +// - ctx: The context for the operation, which can be used for cancellation and deadlines. +// - namespace: The namespace in which the image object resides. +// - image: A pointer to the image object to be updated. +// +// Returns: +// - An error if the update operation fails; otherwise, it returns nil. +func (i *ImageObj) Update(ctx context.Context, image v1alpha1.Image) error { + u, err := encodeUnstructured(image) + if err != nil { + return err + } + + _, err = i.imageClient.Namespace(image.Namespace).Update(ctx, u, v1.UpdateOptions{}) + if err != nil { + return err + } + + return err +} + +// Find finds an image by its image name. Example: `docker.io/library/nginx:latest` +// It takes a context and the image name as parameters. +// Returns a pointer to the Image object and an error if the operation fails. +func (i *ImageObj) Find(ctx context.Context, namespace, imageName string) (v1alpha1.Image, error) { + images, err := i.listImages(ctx, namespace, v1.ListOptions{}) + if err != nil { + return v1alpha1.Image{}, err + } + + for _, image := range images.Items { + if image.Spec.Image == imageName { + return image, nil + } + } + + return v1alpha1.Image{}, fmt.Errorf("image %s %w", imageName, ErrNotFound) +} + +// Watch watches for changes to the image object. +// It takes a context and the namespace as parameters. +// Returns a channel of WatchInterface[v1alpha1.Image] and an error if the operation fails. +func (i *ImageObj) Watch(ctx context.Context) (chan WatchInterface[v1alpha1.Image], error) { + x, err := i.imageClient.Watch(ctx, v1.ListOptions{}) + if err != nil { + return nil, err + } + + ch := make(chan WatchInterface[v1alpha1.Image]) + + go func() { + for { + select { + case <-ctx.Done(): + close(ch) + x.Stop() + return + case event, ok := <-x.ResultChan(): + if !ok { + close(ch) + return + } + + image, err := decodeUnstructured[v1alpha1.Image](event.Object.(*unstructured.Unstructured)) + if err != nil { + log.Errorf("Error decoding image: %v", err) + continue + } + + ch <- WatchInterface[v1alpha1.Image]{Type: event.Type, Value: image} + } + } + }() + + return ch, nil +} diff --git a/internal/kubeclient/watch.go b/internal/kubeclient/watch.go index aff3e43..a4e8fa2 100644 --- a/internal/kubeclient/watch.go +++ b/internal/kubeclient/watch.go @@ -1,55 +1,12 @@ package kubeclient import ( - "context" - - log "github.com/sirupsen/logrus" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/watch" - - "github.com/orange-cloudavenue/kube-image-updater/api/v1alpha1" ) type ( WatchInterface[T any] struct { Type watch.EventType - Value *T + Value T } ) - -// WatchEventsImage watches events for an image in a namespace -func (c *Client) WatchEventsImage(ctx context.Context) (chan WatchInterface[v1alpha1.Image], error) { - x, err := c.cImage().Watch(ctx, metav1.ListOptions{}) - if err != nil { - return nil, err - } - - ch := make(chan WatchInterface[v1alpha1.Image]) - - go func() { - for { - select { - case <-ctx.Done(): - close(ch) - x.Stop() - return - case event, ok := <-x.ResultChan(): - if !ok { - close(ch) - return - } - - image, err := decodeUnstructured[*v1alpha1.Image](event.Object.(*unstructured.Unstructured)) - if err != nil { - log.Errorf("Error decoding image: %v", err) - continue - } - - ch <- WatchInterface[v1alpha1.Image]{Type: event.Type, Value: image} - } - } - }() - - return ch, nil -}