From d1f7e01eb09727b3995b209a877be48810847d51 Mon Sep 17 00:00:00 2001 From: Mickael Stanislas Date: Mon, 14 Oct 2024 15:43:17 +0200 Subject: [PATCH] feat: add kimup admission controller operator --- .goreleaser.yaml | 54 +- Makefile | 12 +- api/v1alpha1/kimup_types.go | 19 +- api/v1alpha1/zz_generated.deepcopy.go | 42 +- .../certificate.go | 0 cmd/{webhook => admission-controller}/main.go | 10 +- .../webhook-configuration.go | 0 .../webhook.go | 0 cmd/kimup/main.go | 21 + .../bases/kimup.cloudavenue.io_kimups.yaml | 40 +- config/manifests/kustomization.yaml | 6 - config/manifests/operator/kustomization.yaml | 8 + config/manifests/operator/namespace.yaml | 4 + config/manifests/operator/role.yaml | 88 ++++ config/manifests/operator/role_binding.yaml | 15 + .../manifests/operator/service_account.yaml | 8 + internal/controller/const.go | 10 +- internal/controller/kimup_controller.go | 464 +++++------------- internal/controller/resources_admission.go | 246 ++++++++++ internal/controller/resources_common.go | 165 +++++++ internal/controller/resources_controller.go | 191 +++++++ 21 files changed, 963 insertions(+), 440 deletions(-) rename cmd/{webhook => admission-controller}/certificate.go (100%) rename cmd/{webhook => admission-controller}/main.go (95%) rename cmd/{webhook => admission-controller}/webhook-configuration.go (100%) rename cmd/{webhook => admission-controller}/webhook.go (100%) create mode 100644 config/manifests/operator/kustomization.yaml create mode 100644 config/manifests/operator/namespace.yaml create mode 100644 config/manifests/operator/role.yaml create mode 100644 config/manifests/operator/role_binding.yaml create mode 100644 config/manifests/operator/service_account.yaml create mode 100644 internal/controller/resources_admission.go create mode 100644 internal/controller/resources_common.go create mode 100644 internal/controller/resources_controller.go diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 6b6f8da..8624d87 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -1,4 +1,4 @@ -project_name: kube-image-updater +project_name: kimup version: 2 before: hooks: @@ -9,7 +9,7 @@ release: prerelease: auto snapshot: - name_template: "{{ .Tag }}" + version_template: "{{ .Tag }}" checksum: name_template: '{{ .ProjectName }}-{{ .Version }}-checksums.txt' @@ -37,9 +37,9 @@ builds: - arm64 env: - CGO_ENABLED=0 - - id: "kimup-webhook" - binary: kimup-webhook - main: ./cmd/webhook + - id: "kimup-admission-controller" + binary: kimup-admission-controller + main: ./cmd/admission-controller goos: - linux - darwin @@ -128,41 +128,41 @@ dockers: - --label=org.opencontainers.image.created={{ time "2006-01-02T15:04:05Z07:00" }} - --label=org.opencontainers.image.revision={{ .FullCommit }} - # * KIMUP-WEBHOOK + # * KIMUP-ADMISSION-CONTROLLER - goarch: amd64 image_templates: - - "ghcr.io/orange-cloudavenue/{{.ProjectName}}-webhook:{{ .Version }}-amd64" + - "ghcr.io/orange-cloudavenue/{{.ProjectName}}-admission-controller:{{ .Version }}-amd64" dockerfile: Dockerfile use: buildx ids: - - kimup-webhook + - kimup-admission-controller build_flag_templates: - --platform=linux/amd64 - - "--build-arg=BINNAME=kimup-webhook" + - "--build-arg=BINNAME=kimup-admission-controller" - --pull - - --label=org.opencontainers.image.title="kimup-webhook" - - --label=org.opencontainers.image.description="kube-image-updater-webhook" - - --label=org.opencontainers.image.url=https://github.com/orange-cloudavenue/{{ .ProjectName }}-webhook - - --label=org.opencontainers.image.source=https://github.com/orange-cloudavenue/{{ .ProjectName }}-webhook + - --label=org.opencontainers.image.title="kimup-admission-controller" + - --label=org.opencontainers.image.description="kube-image-updater-admission-controller" + - --label=org.opencontainers.image.url=https://github.com/orange-cloudavenue/{{ .ProjectName }}-admission-controller + - --label=org.opencontainers.image.source=https://github.com/orange-cloudavenue/{{ .ProjectName }}-admission-controller - --label=org.opencontainers.image.version={{ .Version }} - --label=org.opencontainers.image.created={{ time "2006-01-02T15:04:05Z07:00" }} - --label=org.opencontainers.image.revision={{ .FullCommit }} - goarch: arm64 image_templates: - - "ghcr.io/orange-cloudavenue/{{ .ProjectName }}-webhook:{{ .Version }}-arm64v8" + - "ghcr.io/orange-cloudavenue/{{ .ProjectName }}-admission-controller:{{ .Version }}-arm64v8" dockerfile: Dockerfile use: buildx ids: - - kimup-webhook + - kimup-admission-controller build_flag_templates: - --platform=linux/arm64/v8 - - "--build-arg=BINNAME=kimup-webhook" + - "--build-arg=BINNAME=kimup-admission-controller" - --pull - - --label=org.opencontainers.image.title="kimup-webhook" - - --label=org.opencontainers.image.description="kube-image-updater-webhook" - - --label=org.opencontainers.image.url=https://github.com/orange-cloudavenue/{{ .ProjectName }}-webhook - - --label=org.opencontainers.image.source=https://github.com/orange-cloudavenue/{{ .ProjectName }}-webhook + - --label=org.opencontainers.image.title="kimup-admission-controller" + - --label=org.opencontainers.image.description="kube-image-updater-admission-controller" + - --label=org.opencontainers.image.url=https://github.com/orange-cloudavenue/{{ .ProjectName }}-admission-controller + - --label=org.opencontainers.image.source=https://github.com/orange-cloudavenue/{{ .ProjectName }}-admission-controller - --label=org.opencontainers.image.version={{ .Version }} - --label=org.opencontainers.image.created={{ time "2006-01-02T15:04:05Z07:00" }} - --label=org.opencontainers.image.revision={{ .FullCommit }} @@ -188,12 +188,12 @@ docker_manifests: - "ghcr.io/orange-cloudavenue/{{ .ProjectName }}-operator:v{{ .Version }}-amd64" - "ghcr.io/orange-cloudavenue/{{ .ProjectName }}-operator:v{{ .Version }}-arm64v8" -# * KIMUP-WEBHOOK -- name_template: "ghcr.io/orange-cloudavenue/{{ .ProjectName }}-webhook:v{{ .Version }}" +# * KIMUP-ADMISSION-CONTROLLER +- name_template: "ghcr.io/orange-cloudavenue/{{ .ProjectName }}-admission-controller:v{{ .Version }}" image_templates: - - "ghcr.io/orange-cloudavenue/{{ .ProjectName }}-webhook:v{{ .Version }}-amd64" - - "ghcr.io/orange-cloudavenue/{{ .ProjectName }}-webhook:v{{ .Version }}-arm64v8" -- name_template: "ghcr.io/orange-cloudavenue/{{ .ProjectName }}-webhook:latest" + - "ghcr.io/orange-cloudavenue/{{ .ProjectName }}-admission-controller:v{{ .Version }}-amd64" + - "ghcr.io/orange-cloudavenue/{{ .ProjectName }}-admission-controller:v{{ .Version }}-arm64v8" +- name_template: "ghcr.io/orange-cloudavenue/{{ .ProjectName }}-admission-controller:latest" image_templates: - - "ghcr.io/orange-cloudavenue/{{ .ProjectName }}-webhook:v{{ .Version }}-amd64" - - "ghcr.io/orange-cloudavenue/{{ .ProjectName }}-webhook:v{{ .Version }}-arm64v8" + - "ghcr.io/orange-cloudavenue/{{ .ProjectName }}-admission-controller:v{{ .Version }}-amd64" + - "ghcr.io/orange-cloudavenue/{{ .ProjectName }}-admission-controller:v{{ .Version }}-arm64v8" diff --git a/Makefile b/Makefile index 1ec8e7f..c90884a 100644 --- a/Makefile +++ b/Makefile @@ -82,11 +82,11 @@ lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes build: manifests generate fmt vet ## Build manager binary. go build -o bin/operator cmd/operator/main.go go build -o bin/kimup cmd/kimup/* - go build -o bin/webhook cmd/webhook/* + go build -o bin/admission-controller cmd/admission-controller/* .PHONY: build -build-webhook: manifests generate fmt vet - go build -o bin/webhook cmd/webhook/* +build-admission-controller: manifests generate fmt vet + go build -o bin/admission-controller cmd/admission-controller/* .PHONY: build-kimup build-kimup: manifests generate fmt vet @@ -100,9 +100,9 @@ run-operator: manifests generate fmt vet ## Run a controller from your host. run-kimup: manifests generate fmt vet ## Run the image updater from your host. go run ./cmd/kimup -.PHONY: run-webhook -run-webhook: manifests generate fmt vet ## Run the webhook from your host. - go run ./cmd/webhook/ +.PHONY: run-admission-controller +run-admission-controller: manifests generate fmt vet ## Run the admission-controller from your host. + go run ./cmd/admission-controller/ .PHONY: run-mkdocs run-mkdocs: ## Run mkdocs to serve the documentation locally. diff --git a/api/v1alpha1/kimup_types.go b/api/v1alpha1/kimup_types.go index d789421..6a4469c 100644 --- a/api/v1alpha1/kimup_types.go +++ b/api/v1alpha1/kimup_types.go @@ -27,11 +27,13 @@ import ( type ( // KimupSpec defines the desired state of Kimup KimupSpec struct { + // TODO add namespace and serviceaccount settings + // +kubebuilder:validation:Optional Controller *KimupControllerSpec `json:"controller"` // +kubebuilder:validation:Optional - Webhook *KimupWebhookSpec `json:"webhook"` + AdmissionController *KimupAdmissionControllerSpec `json:"admissionController"` } // ! Controller @@ -44,9 +46,9 @@ type ( // Service *KimupServiceSpec `json:"service,omitempty"` } - // ! Webhook + // ! AdmissionController - KimupWebhookSpec struct { + KimupAdmissionControllerSpec struct { // +kubebuilder:validation:Optional // +kubebuilder:default:=Deployment // +kubebuilder:validation:Enum=Deployment;DaemonSet @@ -54,7 +56,7 @@ type ( // +kubebuilder:validation:Optional // +kubebuilder:default:=3 - // +kubebuilder:description: Number of replicas for the webhook deployment. (Only for Deployment) + // +kubebuilder:description: Number of replicas (default: 3) for the admissionController deployment. (Only for Deployment) Replicas int32 `json:"replicas,omitempty"` KimupInstanceSpec `json:",inline"` @@ -135,6 +137,7 @@ type ( // +kubebuilder:validation:Optional // +kubebuilder:description: Service account name for the Kimup pods. + // +kubebuilder:default:=kimup ServiceAccountName string `json:"serviceAccountName,omitempty"` // +kubebuilder:validation:Optional @@ -170,7 +173,7 @@ type ( KimupStatus struct { Controller KimupInstanceStatus `json:"controller,omitempty"` - Webhook KimupInstanceStatus `json:"webhook,omitempty"` + AdmissionController KimupInstanceStatus `json:"admissionController,omitempty"` } KimupInstanceStatus struct { @@ -178,7 +181,7 @@ type ( // It can be one of the following: // - "ready": The kimup instance is ready to serve requests // - "resources-created": The Kimup instance resources were created but not yet configured - Phase string `json:"phase,omitempty"` + State string `json:"state,omitempty"` // IsRollingUpdate is true if the kimup instance is being updated IsRollingUpdate bool `json:"isRollingUpdate,omitempty"` @@ -189,8 +192,8 @@ type ( // +kubebuilder:subresource:status // Kimup is the Schema for the kimups API -// +kubebuilder:printcolumn:name="Controller",type=string,JSONPath=`.status.controller.phase` -// +kubebuilder:printcolumn:name="Webhook",type=string,JSONPath=`.status.webhook.phase` +// +kubebuilder:printcolumn:name="Controller",type=string,JSONPath=`.status.controller.state` +// +kubebuilder:printcolumn:name="AdmissionController",type=string,JSONPath=`.status.admissionController.state` type Kimup struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 88ab5a7..dc031eb 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -380,6 +380,23 @@ func (in *Kimup) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KimupAdmissionControllerSpec) DeepCopyInto(out *KimupAdmissionControllerSpec) { + *out = *in + in.KimupInstanceSpec.DeepCopyInto(&out.KimupInstanceSpec) + out.KimupExtraSpec = in.KimupExtraSpec +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KimupAdmissionControllerSpec. +func (in *KimupAdmissionControllerSpec) DeepCopy() *KimupAdmissionControllerSpec { + if in == nil { + return nil + } + out := new(KimupAdmissionControllerSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *KimupControllerSpec) DeepCopyInto(out *KimupControllerSpec) { *out = *in @@ -551,9 +568,9 @@ func (in *KimupSpec) DeepCopyInto(out *KimupSpec) { *out = new(KimupControllerSpec) (*in).DeepCopyInto(*out) } - if in.Webhook != nil { - in, out := &in.Webhook, &out.Webhook - *out = new(KimupWebhookSpec) + if in.AdmissionController != nil { + in, out := &in.AdmissionController, &out.AdmissionController + *out = new(KimupAdmissionControllerSpec) (*in).DeepCopyInto(*out) } } @@ -572,7 +589,7 @@ func (in *KimupSpec) DeepCopy() *KimupSpec { func (in *KimupStatus) DeepCopyInto(out *KimupStatus) { *out = *in out.Controller = in.Controller - out.Webhook = in.Webhook + out.AdmissionController = in.AdmissionController } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KimupStatus. @@ -585,23 +602,6 @@ func (in *KimupStatus) DeepCopy() *KimupStatus { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *KimupWebhookSpec) DeepCopyInto(out *KimupWebhookSpec) { - *out = *in - in.KimupInstanceSpec.DeepCopyInto(&out.KimupInstanceSpec) - out.KimupExtraSpec = in.KimupExtraSpec -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KimupWebhookSpec. -func (in *KimupWebhookSpec) DeepCopy() *KimupWebhookSpec { - if in == nil { - return nil - } - out := new(KimupWebhookSpec) - 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 diff --git a/cmd/webhook/certificate.go b/cmd/admission-controller/certificate.go similarity index 100% rename from cmd/webhook/certificate.go rename to cmd/admission-controller/certificate.go diff --git a/cmd/webhook/main.go b/cmd/admission-controller/main.go similarity index 95% rename from cmd/webhook/main.go rename to cmd/admission-controller/main.go index bec8db8..eacff39 100644 --- a/cmd/webhook/main.go +++ b/cmd/admission-controller/main.go @@ -79,14 +79,10 @@ func main() { signalChan := make(chan os.Signal, 1) signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGKILL) - // homedir for kubeconfig - homedir, err := os.UserHomeDir() + // kubernetes golang library provide flag "kubeconfig" to specify the path to the kubeconfig file + kubeClient, err = client.New(flag.Lookup("kubeconfig").Value.String()) if err != nil { - panic(err) - } - kubeClient, err = client.New(homedir + "/.kube/config") - if err != nil { - panic(err) + log.Panicf("Error creating kubeclient: %v", err) } // * Webhook server diff --git a/cmd/webhook/webhook-configuration.go b/cmd/admission-controller/webhook-configuration.go similarity index 100% rename from cmd/webhook/webhook-configuration.go rename to cmd/admission-controller/webhook-configuration.go diff --git a/cmd/webhook/webhook.go b/cmd/admission-controller/webhook.go similarity index 100% rename from cmd/webhook/webhook.go rename to cmd/admission-controller/webhook.go diff --git a/cmd/kimup/main.go b/cmd/kimup/main.go index 6adf3ce..1ae4270 100644 --- a/cmd/kimup/main.go +++ b/cmd/kimup/main.go @@ -3,6 +3,7 @@ package main import ( "context" "flag" + "net" "os" "os/signal" "syscall" @@ -11,7 +12,9 @@ import ( log "github.com/sirupsen/logrus" "github.com/orange-cloudavenue/kube-image-updater/internal/annotations" + "github.com/orange-cloudavenue/kube-image-updater/internal/httpserver" "github.com/orange-cloudavenue/kube-image-updater/internal/kubeclient" + "github.com/orange-cloudavenue/kube-image-updater/internal/models" "github.com/orange-cloudavenue/kube-image-updater/internal/triggers" "github.com/orange-cloudavenue/kube-image-updater/internal/utils" ) @@ -45,6 +48,23 @@ func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + // * Config the metrics and healthz server + a, waitHTTP := httpserver.Init(ctx, httpserver.WithCustomHandlerForHealth( + func() (bool, error) { + // TODO improve + _, err := net.DialTimeout("tcp", models.HealthzDefaultAddr, 5*time.Second) + if err != nil { + return false, err + } + return true, nil + })) + + if err := a.Run(); err != nil { + log.Errorf("Failed to start HTTP servers: %v", err) + // send signal to stop the program + c <- syscall.SIGINT + } + initScheduler(ctx, k) go func() { @@ -117,4 +137,5 @@ func main() { <-c cancel() + waitHTTP() } diff --git a/config/crd/bases/kimup.cloudavenue.io_kimups.yaml b/config/crd/bases/kimup.cloudavenue.io_kimups.yaml index 086f08b..33cc211 100644 --- a/config/crd/bases/kimup.cloudavenue.io_kimups.yaml +++ b/config/crd/bases/kimup.cloudavenue.io_kimups.yaml @@ -15,11 +15,11 @@ spec: scope: Namespaced versions: - additionalPrinterColumns: - - jsonPath: .status.controller.phase + - jsonPath: .status.controller.state name: Controller type: string - - jsonPath: .status.webhook.phase - name: Webhook + - jsonPath: .status.admissionController.state + name: AdmissionController type: string name: v1alpha1 schema: @@ -46,7 +46,7 @@ spec: spec: description: KimupSpec defines the desired state of Kimup properties: - controller: + admissionController: properties: affinity: description: Affinity is a group of affinity scheduling rules. @@ -979,6 +979,12 @@ spec: additionalProperties: type: string type: object + deploymentType: + default: Deployment + enum: + - Deployment + - DaemonSet + type: string env: items: description: EnvVar represents an environment variable present @@ -1144,6 +1150,10 @@ spec: type: object priorityClassName: type: string + replicas: + default: 3 + format: int32 + type: integer resources: description: ResourceRequirements describes the compute resource requirements. @@ -1205,6 +1215,7 @@ spec: type: object type: object serviceAccountName: + default: kimup type: string tolerations: items: @@ -1420,7 +1431,7 @@ spec: type: object type: array type: object - webhook: + controller: properties: affinity: description: Affinity is a group of affinity scheduling rules. @@ -2353,12 +2364,6 @@ spec: additionalProperties: type: string type: object - deploymentType: - default: Deployment - enum: - - Deployment - - DaemonSet - type: string env: items: description: EnvVar represents an environment variable present @@ -2524,10 +2529,6 @@ spec: type: object priorityClassName: type: string - replicas: - default: 3 - format: int32 - type: integer resources: description: ResourceRequirements describes the compute resource requirements. @@ -2589,6 +2590,7 @@ spec: type: object type: object serviceAccountName: + default: kimup type: string tolerations: items: @@ -2808,13 +2810,13 @@ spec: status: description: KimupStatus defines the observed state of Kimup properties: - controller: + admissionController: properties: isRollingUpdate: description: IsRollingUpdate is true if the kimup instance is being updated type: boolean - phase: + state: description: |- Status of the Kimup Instance It can be one of the following: @@ -2822,13 +2824,13 @@ spec: - "resources-created": The Kimup instance resources were created but not yet configured type: string type: object - webhook: + controller: properties: isRollingUpdate: description: IsRollingUpdate is true if the kimup instance is being updated type: boolean - phase: + state: description: |- Status of the Kimup Instance It can be one of the following: diff --git a/config/manifests/kustomization.yaml b/config/manifests/kustomization.yaml index 2f1ff81..2312663 100644 --- a/config/manifests/kustomization.yaml +++ b/config/manifests/kustomization.yaml @@ -1,8 +1,2 @@ resources: -# For outside of the cluster -# - service.yaml -- mutatingWebhookConfiguration.yaml - deployments.yaml - - - \ No newline at end of file diff --git a/config/manifests/operator/kustomization.yaml b/config/manifests/operator/kustomization.yaml new file mode 100644 index 0000000..5700af7 --- /dev/null +++ b/config/manifests/operator/kustomization.yaml @@ -0,0 +1,8 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - namespace.yaml + - role.yaml + - role_binding.yaml + - service_account.yaml diff --git a/config/manifests/operator/namespace.yaml b/config/manifests/operator/namespace.yaml new file mode 100644 index 0000000..33ebf94 --- /dev/null +++ b/config/manifests/operator/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: kimup-operator diff --git a/config/manifests/operator/role.yaml b/config/manifests/operator/role.yaml new file mode 100644 index 0000000..2c71454 --- /dev/null +++ b/config/manifests/operator/role.yaml @@ -0,0 +1,88 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: kimup-role +rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch + +- apiGroups: + - "" + resources: + - pods + - services + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + +- apiGroups: + - apps + resources: + - daemonsets + - deployments + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + +- apiGroups: + - admissionregistration.k8s.io + resources: + - mutatingwebhookconfigurations + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + +- apiGroups: + - kimup.cloudavenue.io + resources: + - alertconfigs + - images + - kimups + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + +- apiGroups: + - kimup.cloudavenue.io + resources: + - alertconfigs/finalizers + - images/finalizers + - kimups/finalizers + verbs: + - update + +- apiGroups: + - kimup.cloudavenue.io + resources: + - alertconfigs/status + - images/status + - kimups/status + verbs: + - get + - patch + - update diff --git a/config/manifests/operator/role_binding.yaml b/config/manifests/operator/role_binding.yaml new file mode 100644 index 0000000..20d7773 --- /dev/null +++ b/config/manifests/operator/role_binding.yaml @@ -0,0 +1,15 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/name: kube-image-updater + app.kubernetes.io/managed-by: kustomize + name: kimup-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: kimup-role +subjects: +- kind: ServiceAccount + name: kimup + namespace: kimup-operator diff --git a/config/manifests/operator/service_account.yaml b/config/manifests/operator/service_account.yaml new file mode 100644 index 0000000..1db7e41 --- /dev/null +++ b/config/manifests/operator/service_account.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/name: kube-image-updater + app.kubernetes.io/managed-by: kustomize + name: kimup + namespace: kimup-operator diff --git a/internal/controller/const.go b/internal/controller/const.go index ba3e781..248f09e 100644 --- a/internal/controller/const.go +++ b/internal/controller/const.go @@ -15,15 +15,15 @@ const ( KimupControllerName = KimupName + "-" + KimupController KimupControllerImage = BaseKimupImage + KimupControllerName - KimupWebhook = "webhook" - KimupWebhookName = KimupName + "-" + KimupWebhook - KimupWebhookImage = BaseKimupImage + KimupWebhookName + KimupAdmissionController = "admission-controler" + KimupAdmissionControllerName = KimupName + "-" + KimupAdmissionController + KimupAdmissionControllerImage = BaseKimupImage + KimupAdmissionControllerName ) const ( - PhaseResourcesCreated string = "resources-created" + StateResourcesCreated string = "resources-created" - PhaseReady string = "ready" + StateReady string = "ready" ) const ( diff --git a/internal/controller/kimup_controller.go b/internal/controller/kimup_controller.go index 00807f8..fb48533 100644 --- a/internal/controller/kimup_controller.go +++ b/internal/controller/kimup_controller.go @@ -7,9 +7,7 @@ import ( appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" @@ -19,8 +17,6 @@ import ( "github.com/orange-cloudavenue/kube-image-updater/api/v1alpha1" "github.com/orange-cloudavenue/kube-image-updater/internal/kubeclient" - "github.com/orange-cloudavenue/kube-image-updater/internal/models" - "github.com/orange-cloudavenue/kube-image-updater/internal/utils" ) // KimupReconciler reconciles a Image object @@ -45,7 +41,7 @@ type KimupReconciler struct { // // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.0/pkg/reconcile -func (r *KimupReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { +func (r *KimupReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { //nolint:gocyclo log := log.FromContext(ctx) var kim v1alpha1.Kimup @@ -54,70 +50,150 @@ func (r *KimupReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl return ctrl.Result{}, client.IgnoreNotFound(err) } - log.Info("Reconciling kimup objects") + var ( + rescheduleAfter = false + resourcesToCreate = make([]Object, 0) + resourcesToUpdate = make([]Object, 0) + ) + if kim.Spec.Controller != nil { - log.Info("Reconciling kimup controller") + switch { + case kim.Status.Controller.State == "": + resourcesToCreate = append(resourcesToCreate, GetKimupControllerResources(ctx, kim)...) + default: + resourcesToUpdate = append(resourcesToUpdate, GetKimupControllerResources(ctx, kim)...) + } + } + if kim.Spec.AdmissionController != nil { switch { - // if phase is empty, create the resources - case kim.Status.Controller.Phase == "": - log.Info("Creating the resources for the controller") - resources, err := GetKimupControllerResources(ctx, kim) - if err != nil { - log.Error(err, "could not get the resources for the controller") - return ctrl.Result{}, err + case kim.Status.AdmissionController.State == "": + resourcesToCreate = append(resourcesToCreate, GetKimupAdmissionResources(ctx, kim)...) + default: + resourcesToUpdate = append(resourcesToUpdate, GetKimupAdmissionResources(ctx, kim)...) + } + } + + for _, resource := range resourcesToCreate { + if err := r.Create(ctx, resource.obj); err != nil { + if client.IgnoreAlreadyExists(err) == nil { + resourcesToUpdate = append(resourcesToUpdate, resource) + continue + } + log.Error(err, "could not create the resource") + rescheduleAfter = true + continue + } + } + + for _, resource := range resourcesToUpdate { + if err := r.Update(ctx, resource.obj); err != nil { + log.Error(err, "could not update the resource") + rescheduleAfter = true + continue + } + } + + time.Sleep(1 * time.Second) + + // Get all the resources for the kimup object and check if they are ready + // If they are not ready, requeue the request + // If they are ready, update the status of the kimup object + + allResources := []Object{} + allResources = append(allResources, resourcesToCreate...) + allResources = append(allResources, resourcesToUpdate...) + + for _, resource := range allResources { + switch resource.kind { + case "Deployment": + var deployment appsv1.Deployment + if err := r.Get(ctx, client.ObjectKeyFromObject(resource.obj), &deployment); err != nil { + log.Error(err, "could not get the deployment") + rescheduleAfter = true + continue + } + + switch deployment.Name { + case KimupControllerName: + kim.Status.Controller.State = StateResourcesCreated + case KimupAdmissionControllerName: + kim.Status.AdmissionController.State = StateResourcesCreated } - for _, resource := range resources { - if err := r.Create(ctx, resource); err != nil { - log.Error(err, "could not create the resource") - return ctrl.Result{RequeueAfter: 10 * time.Second}, err + if deployment.Status.Replicas != deployment.Status.ReadyReplicas { + log.Info(fmt.Sprintf("The %s deployment in namespace %s is not ready yet", deployment.Name, deployment.Namespace)) + rescheduleAfter = true + continue + } else { + switch deployment.Name { + case KimupControllerName: + kim.Status.Controller.State = StateReady + case KimupAdmissionControllerName: + kim.Status.AdmissionController.State = StateReady } + log.Info(fmt.Sprintf("The %s deployment in namespace %s is ready", deployment.Name, deployment.Namespace)) } - // Update status - kim.Status.Controller.Phase = PhaseResourcesCreated - log.Info("Created resources for the controller") - if err := r.Status().Update(ctx, &kim); err != nil { - log.Error(err, "could not update the status of the kimup controller") - return ctrl.Result{}, err + case "DaemonSet": + var daemonset appsv1.DaemonSet + if err := r.Get(ctx, client.ObjectKeyFromObject(resource.obj), &daemonset); err != nil { + log.Error(err, "could not get the daemonset") + rescheduleAfter = true + continue } - r.Recorder.Event(&kim, corev1.EventTypeNormal, "Resources", "Created resources for the controller") - return ctrl.Result{}, nil - default: - var deployment appsv1.Deployment - if err := r.Get(ctx, client.ObjectKey{Namespace: kim.Namespace, Name: KimupControllerName}, &deployment); err != nil { - log.Error(err, "could not get the deployment for the controller") - return ctrl.Result{}, err + switch daemonset.Name { + case KimupControllerName: + kim.Status.Controller.State = StateResourcesCreated + case KimupAdmissionControllerName: + kim.Status.AdmissionController.State = StateResourcesCreated } - if deployment.Status.Replicas != deployment.Status.ReadyReplicas { - log.Info("The controller is not ready yet") - return ctrl.Result{ - Requeue: true, - }, nil + if daemonset.Status.DesiredNumberScheduled != daemonset.Status.NumberReady { + log.Info("The daemonset is not ready yet") + rescheduleAfter = true + continue + } else { + switch daemonset.Name { + case KimupControllerName: + kim.Status.Controller.State = StateReady + case KimupAdmissionControllerName: + kim.Status.AdmissionController.State = StateReady + } + log.Info(fmt.Sprintf("The %s daemonset in namespace %s is ready", daemonset.Name, daemonset.Namespace)) } - resources, err := GetKimupControllerResources(ctx, kim) - if err != nil { - log.Error(err, "could not get the resources for the controller") - return ctrl.Result{}, err + case "Service": + var service corev1.Service + if err := r.Get(ctx, client.ObjectKeyFromObject(resource.obj), &service); err != nil { + log.Error(err, "could not get the service") + rescheduleAfter = true + continue } - for _, resource := range resources { - if err := r.Update(ctx, resource); err != nil { - log.Error(err, "could not update the resource %s/%s/%s", resource.GetObjectKind(), resource.GetNamespace(), resource.GetName()) - return ctrl.Result{}, err + if service.Spec.Type == corev1.ServiceTypeLoadBalancer { + if len(service.Status.LoadBalancer.Ingress) == 0 { + log.Info("The service is not ready yet") + rescheduleAfter = true + continue } } - log.Info("Updated resources for the controller") - r.Recorder.Event(&kim, corev1.EventTypeNormal, "Resources", "Updated resources for the controller") - return ctrl.Result{}, nil + default: + log.Info(fmt.Sprintf("Unknown resource type: %s", resource.kind)) } } + // Update status + if err := r.Status().Update(ctx, &kim); err != nil { + log.Error(err, "could not update the status of the kimup object") + } + + if rescheduleAfter { + return ctrl.Result{RequeueAfter: 5 * time.Second}, nil + } + return ctrl.Result{}, nil } @@ -131,297 +207,3 @@ func (r *KimupReconciler) SetupWithManager(mgr ctrl.Manager) error { Owns(&corev1.Service{}). Complete(r) } - -func GetKimupControllerResources(ctx context.Context, ki v1alpha1.Kimup) ([]client.Object, error) { - // log := log.FromContext(ctx) - - var resources []client.Object - - var ( - name = KimupControllerName - image = ki.Spec.Controller.Image - ) - - if image == "" { - image = fmt.Sprintf("%s:%s", KimupControllerImage, Version) - } - - // Create a deployment - deployment := appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: ki.Namespace, - // Useful for automatically deleting the resources when the kimup object is deleted - OwnerReferences: []metav1.OwnerReference{ - { - APIVersion: ki.APIVersion, - Kind: ki.Kind, - Name: ki.Name, - UID: ki.UID, - }, - }, - Labels: map[string]string{ - KubernetesAppComponentLabelKey: KimupControllerName, - KubernetesAppInstanceNameLabel: name, - KubernetesAppNameLabelKey: KimupControllerName, - KubernetesAppVersionLabelKey: Version, - KubernetesPartOfLabelKey: KimupControllerName, - KubernetesManagedByLabelKey: KimupOperatorName, - "app": name, - }, - }, - Spec: appsv1.DeploymentSpec{ - Replicas: utils.ToPTR(int32(1)), - Selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "app": name, - KubernetesPartOfLabelKey: KimupControllerName, - KubernetesAppNameLabelKey: KimupControllerName, - }, - }, - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: func() map[string]string { - labels := map[string]string{ - "app": name, - KubernetesPartOfLabelKey: KimupControllerName, - KubernetesAppNameLabelKey: KimupControllerName, - } - for k, v := range ki.Spec.Controller.Labels { - labels[k] = v - } - return labels - }(), - Annotations: ki.Spec.Controller.Annotations, - }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "kimup", - Image: image, - Ports: func() []corev1.ContainerPort { - ports := []corev1.ContainerPort{} - - if ki.Spec.Controller.Metrics.Enabled { - // set the metrics port - metricsPort := ki.Spec.Controller.Metrics.Port - if metricsPort != 0 { - metricsPort = models.MetricsDefaultPort - } - - ports = append(ports, corev1.ContainerPort{ - Name: models.MetricsFlagName, - ContainerPort: metricsPort, - }) - } - - if ki.Spec.Controller.Healthz.Enabled { - // set the healthz port - healthzPort := ki.Spec.Controller.Healthz.Port - if healthzPort != 0 { - healthzPort = models.HealthzDefaultPort - } - - ports = append(ports, corev1.ContainerPort{ - Name: models.HealthzFlagName, - ContainerPort: healthzPort, - }) - } - - return ports - }(), - Args: func() []string { - a := []string{} - if ki.Spec.Controller.Healthz.Enabled { - // enable healthz - a = append(a, fmt.Sprintf("--%s", models.HealthzFlagName)) - - // set the healthz port - healthzPort := ki.Spec.Controller.Healthz.Port - if healthzPort != 0 { - healthzPort = models.HealthzDefaultPort - } - a = append(a, fmt.Sprintf("--%s=%d", models.HealthzPortFlagName, healthzPort)) - - // set the healthz path - healthzPath := ki.Spec.Controller.Healthz.Path - if healthzPath == "" { - healthzPath = models.HealthzDefaultPath - } - a = append(a, fmt.Sprintf("--%s=%s", models.HealthzPathFlagName, healthzPath)) - } - - if ki.Spec.Controller.Metrics.Enabled { - // enable metrics - a = append(a, fmt.Sprintf("--%s", models.MetricsFlagName)) - - // set the metrics port - metricsPort := ki.Spec.Controller.Metrics.Port - if metricsPort != 0 { - metricsPort = models.MetricsDefaultPort - } - - a = append(a, fmt.Sprintf("--%s=%d", models.MetricsPortFlagName, metricsPort)) - - // set the metrics path - metricsPath := ki.Spec.Controller.Metrics.Path - if metricsPath == "" { - metricsPath = models.MetricsDefaultPath - } - - a = append(a, fmt.Sprintf("--%s=%s", models.MetricsPathFlagName, metricsPath)) - } - - a = append(a, fmt.Sprintf("--%s=%s", models.LogLevelFlagName, ki.Spec.Controller.LogLevel)) - - return a - }(), - ReadinessProbe: func() *corev1.Probe { - if !ki.Spec.Controller.Healthz.Enabled { - return nil - } - healthzPath := ki.Spec.Controller.Healthz.Path - if healthzPath == "" { - healthzPath = models.HealthzDefaultPath - } - - healthzPort := ki.Spec.Controller.Healthz.Port - if healthzPort != 0 { - healthzPort = models.HealthzDefaultPort - } - - return &corev1.Probe{ - ProbeHandler: corev1.ProbeHandler{ - HTTPGet: &corev1.HTTPGetAction{ - Path: healthzPath, - Port: intstr.FromInt32(healthzPort), - }, - }, - FailureThreshold: 3, - InitialDelaySeconds: 10, - PeriodSeconds: 10, - SuccessThreshold: 1, - TimeoutSeconds: 2, - } - }(), - LivenessProbe: func() *corev1.Probe { - if !ki.Spec.Controller.Healthz.Enabled { - return nil - } - - healthzPath := ki.Spec.Controller.Healthz.Path - if healthzPath == "" { - healthzPath = models.HealthzDefaultPath - } - - healthzPort := ki.Spec.Controller.Healthz.Port - if healthzPort != 0 { - healthzPort = models.HealthzDefaultPort - } - - return &corev1.Probe{ - ProbeHandler: corev1.ProbeHandler{ - HTTPGet: &corev1.HTTPGetAction{ - Path: healthzPath, - Port: intstr.FromInt32(healthzPort), - }, - }, - FailureThreshold: 3, - InitialDelaySeconds: 10, - PeriodSeconds: 10, - SuccessThreshold: 1, - TimeoutSeconds: 2, - } - }(), - ImagePullPolicy: corev1.PullIfNotPresent, - Resources: func() corev1.ResourceRequirements { - if ki.Spec.Controller.Resources == nil { - return corev1.ResourceRequirements{} - } - return *ki.Spec.Controller.Resources - }(), - }, - }, - Affinity: ki.Spec.Controller.Affinity, - NodeSelector: ki.Spec.Controller.NodeSelector, - Tolerations: ki.Spec.Controller.Tolerations, - TopologySpreadConstraints: ki.Spec.Controller.TopologySpreadConstraints, - ServiceAccountName: ki.Spec.Controller.ServiceAccountName, - PriorityClassName: ki.Spec.Controller.PriorityClassName, - }, - }, - }, - } - - resources = append(resources, &deployment) - - // TODO Check if healthz or metrics are enabled - - service := corev1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Name: fmt.Sprintf("%s-healthz-metrics", name), - Namespace: ki.Namespace, - // Useful for automatically deleting the resources when the kimup object is deleted - OwnerReferences: []metav1.OwnerReference{ - { - APIVersion: ki.APIVersion, - Kind: ki.Kind, - Name: ki.Name, - UID: ki.UID, - }, - }, - Labels: map[string]string{ - KubernetesAppComponentLabelKey: KimupControllerName, - KubernetesAppInstanceNameLabel: name, - KubernetesAppNameLabelKey: KimupControllerName, - KubernetesAppVersionLabelKey: Version, - KubernetesPartOfLabelKey: KimupControllerName, - KubernetesManagedByLabelKey: KimupOperatorName, - "app": name, - }, - }, - Spec: corev1.ServiceSpec{ - Selector: map[string]string{ - "app": name, - KubernetesAppNameLabelKey: KimupControllerName, - }, - Ports: func() []corev1.ServicePort { - svcs := []corev1.ServicePort{} - - if ki.Spec.Controller.Metrics.Enabled { - // set the metrics port - metricsPort := ki.Spec.Controller.Metrics.Port - if metricsPort == 0 { - metricsPort = models.MetricsDefaultPort - } - - svcs = append(svcs, corev1.ServicePort{ - Name: models.MetricsFlagName, - Port: metricsPort, - TargetPort: intstr.FromString(models.MetricsFlagName), - }) - } - - if ki.Spec.Controller.Healthz.Enabled { - // set the healthz port - healthzPort := ki.Spec.Controller.Healthz.Port - if healthzPort == 0 { - healthzPort = models.HealthzDefaultPort - } - - svcs = append(svcs, corev1.ServicePort{ - Name: models.HealthzFlagName, - Port: healthzPort, - TargetPort: intstr.FromString(models.HealthzFlagName), - }) - } - - return svcs - }(), - }, - } - - resources = append(resources, &service) - - return resources, nil -} diff --git a/internal/controller/resources_admission.go b/internal/controller/resources_admission.go new file mode 100644 index 0000000..3a9a5ff --- /dev/null +++ b/internal/controller/resources_admission.go @@ -0,0 +1,246 @@ +package controller + +import ( + "context" + "fmt" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/orange-cloudavenue/kube-image-updater/api/v1alpha1" +) + +func GetKimupAdmissionResources(ctx context.Context, ki v1alpha1.Kimup) []Object { + // log := log.FromContext(ctx) + + var resources []Object + + var ( + name = KimupAdmissionControllerName + image = ki.Spec.AdmissionController.Image + ) + + if image == "" { + image = fmt.Sprintf("%s:%s", KimupAdmissionControllerImage, Version) + } + + switch ki.Spec.AdmissionController.DeploymentType { + case "Deployment": + // Create a deployment + deployment := appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "apps/v1", + Kind: "Deployment", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: ki.Namespace, + // Useful for automatically deleting the resources when the kimup object is deleted + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: ki.APIVersion, + Kind: ki.Kind, + Name: ki.Name, + UID: ki.UID, + }, + }, + Labels: map[string]string{ + KubernetesAppComponentLabelKey: name, + KubernetesAppInstanceNameLabel: name, + KubernetesAppNameLabelKey: name, + KubernetesAppVersionLabelKey: Version, + KubernetesPartOfLabelKey: name, + KubernetesManagedByLabelKey: KimupOperatorName, + "app": name, + }, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: &ki.Spec.AdmissionController.Replicas, + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": name, + KubernetesPartOfLabelKey: name, + KubernetesAppNameLabelKey: name, + }, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: func() map[string]string { + labels := map[string]string{ + "app": name, + KubernetesPartOfLabelKey: name, + KubernetesAppNameLabelKey: name, + } + for k, v := range ki.Spec.AdmissionController.Labels { + labels[k] = v + } + return labels + }(), + Annotations: ki.Spec.AdmissionController.Annotations, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "kimup-admission-controller", + Image: image, + Ports: func() []corev1.ContainerPort { + return buildContainerPorts(ki.Spec.AdmissionController.KimupExtraSpec) + }(), + Args: func() []string { + return buildKimupArgs(ki.Spec.AdmissionController.KimupExtraSpec) + }(), + ReadinessProbe: buildReadinessProbe(ki.Spec.AdmissionController.KimupExtraSpec), + LivenessProbe: buildLivenessProbe(ki.Spec.AdmissionController.KimupExtraSpec), + ImagePullPolicy: corev1.PullIfNotPresent, + Resources: func() corev1.ResourceRequirements { + if ki.Spec.AdmissionController.Resources == nil { + return corev1.ResourceRequirements{} + } + return *ki.Spec.AdmissionController.Resources + }(), + }, + }, + Affinity: ki.Spec.AdmissionController.Affinity, + NodeSelector: ki.Spec.AdmissionController.NodeSelector, + Tolerations: ki.Spec.AdmissionController.Tolerations, + TopologySpreadConstraints: ki.Spec.AdmissionController.TopologySpreadConstraints, + ServiceAccountName: ki.Spec.AdmissionController.ServiceAccountName, + PriorityClassName: ki.Spec.AdmissionController.PriorityClassName, + }, + }, + }, + } + + resources = append(resources, Object{kind: deployment.TypeMeta.Kind, obj: &deployment}) + case "DaemonSet": + // Create a daemonset + + daemonset := appsv1.DaemonSet{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "apps/v1", + Kind: "DaemonSet", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: ki.Namespace, + // Useful for automatically deleting the resources when the kimup object is deleted + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: ki.APIVersion, + Kind: ki.Kind, + Name: ki.Name, + UID: ki.UID, + }, + }, + Labels: map[string]string{ + KubernetesAppComponentLabelKey: name, + KubernetesAppInstanceNameLabel: name, + KubernetesAppNameLabelKey: name, + KubernetesAppVersionLabelKey: Version, + KubernetesPartOfLabelKey: name, + KubernetesManagedByLabelKey: KimupOperatorName, + "app": name, + }, + }, + Spec: appsv1.DaemonSetSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": name, + KubernetesPartOfLabelKey: name, + KubernetesAppNameLabelKey: name, + }, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: func() map[string]string { + labels := map[string]string{ + "app": name, + KubernetesPartOfLabelKey: name, + KubernetesAppNameLabelKey: name, + } + for k, v := range ki.Spec.AdmissionController.Labels { + labels[k] = v + } + return labels + }(), + Annotations: ki.Spec.AdmissionController.Annotations, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "kimup", + Image: image, + Ports: func() []corev1.ContainerPort { + return buildContainerPorts(ki.Spec.AdmissionController.KimupExtraSpec) + }(), + Args: func() []string { + return buildKimupArgs(ki.Spec.AdmissionController.KimupExtraSpec) + }(), + ReadinessProbe: buildReadinessProbe(ki.Spec.AdmissionController.KimupExtraSpec), + LivenessProbe: buildLivenessProbe(ki.Spec.AdmissionController.KimupExtraSpec), + ImagePullPolicy: corev1.PullIfNotPresent, + Resources: func() corev1.ResourceRequirements { + if ki.Spec.AdmissionController.Resources == nil { + return corev1.ResourceRequirements{} + } + return *ki.Spec.AdmissionController.Resources + }(), + }, + }, + Affinity: ki.Spec.AdmissionController.Affinity, + NodeSelector: ki.Spec.AdmissionController.NodeSelector, + Tolerations: ki.Spec.AdmissionController.Tolerations, + TopologySpreadConstraints: ki.Spec.AdmissionController.TopologySpreadConstraints, + ServiceAccountName: ki.Spec.AdmissionController.ServiceAccountName, + PriorityClassName: ki.Spec.AdmissionController.PriorityClassName, + }, + }, + }, + } + resources = append(resources, Object{kind: daemonset.TypeMeta.Kind, obj: &daemonset}) + } // end switch + + if ki.Spec.Controller.Healthz.Enabled || ki.Spec.Controller.Metrics.Enabled { + service := corev1.Service{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Service", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-healthz-metrics", name), + Namespace: ki.Namespace, + // Useful for automatically deleting the resources when the kimup object is deleted + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: ki.APIVersion, + Kind: ki.Kind, + Name: ki.Name, + UID: ki.UID, + }, + }, + Labels: map[string]string{ + KubernetesAppComponentLabelKey: KimupControllerName, + KubernetesAppInstanceNameLabel: name, + KubernetesAppNameLabelKey: KimupControllerName, + KubernetesAppVersionLabelKey: Version, + KubernetesPartOfLabelKey: KimupControllerName, + KubernetesManagedByLabelKey: KimupOperatorName, + "app": name, + }, + }, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{ + "app": name, + KubernetesAppNameLabelKey: KimupControllerName, + }, + Ports: func() []corev1.ServicePort { + return buildServicePorts(ki.Spec.AdmissionController.KimupExtraSpec) + }(), + }, + } + + resources = append(resources, Object{kind: service.TypeMeta.Kind, obj: &service}) + } + return resources +} diff --git a/internal/controller/resources_common.go b/internal/controller/resources_common.go new file mode 100644 index 0000000..7fd08c2 --- /dev/null +++ b/internal/controller/resources_common.go @@ -0,0 +1,165 @@ +package controller + +import ( + "fmt" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/intstr" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/orange-cloudavenue/kube-image-updater/api/v1alpha1" + "github.com/orange-cloudavenue/kube-image-updater/internal/models" +) + +type Object struct { + kind string + obj client.Object +} + +func buildKimupArgs(extra v1alpha1.KimupExtraSpec) (args []string) { + args = []string{} + if extra.Healthz.Enabled { + // enable healthz + // TODO + // args = append(args, fmt.Sprintf("--%s", models.HealthzFlagName)) + + // set the healthz port + healthzPort := extra.Healthz.Port + if healthzPort != 0 { + healthzPort = models.HealthzDefaultPort + } + args = append(args, fmt.Sprintf("--%s=%d", models.HealthzPortFlagName, healthzPort)) + + // set the healthz path + healthzPath := extra.Healthz.Path + if healthzPath == "" { + healthzPath = models.HealthzDefaultPath + } + args = append(args, fmt.Sprintf("--%s=%s", models.HealthzPathFlagName, healthzPath)) + } + + if extra.Metrics.Enabled { + // enable metrics + // args = append(args, fmt.Sprintf("--%s", models.MetricsFlagName)) + + // set the metrics port + metricsPort := extra.Metrics.Port + if metricsPort != 0 { + metricsPort = models.MetricsDefaultPort + } + + args = append(args, fmt.Sprintf("--%s=%d", models.MetricsPortFlagName, metricsPort)) + + // set the metrics path + metricsPath := extra.Metrics.Path + if metricsPath == "" { + metricsPath = models.MetricsDefaultPath + } + + args = append(args, fmt.Sprintf("--%s=%s", models.MetricsPathFlagName, metricsPath)) + } + + // TODO + // args = append(args, fmt.Sprintf("--%s=%s", models.LogLevelFlagName, extra.LogLevel)) + + return args +} + +func buildReadinessProbe(extra v1alpha1.KimupExtraSpec) *corev1.Probe { + if !extra.Healthz.Enabled { + return nil + } + healthzPath := extra.Healthz.Path + if healthzPath == "" { + healthzPath = models.HealthzDefaultPath + } + + healthzPort := extra.Healthz.Port + if healthzPort == 0 { + healthzPort = models.HealthzDefaultPort + } + + return &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: healthzPath, + Port: intstr.FromInt32(healthzPort), + }, + }, + FailureThreshold: 3, + InitialDelaySeconds: 10, + PeriodSeconds: 10, + SuccessThreshold: 1, + TimeoutSeconds: 2, + } +} + +func buildLivenessProbe(extra v1alpha1.KimupExtraSpec) *corev1.Probe { + return buildReadinessProbe(extra) +} + +func buildContainerPorts(extra v1alpha1.KimupExtraSpec) (ports []corev1.ContainerPort) { + ports = []corev1.ContainerPort{} + + if extra.Metrics.Enabled { + // set the metrics port + metricsPort := extra.Metrics.Port + if metricsPort == 0 { + metricsPort = models.MetricsDefaultPort + } + + ports = append(ports, corev1.ContainerPort{ + Name: models.MetricsFlagName, + ContainerPort: metricsPort, + }) + } + + if extra.Healthz.Enabled { + // set the healthz port + healthzPort := extra.Healthz.Port + if healthzPort == 0 { + healthzPort = models.HealthzDefaultPort + } + + ports = append(ports, corev1.ContainerPort{ + Name: models.HealthzFlagName, + ContainerPort: healthzPort, + }) + } + + return ports +} + +func buildServicePorts(extra v1alpha1.KimupExtraSpec) (ports []corev1.ServicePort) { + ports = []corev1.ServicePort{} + + if extra.Metrics.Enabled { + // set the metrics port + metricsPort := extra.Metrics.Port + if metricsPort == 0 { + metricsPort = models.MetricsDefaultPort + } + + ports = append(ports, corev1.ServicePort{ + Name: models.MetricsFlagName, + Port: metricsPort, + TargetPort: intstr.FromString(models.MetricsFlagName), + }) + } + + if extra.Healthz.Enabled { + // set the healthz port + healthzPort := extra.Healthz.Port + if healthzPort == 0 { + healthzPort = models.HealthzDefaultPort + } + + ports = append(ports, corev1.ServicePort{ + Name: models.HealthzFlagName, + Port: healthzPort, + TargetPort: intstr.FromString(models.HealthzFlagName), + }) + } + + return ports +} diff --git a/internal/controller/resources_controller.go b/internal/controller/resources_controller.go new file mode 100644 index 0000000..d94da6b --- /dev/null +++ b/internal/controller/resources_controller.go @@ -0,0 +1,191 @@ +package controller + +import ( + "context" + "fmt" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + + "github.com/orange-cloudavenue/kube-image-updater/api/v1alpha1" + "github.com/orange-cloudavenue/kube-image-updater/internal/models" + "github.com/orange-cloudavenue/kube-image-updater/internal/utils" +) + +func GetKimupControllerResources(ctx context.Context, ki v1alpha1.Kimup) []Object { + // log := log.FromContext(ctx) + + var resources []Object + + var ( + name = KimupControllerName + image = ki.Spec.Controller.Image + ) + + if image == "" { + image = fmt.Sprintf("%s:%s", KimupControllerImage, Version) + } + + // Create a deployment + deployment := appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "apps/v1", + Kind: "Deployment", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: ki.Namespace, + // Useful for automatically deleting the resources when the kimup object is deleted + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: ki.APIVersion, + Kind: ki.Kind, + Name: ki.Name, + UID: ki.UID, + }, + }, + Labels: map[string]string{ + KubernetesAppComponentLabelKey: KimupControllerName, + KubernetesAppInstanceNameLabel: name, + KubernetesAppNameLabelKey: KimupControllerName, + KubernetesAppVersionLabelKey: Version, + KubernetesPartOfLabelKey: KimupControllerName, + KubernetesManagedByLabelKey: KimupOperatorName, + "app": name, + }, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: utils.ToPTR(int32(1)), + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": name, + KubernetesPartOfLabelKey: KimupControllerName, + KubernetesAppNameLabelKey: KimupControllerName, + }, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: func() map[string]string { + labels := map[string]string{ + "app": name, + KubernetesPartOfLabelKey: KimupControllerName, + KubernetesAppNameLabelKey: KimupControllerName, + } + for k, v := range ki.Spec.Controller.Labels { + labels[k] = v + } + return labels + }(), + Annotations: ki.Spec.Controller.Annotations, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "kimup", + Image: image, + Ports: func() []corev1.ContainerPort { + return buildContainerPorts(ki.Spec.Controller.KimupExtraSpec) + }(), + Args: func() []string { + return buildKimupArgs(ki.Spec.Controller.KimupExtraSpec) + }(), + ReadinessProbe: buildReadinessProbe(ki.Spec.Controller.KimupExtraSpec), + LivenessProbe: buildLivenessProbe(ki.Spec.Controller.KimupExtraSpec), + ImagePullPolicy: corev1.PullIfNotPresent, + Resources: func() corev1.ResourceRequirements { + if ki.Spec.Controller.Resources == nil { + return corev1.ResourceRequirements{} + } + return *ki.Spec.Controller.Resources + }(), + }, + }, + Affinity: ki.Spec.Controller.Affinity, + NodeSelector: ki.Spec.Controller.NodeSelector, + Tolerations: ki.Spec.Controller.Tolerations, + TopologySpreadConstraints: ki.Spec.Controller.TopologySpreadConstraints, + ServiceAccountName: ki.Spec.Controller.ServiceAccountName, + PriorityClassName: ki.Spec.Controller.PriorityClassName, + }, + }, + }, + } + + resources = append(resources, Object{kind: deployment.TypeMeta.Kind, obj: &deployment}) + + if ki.Spec.Controller.Healthz.Enabled || ki.Spec.Controller.Metrics.Enabled { + service := corev1.Service{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Service", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-healthz-metrics", name), + Namespace: ki.Namespace, + // Useful for automatically deleting the resources when the kimup object is deleted + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: ki.APIVersion, + Kind: ki.Kind, + Name: ki.Name, + UID: ki.UID, + }, + }, + Labels: map[string]string{ + KubernetesAppComponentLabelKey: KimupControllerName, + KubernetesAppInstanceNameLabel: name, + KubernetesAppNameLabelKey: KimupControllerName, + KubernetesAppVersionLabelKey: Version, + KubernetesPartOfLabelKey: KimupControllerName, + KubernetesManagedByLabelKey: KimupOperatorName, + "app": name, + }, + }, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{ + "app": name, + KubernetesAppNameLabelKey: KimupControllerName, + }, + Ports: func() []corev1.ServicePort { + svcs := []corev1.ServicePort{} + + if ki.Spec.Controller.Metrics.Enabled { + // set the metrics port + metricsPort := ki.Spec.Controller.Metrics.Port + if metricsPort == 0 { + metricsPort = models.MetricsDefaultPort + } + + svcs = append(svcs, corev1.ServicePort{ + Name: models.MetricsFlagName, + Port: metricsPort, + TargetPort: intstr.FromString(models.MetricsFlagName), + }) + } + + if ki.Spec.Controller.Healthz.Enabled { + // set the healthz port + healthzPort := ki.Spec.Controller.Healthz.Port + if healthzPort == 0 { + healthzPort = models.HealthzDefaultPort + } + + svcs = append(svcs, corev1.ServicePort{ + Name: models.HealthzFlagName, + Port: healthzPort, + TargetPort: intstr.FromString(models.HealthzFlagName), + }) + } + + return svcs + }(), + }, + } + + resources = append(resources, Object{kind: service.TypeMeta.Kind, obj: &service}) + } + + return resources +}