From f7971a871aee6badffdc974706df52c72868e576 Mon Sep 17 00:00:00 2001
From: Philip Laine <philip.laine@gmail.com>
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=
-- 
GitLab