From f7971a871aee6badffdc974706df52c72868e576 Mon Sep 17 00:00:00 2001 From: Philip Laine Date: Sun, 4 Oct 2020 16:50:36 +0200 Subject: [PATCH] Add alert provider commands --- cmd/gotk/create_alertprovider.go | 190 ++++++++++++++++++++++++++++ cmd/gotk/delete_alertprovider.go | 88 +++++++++++++ cmd/gotk/export_alertprovider.go | 120 ++++++++++++++++++ cmd/gotk/get_alertprovider.go | 83 ++++++++++++ cmd/gotk/reconcile_alertprovider.go | 93 ++++++++++++++ cmd/gotk/utils.go | 2 + go.mod | 1 + go.sum | 2 + 8 files changed, 579 insertions(+) create mode 100644 cmd/gotk/create_alertprovider.go create mode 100644 cmd/gotk/delete_alertprovider.go create mode 100644 cmd/gotk/export_alertprovider.go create mode 100644 cmd/gotk/get_alertprovider.go create mode 100644 cmd/gotk/reconcile_alertprovider.go diff --git a/cmd/gotk/create_alertprovider.go b/cmd/gotk/create_alertprovider.go new file mode 100644 index 00000000..64098f43 --- /dev/null +++ b/cmd/gotk/create_alertprovider.go @@ -0,0 +1,190 @@ +/* +Copyright 2020 The Flux CD contributors. + +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 main + +import ( + "context" + "fmt" + "github.com/fluxcd/pkg/apis/meta" + + "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" + "sigs.k8s.io/controller-runtime/pkg/client" + + notificationv1 "github.com/fluxcd/notification-controller/api/v1beta1" +) + +var createAlertProviderCmd = &cobra.Command{ + Use: "alert-provider [name]", + Aliases: []string{"ap"}, + Short: "Create or update a Provider resource", + Long: "The create alert-provider command generates a Provider resource.", + Example: ` # Create a Provider for a Slack channel + gotk create ap slack \ + --type slack \ + --channel general \ + --address https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK \ + --secret-ref webhook-url + + # Create a Provider for a Github repository + gotk create ap github-podinfo \ + --type github \ + --address https://github.com/stefanprodan/podinfo \ + --secret-ref github-token +`, + RunE: createAlertProviderCmdRun, +} + +var ( + apType string + apChannel string + apUsername string + apAddress string + apSecretRef string +) + +func init() { + createAlertProviderCmd.Flags().StringVar(&apType, "type", "", "type of provider") + createAlertProviderCmd.Flags().StringVar(&apChannel, "channel", "", "channel to send messages to in the case of a chat provider") + createAlertProviderCmd.Flags().StringVar(&apUsername, "username", "", "bot username used by the provider") + createAlertProviderCmd.Flags().StringVar(&apAddress, "address", "", "path to either the git repository, chat provider or webhook") + createAlertProviderCmd.Flags().StringVar(&apSecretRef, "secret-ref", "", "name of secret containing authentication token") + createCmd.AddCommand(createAlertProviderCmd) +} + +func createAlertProviderCmdRun(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return fmt.Errorf("provider name is required") + } + name := args[0] + + if apType == "" { + return fmt.Errorf("type is required") + } + + sourceLabels, err := parseLabels() + if err != nil { + return err + } + + if !export { + logger.Generatef("generating provider") + } + + alertProvider := notificationv1.Provider{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: sourceLabels, + }, + Spec: notificationv1.ProviderSpec{ + Type: apType, + Channel: apChannel, + Username: apUsername, + Address: apAddress, + SecretRef: &corev1.LocalObjectReference{ + Name: apSecretRef, + }, + }, + } + + if export { + return exportAlertProvider(alertProvider) + } + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + kubeClient, err := utils.kubeClient(kubeconfig) + if err != nil { + return err + } + + logger.Actionf("applying provider") + if err := upsertAlertProvider(ctx, kubeClient, alertProvider); err != nil { + return err + } + + logger.Waitingf("waiting for reconciliation") + if err := wait.PollImmediate(pollInterval, timeout, + isAlertProviderReady(ctx, kubeClient, name, namespace)); err != nil { + return err + } + + logger.Successf("provider %s is ready", name) + + return nil +} + +func upsertAlertProvider(ctx context.Context, kubeClient client.Client, alertProvider notificationv1.Provider) error { + namespacedName := types.NamespacedName{ + Namespace: alertProvider.GetNamespace(), + Name: alertProvider.GetName(), + } + + var existing notificationv1.Provider + err := kubeClient.Get(ctx, namespacedName, &existing) + if err != nil { + if errors.IsNotFound(err) { + if err := kubeClient.Create(ctx, &alertProvider); err != nil { + return err + } else { + logger.Successf("provider created") + return nil + } + } + return err + } + + existing.Labels = alertProvider.Labels + existing.Spec = alertProvider.Spec + if err := kubeClient.Update(ctx, &existing); err != nil { + return err + } + + logger.Successf("provider updated") + return nil +} + +func isAlertProviderReady(ctx context.Context, kubeClient client.Client, name, namespace string) wait.ConditionFunc { + return func() (bool, error) { + var alertProvider notificationv1.Provider + namespacedName := types.NamespacedName{ + Namespace: namespace, + Name: name, + } + + err := kubeClient.Get(ctx, namespacedName, &alertProvider) + if err != nil { + return false, err + } + + if c := meta.GetCondition(alertProvider.Status.Conditions, meta.ReadyCondition); c != nil { + switch c.Status { + case corev1.ConditionTrue: + return true, nil + case corev1.ConditionFalse: + return false, fmt.Errorf(c.Message) + } + } + return false, nil + } +} diff --git a/cmd/gotk/delete_alertprovider.go b/cmd/gotk/delete_alertprovider.go new file mode 100644 index 00000000..d4eceeef --- /dev/null +++ b/cmd/gotk/delete_alertprovider.go @@ -0,0 +1,88 @@ +/* +Copyright 2020 The Flux CD contributors. + +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 main + +import ( + "context" + "fmt" + + "github.com/manifoldco/promptui" + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/types" + + notificationv1 "github.com/fluxcd/notification-controller/api/v1beta1" +) + +var deleteAlertProviderCmd = &cobra.Command{ + Use: "alert-provider [name]", + Aliases: []string{"ap"}, + Short: "Delete a Provider resource", + Long: "The delete alert-provider command removes the given Provider from the cluster.", + Example: ` # Delete a Provider and the Kubernetes resources created by it + gotk delete ap slack +`, + RunE: deleteAlertProviderCmdRun, +} + +func init() { + deleteCmd.AddCommand(deleteAlertProviderCmd) +} + +func deleteAlertProviderCmdRun(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return fmt.Errorf("provider name is required") + } + name := args[0] + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + kubeClient, err := utils.kubeClient(kubeconfig) + if err != nil { + return err + } + + namespacedName := types.NamespacedName{ + Namespace: namespace, + Name: name, + } + + var alertProvider notificationv1.Provider + err = kubeClient.Get(ctx, namespacedName, &alertProvider) + if err != nil { + return err + } + + if !deleteSilent { + prompt := promptui.Prompt{ + Label: "Are you sure you want to delete this Provider", + IsConfirm: true, + } + if _, err := prompt.Run(); err != nil { + return fmt.Errorf("aborting") + } + } + + logger.Actionf("deleting provider %s in %s namespace", name, namespace) + err = kubeClient.Delete(ctx, &alertProvider) + if err != nil { + return err + } + logger.Successf("provider deleted") + + return nil +} diff --git a/cmd/gotk/export_alertprovider.go b/cmd/gotk/export_alertprovider.go new file mode 100644 index 00000000..d4ecd771 --- /dev/null +++ b/cmd/gotk/export_alertprovider.go @@ -0,0 +1,120 @@ +/* +Copyright 2020 The Flux CD contributors. + +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 main + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/yaml" + + notificationv1 "github.com/fluxcd/notification-controller/api/v1beta1" +) + +var exportAlertProviderCmd = &cobra.Command{ + Use: "alert-provider [name]", + Aliases: []string{"ap"}, + Short: "Export Provider resources in YAML format", + Long: "The export alert-provider command exports one or all Provider resources in YAML format.", + Example: ` # Export all Provider resources + gotk export ap --all > kustomizations.yaml + + # Export a Provider + gotk export ap slack > slack.yaml +`, + RunE: exportAlertProviderCmdRun, +} + +func init() { + exportCmd.AddCommand(exportAlertProviderCmd) +} + +func exportAlertProviderCmdRun(cmd *cobra.Command, args []string) error { + if !exportAll && len(args) < 1 { + return fmt.Errorf("name is required") + } + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + kubeClient, err := utils.kubeClient(kubeconfig) + if err != nil { + return err + } + + if exportAll { + var list notificationv1.ProviderList + err = kubeClient.List(ctx, &list, client.InNamespace(namespace)) + if err != nil { + return err + } + + if len(list.Items) == 0 { + logger.Failuref("no alertproviders found in %s namespace", namespace) + return nil + } + + for _, alertProvider := range list.Items { + if err := exportAlertProvider(alertProvider); err != nil { + return err + } + } + } else { + name := args[0] + namespacedName := types.NamespacedName{ + Namespace: namespace, + Name: name, + } + var alertProvider notificationv1.Provider + err = kubeClient.Get(ctx, namespacedName, &alertProvider) + if err != nil { + return err + } + return exportAlertProvider(alertProvider) + } + return nil +} + +func exportAlertProvider(alertProvider notificationv1.Provider) error { + gvk := notificationv1.GroupVersion.WithKind("Provider") + export := notificationv1.Provider{ + TypeMeta: metav1.TypeMeta{ + Kind: gvk.Kind, + APIVersion: gvk.GroupVersion().String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: alertProvider.Name, + Namespace: alertProvider.Namespace, + Labels: alertProvider.Labels, + Annotations: alertProvider.Annotations, + }, + Spec: alertProvider.Spec, + } + + data, err := yaml.Marshal(export) + if err != nil { + return err + } + + fmt.Println("---") + fmt.Println(resourceToString(data)) + return nil +} diff --git a/cmd/gotk/get_alertprovider.go b/cmd/gotk/get_alertprovider.go new file mode 100644 index 00000000..4b3a892e --- /dev/null +++ b/cmd/gotk/get_alertprovider.go @@ -0,0 +1,83 @@ +/* +Copyright 2020 The Flux CD contributors. + +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 main + +import ( + "context" + + "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + notificationv1 "github.com/fluxcd/notification-controller/api/v1beta1" + "github.com/fluxcd/pkg/apis/meta" +) + +var getAlertProviderCmd = &cobra.Command{ + Use: "alert-provider", + Aliases: []string{"ap"}, + Short: "Get Provider statuses", + Long: "The get alert-provider command prints the statuses of the resources.", + Example: ` # List all Providers and their status + gotk get alert-provider +`, + RunE: getAlertProviderCmdRun, +} + +func init() { + getCmd.AddCommand(getAlertProviderCmd) +} + +func getAlertProviderCmdRun(cmd *cobra.Command, args []string) error { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + kubeClient, err := utils.kubeClient(kubeconfig) + if err != nil { + return err + } + + var list notificationv1.ProviderList + err = kubeClient.List(ctx, &list, client.InNamespace(namespace)) + if err != nil { + return err + } + + if len(list.Items) == 0 { + logger.Failuref("no providers found in %s namespace", namespace) + return nil + } + + for _, provider := range list.Items { + isInitialized := false + if c := meta.GetCondition(provider.Status.Conditions, meta.ReadyCondition); c != nil { + switch c.Status { + case corev1.ConditionTrue: + logger.Successf("%s is ready", provider.GetName()) + case corev1.ConditionUnknown: + logger.Successf("%s reconciling", provider.GetName()) + default: + logger.Failuref("%s %s", provider.GetName(), c.Message) + } + isInitialized = true + } + if !isInitialized { + logger.Failuref("%s is not ready", provider.GetName()) + } + } + return nil +} diff --git a/cmd/gotk/reconcile_alertprovider.go b/cmd/gotk/reconcile_alertprovider.go new file mode 100644 index 00000000..cb01165f --- /dev/null +++ b/cmd/gotk/reconcile_alertprovider.go @@ -0,0 +1,93 @@ +/* +Copyright 2020 The Flux CD contributors. + +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 main + +import ( + "context" + "fmt" + "github.com/fluxcd/pkg/apis/meta" + "time" + + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" + + notificationv1 "github.com/fluxcd/notification-controller/api/v1beta1" +) + +var reconcileAlertProviderCmd = &cobra.Command{ + Use: "alert-provider [name]", + Short: "Reconcile a Provider source", + Long: `The reconcile source command triggers a reconciliation of a Provider resource and waits for it to finish.`, + Example: ` # Trigger a reconciliation for an existing source + gotk reconcile alert-provider slack +`, + RunE: reconcileAlertProviderCmdRun, +} + +func init() { + reconcileCmd.AddCommand(reconcileAlertProviderCmd) +} + +func reconcileAlertProviderCmdRun(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return fmt.Errorf("source name is required") + } + name := args[0] + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + kubeClient, err := utils.kubeClient(kubeconfig) + if err != nil { + return err + } + + namespacedName := types.NamespacedName{ + Namespace: namespace, + Name: name, + } + + logger.Actionf("annotating source %s in %s namespace", name, namespace) + var alertProvider notificationv1.Provider + err = kubeClient.Get(ctx, namespacedName, &alertProvider) + if err != nil { + return err + } + + if alertProvider.Annotations == nil { + alertProvider.Annotations = map[string]string{ + meta.ReconcileAtAnnotation: time.Now().Format(time.RFC3339Nano), + } + } else { + alertProvider.Annotations[meta.ReconcileAtAnnotation] = time.Now().Format(time.RFC3339Nano) + } + if err := kubeClient.Update(ctx, &alertProvider); err != nil { + return err + } + logger.Successf("source annotated") + + logger.Waitingf("waiting for reconciliation") + if err := wait.PollImmediate(pollInterval, timeout, + isAlertProviderReady(ctx, kubeClient, name, namespace)); err != nil { + return err + } + + logger.Successf("provider reconciliation completed") + + return nil +} diff --git a/cmd/gotk/utils.go b/cmd/gotk/utils.go index f0707de2..2d80e71d 100644 --- a/cmd/gotk/utils.go +++ b/cmd/gotk/utils.go @@ -43,6 +43,7 @@ import ( helmv2 "github.com/fluxcd/helm-controller/api/v2beta1" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta1" + notificationv1 "github.com/fluxcd/notification-controller/api/v1beta1" "github.com/fluxcd/pkg/runtime/dependency" sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" "github.com/olekukonko/tablewriter" @@ -137,6 +138,7 @@ func (*Utils) kubeClient(kubeConfigPath string) (client.Client, error) { _ = sourcev1.AddToScheme(scheme) _ = kustomizev1.AddToScheme(scheme) _ = helmv2.AddToScheme(scheme) + _ = notificationv1.AddToScheme(scheme) kubeClient, err := client.New(cfg, client.Options{ Scheme: scheme, diff --git a/go.mod b/go.mod index 8742d0de..db86c74d 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/blang/semver/v4 v4.0.0 github.com/fluxcd/helm-controller/api v0.1.1 github.com/fluxcd/kustomize-controller/api v0.1.0 + github.com/fluxcd/notification-controller/api v0.1.0 github.com/fluxcd/pkg/apis/meta v0.0.2 github.com/fluxcd/pkg/git v0.0.7 github.com/fluxcd/pkg/runtime v0.0.6 diff --git a/go.sum b/go.sum index 727871a7..568b16c3 100644 --- a/go.sum +++ b/go.sum @@ -115,6 +115,8 @@ github.com/fluxcd/helm-controller/api v0.1.1 h1:iKskkLGRYRi5hiZg/+Rn+rpneGPayGQP github.com/fluxcd/helm-controller/api v0.1.1/go.mod h1:orwdS+iYGcM8BReUQfIb5CJ+jiFdlKmnLnzp6K3FK2U= github.com/fluxcd/kustomize-controller/api v0.1.0 h1:dPowX408q0jO7wnWBj5Dglc22euAQBLxDhPS8XHlLM0= github.com/fluxcd/kustomize-controller/api v0.1.0/go.mod h1:upR7/OzX/wXJlKgiBLUn7ez4XG4Lo5edep2WKSx0u7c= +github.com/fluxcd/notification-controller/api v0.1.0 h1:+gJ0CFFg3OkjLGl48gBCVgqNbKNy54xzfjYVlPp8064= +github.com/fluxcd/notification-controller/api v0.1.0/go.mod h1:w1gILYTSqt3dFMYRmCihA/K84yDBfIkL5m5dcbaUyUY= github.com/fluxcd/pkg/apis/meta v0.0.2 h1:kyA4Y0IzNjf1joBOnFqpWG7aNDHvtLExZcaHQM7qhRI= github.com/fluxcd/pkg/apis/meta v0.0.2/go.mod h1:nCNps5JJOcEQr3MNDmZqI4o0chjePSUYL6Q2ktDtotU= github.com/fluxcd/pkg/git v0.0.7 h1:tFSYPy7tcIYfOt8H5EUERXIRz7fk0id302oQZde1NtU=