From e9966c49e884a3cace7f9addd14e30235b5b6c43 Mon Sep 17 00:00:00 2001 From: dzsak Date: Wed, 27 Mar 2024 11:45:08 +0100 Subject: [PATCH] Suspend command --- pkg/api/api.go | 13 ++++ pkg/api/router.go | 1 + pkg/flux/helmrelease.go | 24 ++++++ pkg/flux/kustomization.go | 24 ++++++ pkg/flux/reconcile.go | 3 +- pkg/flux/source.go | 73 ++++++++++++++++++ pkg/flux/suspend.go | 154 ++++++++++++++++++++++++++++++++++++++ web/src/HelmRelease.jsx | 7 +- web/src/Kustomization.jsx | 7 +- web/src/Source.jsx | 7 +- web/src/client.js | 2 + 11 files changed, 310 insertions(+), 5 deletions(-) create mode 100644 pkg/flux/suspend.go diff --git a/pkg/api/api.go b/pkg/api/api.go index 6c9e710..aafe1a1 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -203,6 +203,19 @@ func stopLogs(w http.ResponseWriter, r *http.Request) { w.Write([]byte("{}")) } +func suspend(w http.ResponseWriter, r *http.Request) { + resource := r.URL.Query().Get("resource") + namespace := r.URL.Query().Get("namespace") + name := r.URL.Query().Get("name") + config, _ := r.Context().Value("config").(*rest.Config) + + reconcileCommand := flux.NewSuspendCommand(resource) + go reconcileCommand.Run(config, namespace, name) + + w.WriteHeader(http.StatusOK) + w.Write([]byte("{}")) +} + func reconcile(w http.ResponseWriter, r *http.Request) { resource := r.URL.Query().Get("resource") namespace := r.URL.Query().Get("namespace") diff --git a/pkg/api/router.go b/pkg/api/router.go index ca6731b..ed549b2 100644 --- a/pkg/api/router.go +++ b/pkg/api/router.go @@ -39,6 +39,7 @@ func SetupRouter( r.Get("/api/describePod", describePod) r.Get("/api/logs", streamLogs) r.Get("/api/stopLogs", stopLogs) + r.Post("/api/suspend", suspend) r.Post("/api/reconcile", reconcile) r.Get("/ws/", func(w http.ResponseWriter, r *http.Request) { streaming.ServeWs(clientHub, w, r) diff --git a/pkg/flux/helmrelease.go b/pkg/flux/helmrelease.go index 4ca4c80..610d06c 100644 --- a/pkg/flux/helmrelease.go +++ b/pkg/flux/helmrelease.go @@ -32,10 +32,18 @@ func (h helmReleaseAdapter) asClientObject() client.Object { return h.HelmRelease } +func (h helmReleaseAdapter) deepCopyClientObject() client.Object { + return h.HelmRelease.DeepCopy() +} + func (obj helmReleaseAdapter) isSuspended() bool { return obj.HelmRelease.Spec.Suspend } +func (obj helmReleaseAdapter) setSuspended() { + obj.HelmRelease.Spec.Suspend = true +} + func (obj helmReleaseAdapter) lastHandledReconcileRequest() string { return obj.Status.GetLastHandledReconcileRequest() } @@ -43,3 +51,19 @@ func (obj helmReleaseAdapter) lastHandledReconcileRequest() string { func (obj helmReleaseAdapter) successMessage() string { return fmt.Sprintf("applied revision %s", obj.Status.LastAppliedRevision) } + +type helmReleaseListAdapter struct { + *helmv2beta1.HelmReleaseList +} + +func (h helmReleaseListAdapter) asClientList() client.ObjectList { + return h.HelmReleaseList +} + +func (h helmReleaseListAdapter) len() int { + return len(h.HelmReleaseList.Items) +} + +func (a helmReleaseListAdapter) item(i int) suspendable { + return &helmReleaseAdapter{&a.HelmReleaseList.Items[i]} +} diff --git a/pkg/flux/kustomization.go b/pkg/flux/kustomization.go index cfceaf6..681409b 100644 --- a/pkg/flux/kustomization.go +++ b/pkg/flux/kustomization.go @@ -32,10 +32,18 @@ func (a kustomizationAdapter) asClientObject() client.Object { return a.Kustomization } +func (a kustomizationAdapter) deepCopyClientObject() client.Object { + return a.Kustomization.DeepCopy() +} + func (obj kustomizationAdapter) isSuspended() bool { return obj.Kustomization.Spec.Suspend } +func (obj kustomizationAdapter) setSuspended() { + obj.Kustomization.Spec.Suspend = true +} + func (obj kustomizationAdapter) lastHandledReconcileRequest() string { return obj.Status.GetLastHandledReconcileRequest() } @@ -43,3 +51,19 @@ func (obj kustomizationAdapter) lastHandledReconcileRequest() string { func (obj kustomizationAdapter) successMessage() string { return fmt.Sprintf("applied revision %s", obj.Status.LastAppliedRevision) } + +type kustomizationListAdapter struct { + *kustomizationv1.KustomizationList +} + +func (a kustomizationListAdapter) asClientList() client.ObjectList { + return a.KustomizationList +} + +func (a kustomizationListAdapter) len() int { + return len(a.KustomizationList.Items) +} + +func (a kustomizationListAdapter) item(i int) suspendable { + return &kustomizationAdapter{&a.KustomizationList.Items[i]} +} diff --git a/pkg/flux/reconcile.go b/pkg/flux/reconcile.go index 96b587d..2933d88 100644 --- a/pkg/flux/reconcile.go +++ b/pkg/flux/reconcile.go @@ -31,7 +31,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" - apiruntime "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" @@ -92,7 +91,7 @@ func NewReconcileCommand(resource string) *reconcileCommand { } func (r *reconcileCommand) Run(config *rest.Config, namespace, name string) { - scheme := apiruntime.NewScheme() + scheme := runtime.NewScheme() sourcev1.AddToScheme(scheme) sourcev1beta2.AddToScheme(scheme) kustomizationv1.AddToScheme(scheme) diff --git a/pkg/flux/source.go b/pkg/flux/source.go index 991db6e..db9d343 100644 --- a/pkg/flux/source.go +++ b/pkg/flux/source.go @@ -21,6 +21,7 @@ import ( "fmt" sourcev1 "github.com/fluxcd/source-controller/api/v1" + sourcev1b2 "github.com/fluxcd/source-controller/api/v1beta2" sourcev1beta2 "github.com/fluxcd/source-controller/api/v1beta2" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -33,10 +34,18 @@ func (a gitRepositoryAdapter) asClientObject() client.Object { return a.GitRepository } +func (a gitRepositoryAdapter) deepCopyClientObject() client.Object { + return a.GitRepository.DeepCopy() +} + func (obj gitRepositoryAdapter) isSuspended() bool { return obj.GitRepository.Spec.Suspend } +func (obj gitRepositoryAdapter) setSuspended() { + obj.GitRepository.Spec.Suspend = true +} + func (obj gitRepositoryAdapter) lastHandledReconcileRequest() string { return obj.Status.GetLastHandledReconcileRequest() } @@ -45,6 +54,22 @@ func (obj gitRepositoryAdapter) successMessage() string { return fmt.Sprintf("fetched revision %s", obj.Status.Artifact.Revision) } +type gitRepositoryListAdapter struct { + *sourcev1.GitRepositoryList +} + +func (a gitRepositoryListAdapter) asClientList() client.ObjectList { + return a.GitRepositoryList +} + +func (a gitRepositoryListAdapter) len() int { + return len(a.GitRepositoryList.Items) +} + +func (a gitRepositoryListAdapter) item(i int) suspendable { + return &gitRepositoryAdapter{&a.GitRepositoryList.Items[i]} +} + type ociRepositoryAdapter struct { *sourcev1beta2.OCIRepository } @@ -53,10 +78,18 @@ func (a ociRepositoryAdapter) asClientObject() client.Object { return a.OCIRepository } +func (a ociRepositoryAdapter) deepCopyClientObject() client.Object { + return a.OCIRepository.DeepCopy() +} + func (obj ociRepositoryAdapter) isSuspended() bool { return obj.OCIRepository.Spec.Suspend } +func (obj ociRepositoryAdapter) setSuspended() { + obj.OCIRepository.Spec.Suspend = true +} + func (obj ociRepositoryAdapter) lastHandledReconcileRequest() string { return obj.Status.GetLastHandledReconcileRequest() } @@ -65,6 +98,22 @@ func (obj ociRepositoryAdapter) successMessage() string { return fmt.Sprintf("fetched revision %s", obj.Status.Artifact.Revision) } +type ociRepositoryListAdapter struct { + *sourcev1b2.OCIRepositoryList +} + +func (a ociRepositoryListAdapter) asClientList() client.ObjectList { + return a.OCIRepositoryList +} + +func (a ociRepositoryListAdapter) len() int { + return len(a.OCIRepositoryList.Items) +} + +func (a ociRepositoryListAdapter) item(i int) suspendable { + return &ociRepositoryAdapter{&a.OCIRepositoryList.Items[i]} +} + type bucketAdapter struct { *sourcev1beta2.Bucket } @@ -73,10 +122,18 @@ func (a bucketAdapter) asClientObject() client.Object { return a.Bucket } +func (a bucketAdapter) deepCopyClientObject() client.Object { + return a.Bucket.DeepCopy() +} + func (obj bucketAdapter) isSuspended() bool { return obj.Bucket.Spec.Suspend } +func (obj bucketAdapter) setSuspended() { + obj.Bucket.Spec.Suspend = true +} + func (obj bucketAdapter) lastHandledReconcileRequest() string { return obj.Status.GetLastHandledReconcileRequest() } @@ -84,3 +141,19 @@ func (obj bucketAdapter) lastHandledReconcileRequest() string { func (obj bucketAdapter) successMessage() string { return fmt.Sprintf("fetched revision %s", obj.Status.Artifact.Revision) } + +type bucketListAdapter struct { + *sourcev1b2.BucketList +} + +func (a bucketListAdapter) asClientList() client.ObjectList { + return a.BucketList +} + +func (a bucketListAdapter) len() int { + return len(a.BucketList.Items) +} + +func (a bucketListAdapter) item(i int) suspendable { + return &bucketAdapter{&a.BucketList.Items[i]} +} diff --git a/pkg/flux/suspend.go b/pkg/flux/suspend.go new file mode 100644 index 0000000..e3db6ba --- /dev/null +++ b/pkg/flux/suspend.go @@ -0,0 +1,154 @@ +/* +Copyright 2020 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +Original version: https://github.com/fluxcd/flux2/blob/437a94367784541695fa68deba7a52b188d97ea8/cmd/flux/suspend.go +*/ + +package flux + +import ( + "context" + "errors" + + helmv2beta1 "github.com/fluxcd/helm-controller/api/v2beta1" + kustomizationv1 "github.com/fluxcd/kustomize-controller/api/v1" + sourcev1 "github.com/fluxcd/source-controller/api/v1" + sourcev1beta2 "github.com/fluxcd/source-controller/api/v1beta2" + "github.com/sirupsen/logrus" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type suspendCommand struct { + kind string + groupVersion schema.GroupVersion + list listSuspendable + object suspendable +} + +type listSuspendable interface { + asClientList() client.ObjectList + len() int + item(i int) suspendable +} + +type suspendable interface { + asClientObject() client.Object + deepCopyClientObject() client.Object + isSuspended() bool + setSuspended() +} + +func NewSuspendCommand(resource string) *suspendCommand { + switch resource { + case "kustomization": + return &suspendCommand{ + kind: kustomizationv1.KustomizationKind, + groupVersion: kustomizationv1.GroupVersion, + object: kustomizationAdapter{&kustomizationv1.Kustomization{}}, + list: &kustomizationListAdapter{&kustomizationv1.KustomizationList{}}, + } + case "helmrelease": + return &suspendCommand{ + kind: helmv2beta1.HelmReleaseKind, + groupVersion: helmv2beta1.GroupVersion, + object: &helmReleaseAdapter{&helmv2beta1.HelmRelease{}}, + list: &helmReleaseListAdapter{&helmv2beta1.HelmReleaseList{}}, + } + case sourcev1.GitRepositoryKind: + return &suspendCommand{ + kind: sourcev1.GitRepositoryKind, + groupVersion: sourcev1.GroupVersion, + object: gitRepositoryAdapter{&sourcev1.GitRepository{}}, + list: gitRepositoryListAdapter{&sourcev1.GitRepositoryList{}}, + } + case sourcev1beta2.OCIRepositoryKind: + return &suspendCommand{ + kind: sourcev1beta2.OCIRepositoryKind, + groupVersion: sourcev1beta2.GroupVersion, + object: ociRepositoryAdapter{&sourcev1beta2.OCIRepository{}}, + list: ociRepositoryListAdapter{&sourcev1beta2.OCIRepositoryList{}}, + } + case sourcev1beta2.BucketKind: + return &suspendCommand{ + kind: sourcev1beta2.BucketKind, + groupVersion: sourcev1beta2.GroupVersion, + object: bucketAdapter{&sourcev1beta2.Bucket{}}, + list: bucketListAdapter{&sourcev1beta2.BucketList{}}, + } + } + + return nil +} + +func (s *suspendCommand) Run(config *rest.Config, namespace, name string) { + scheme := runtime.NewScheme() + sourcev1.AddToScheme(scheme) + sourcev1beta2.AddToScheme(scheme) + kustomizationv1.AddToScheme(scheme) + helmv2beta1.AddToScheme(scheme) + + //TODO log.SetLogger(...) was never called; logs will not be displayed. + kubeClient, err := client.NewWithWatch(config, client.Options{ + Scheme: scheme, + }) + if err != nil { + logrus.Errorf("kubernetes client initialization failed: %s", err) + return + } + + listOpts := []client.ListOption{ + client.InNamespace(namespace), + client.MatchingFields{ + "metadata.name": name, + }, + } + + if err := s.patch(context.TODO(), kubeClient, listOpts, namespace); err != nil { + if err == ErrNoObjectsFound { + logrus.Errorf("%s %s not found in %s namespace", s.kind, name, namespace) + } else { + logrus.Errorf("failed suspending %s %s in %s namespace: %s", s.kind, name, namespace, err.Error()) + } + } +} + +var ErrNoObjectsFound = errors.New("no objects found") + +func (s suspendCommand) patch(ctx context.Context, kubeClient client.WithWatch, listOpts []client.ListOption, namespace string) error { + if err := kubeClient.List(ctx, s.list.asClientList(), listOpts...); err != nil { + return err + } + + if s.list.len() == 0 { + return ErrNoObjectsFound + } + + for i := 0; i < s.list.len(); i++ { + logrus.Infof("suspending %s %s in %s namespace", s.kind, s.list.item(i).asClientObject().GetName(), namespace) + + obj := s.list.item(i) + patch := client.MergeFrom(obj.deepCopyClientObject()) + obj.setSuspended() + if err := kubeClient.Patch(ctx, obj.asClientObject(), patch); err != nil { + return err + } + + logrus.Infof("%s suspended", s.kind) + } + + return nil +} diff --git a/web/src/HelmRelease.jsx b/web/src/HelmRelease.jsx index 1126941..f7dedae 100644 --- a/web/src/HelmRelease.jsx +++ b/web/src/HelmRelease.jsx @@ -35,7 +35,12 @@ export function HelmRelease(props) {
-
+
+
-
+
+
-
+
+