merge: delete the notification subsystem, argoproj deps now zero

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-07-28 16:21:15 -07:00
co-authored by Hanzo Dev
32 changed files with 0 additions and 2997 deletions
@@ -1,185 +0,0 @@
package commands
import (
"context"
"fmt"
"math"
"net/http"
"os"
"os/signal"
"runtime/debug"
"strings"
"sync"
"syscall"
"github.com/argoproj/notifications-engine/pkg/controller"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
"k8s.io/client-go/tools/clientcmd"
"github.com/hanzoai/deploy/common"
notificationscontroller "github.com/hanzoai/deploy/notification_controller/controller"
"github.com/hanzoai/deploy/reposerver/apiclient"
"github.com/hanzoai/deploy/util/cli"
"github.com/hanzoai/deploy/util/env"
"github.com/hanzoai/deploy/util/errors"
service "github.com/hanzoai/deploy/util/notification/cd"
"github.com/hanzoai/deploy/util/tls"
)
const (
defaultMetricsPort = 9001
)
func NewCommand() *cobra.Command {
var (
clientConfig clientcmd.ClientConfig
processorsCount int
appLabelSelector string
logLevel string
logFormat string
metricsPort int
cdRepoServer string
cdRepoServerPlaintext bool
cdRepoServerStrictTLS bool
configMapName string
secretName string
applicationNamespaces []string
selfServiceNotificationEnabled bool
repoServerClientTLSConfigSrc func() (tls.Configuration, error)
)
command := cobra.Command{
Use: common.CommandNotifications,
Short: "Starts Hanzo CD Notifications controller",
RunE: func(_ *cobra.Command, _ []string) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
vers := common.GetVersion()
namespace, _, err := clientConfig.Namespace()
errors.CheckError(err)
vers.LogStartupInfo(
"Hanzo CD Notifications Controller",
map[string]any{
"namespace": namespace,
},
)
restConfig, err := clientConfig.ClientConfig()
if err != nil {
return fmt.Errorf("failed to create REST client config: %w", err)
}
restConfig.UserAgent = fmt.Sprintf("cd-notifications-controller/%s (%s)", vers.Version, vers.Platform)
dynamicClient, err := dynamic.NewForConfig(restConfig)
if err != nil {
return fmt.Errorf("failed to create dynamic client: %w", err)
}
k8sClient, err := kubernetes.NewForConfig(restConfig)
if err != nil {
return fmt.Errorf("failed to create Kubernetes client: %w", err)
}
if namespace == "" {
namespace, _, err = clientConfig.Namespace()
if err != nil {
return fmt.Errorf("failed to determine controller's host namespace: %w", err)
}
}
level, err := log.ParseLevel(logLevel)
if err != nil {
return fmt.Errorf("failed to parse log level: %w", err)
}
log.SetLevel(level)
switch strings.ToLower(logFormat) {
case "json":
log.SetFormatter(&log.JSONFormatter{})
case "text":
if os.Getenv("FORCE_LOG_COLORS") == "1" {
log.SetFormatter(&log.TextFormatter{ForceColors: true})
}
default:
return fmt.Errorf("unknown log format '%s'", logFormat)
}
// Recover from panic and log the error using the configured logger instead of the default.
defer func() {
if r := recover(); r != nil {
log.WithField("trace", string(debug.Stack())).Fatal("Recovered from panic: ", r)
}
}()
tlsConfig, err := repoServerClientTLSConfigSrc()
if err != nil {
return fmt.Errorf("failed to get repo-server client TLS configuration: %w", err)
}
tlsConfig.DisableTLS = cdRepoServerPlaintext
tlsConfig.StrictValidation = tlsConfig.StrictValidation || cdRepoServerStrictTLS
if !tlsConfig.DisableTLS && tlsConfig.StrictValidation && tlsConfig.Certificates == nil {
pool, err := tls.LoadX509CertPool(
env.StringFromEnv(common.EnvAppConfigPath, common.DefaultAppConfigPath)+"/reposerver/tls/tls.crt",
env.StringFromEnv(common.EnvAppConfigPath, common.DefaultAppConfigPath)+"/reposerver/tls/ca.crt",
)
if err != nil {
return fmt.Errorf("failed to load repo-server certificate pool: %w", err)
}
tlsConfig.Certificates = pool
}
repoClientset := apiclient.NewRepoServerClientset(cdRepoServer, 5, tlsConfig)
cdService, err := service.NewArgoCDService(k8sClient, dynamicClient, namespace, repoClientset)
if err != nil {
return fmt.Errorf("failed to initialize Hanzo CD service: %w", err)
}
defer cdService.Close()
registry := controller.NewMetricsRegistry("cd")
http.Handle("/metrics", promhttp.HandlerFor(prometheus.Gatherers{registry, prometheus.DefaultGatherer}, promhttp.HandlerOpts{}))
go func() {
log.Fatal(http.ListenAndServe(fmt.Sprintf("0.0.0.0:%d", metricsPort), http.DefaultServeMux))
}()
log.Infof("serving metrics on port %d", metricsPort)
log.Infof("loading configuration %d", metricsPort)
ctrl := notificationscontroller.NewController(k8sClient, dynamicClient, cdService, namespace, applicationNamespaces, appLabelSelector, registry, secretName, configMapName, selfServiceNotificationEnabled)
err = ctrl.Init(ctx)
if err != nil {
return fmt.Errorf("failed to initialize controller: %w", err)
}
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
wg := sync.WaitGroup{}
wg.Go(func() {
s := <-sigCh
log.Printf("got signal %v, attempting graceful shutdown", s)
cancel()
})
go ctrl.Run(ctx, processorsCount)
<-ctx.Done()
return nil
},
}
clientConfig = cli.AddKubectlFlagsToCmd(&command)
command.Flags().IntVar(&processorsCount, "processors-count", env.ParseNumFromEnv("CD_NOTIFICATION_CONTROLLER_PROCESSORS_COUNT", 1, 1, math.MaxInt32), "Processors count.")
command.Flags().StringVar(&appLabelSelector, "app-label-selector", "", "App label selector.")
command.Flags().StringVar(&logLevel, "loglevel", env.StringFromEnv("CD_NOTIFICATIONS_CONTROLLER_LOGLEVEL", "info"), "Set the logging level. One of: debug|info|warn|error")
command.Flags().StringVar(&logFormat, "logformat", env.StringFromEnv("CD_NOTIFICATIONS_CONTROLLER_LOGFORMAT", "json"), "Set the logging format. One of: json|text")
command.Flags().IntVar(&metricsPort, "metrics-port", defaultMetricsPort, "Metrics port")
command.Flags().StringVar(&cdRepoServer, "cd-repo-server", common.DefaultRepoServerAddr, "Hanzo CD repo server address")
command.Flags().BoolVar(&cdRepoServerPlaintext, "cd-repo-server-plaintext", env.ParseBoolFromEnv("CD_NOTIFICATION_CONTROLLER_REPO_SERVER_PLAINTEXT", false), "Use a plaintext client (non-TLS) to connect to repository server")
command.Flags().BoolVar(&cdRepoServerStrictTLS, "cd-repo-server-strict-tls", false, "Perform strict validation of TLS certificates when connecting to repo server")
errors.CheckError(command.Flags().MarkDeprecated("cd-repo-server-strict-tls", "use --cd-repo-server-ca-cert-path instead"))
command.Flags().StringVar(&configMapName, "config-map-name", "cd-notifications-cm", "Set notifications ConfigMap name")
command.Flags().StringVar(&secretName, "secret-name", "cd-notifications-secret", "Set notifications Secret name")
command.Flags().StringSliceVar(&applicationNamespaces, "application-namespaces", env.StringsFromEnv("CD_APPLICATION_NAMESPACES", []string{}, ","), "List of additional namespaces that this controller should send notifications for")
command.Flags().BoolVar(&selfServiceNotificationEnabled, "self-service-notification-enabled", env.ParseBoolFromEnv("CD_NOTIFICATION_CONTROLLER_SELF_SERVICE_NOTIFICATION_ENABLED", false), "Allows the Hanzo CD notification controller to pull notification config from the namespace that the resource is in. This is useful for self-service notification.")
repoServerClientTLSConfigSrc = tls.AddClientTLSFlagsToCmdWithPrefix(&command, "NOTIFICATION_CONTROLLER")
return &command
}
@@ -1,28 +0,0 @@
package commands
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewCommandProcessorsCountDefault(t *testing.T) {
t.Setenv("CD_NOTIFICATION_CONTROLLER_PROCESSORS_COUNT", "4")
cmd := NewCommand()
processorsCount, err := cmd.Flags().GetInt("processors-count")
require.NoError(t, err)
assert.Equal(t, 4, processorsCount)
}
func TestNewCommandProcessorsCountInvalidEnvFallsBackToDefault(t *testing.T) {
t.Setenv("CD_NOTIFICATION_CONTROLLER_PROCESSORS_COUNT", "0")
cmd := NewCommand()
processorsCount, err := cmd.Flags().GetInt("processors-count")
require.NoError(t, err)
assert.Equal(t, 1, processorsCount)
}
-1
View File
@@ -78,7 +78,6 @@ $ cd admin initial-password reset
command.AddCommand(NewImportCommand())
command.AddCommand(NewExportCommand())
command.AddCommand(NewDashboardCommand(clientOpts))
command.AddCommand(NewNotificationsCommand())
command.AddCommand(NewInitialPasswordCommand())
command.AddCommand(NewRedisInitialPasswordCommand())
-86
View File
@@ -1,86 +0,0 @@
package admin
import (
"context"
"log"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
"github.com/hanzoai/deploy/common"
"github.com/hanzoai/deploy/reposerver/apiclient"
"github.com/hanzoai/deploy/util/env"
service "github.com/hanzoai/deploy/util/notification/cd"
"github.com/hanzoai/deploy/util/notification/settings"
"github.com/hanzoai/deploy/util/tls"
"github.com/argoproj/notifications-engine/pkg/cmd"
"github.com/spf13/cobra"
"github.com/hanzoai/deploy/pkg/apis/application"
)
var applications = schema.GroupVersionResource{Group: application.Group, Version: "v1alpha1", Resource: application.ApplicationPlural}
func NewNotificationsCommand() *cobra.Command {
var (
cdRepoServer string
cdRepoServerPlaintext bool
cdRepoServerStrictTLS bool
)
var cdService service.Service
var repoServerClientTLSConfigSrc func() (tls.Configuration, error)
toolsCommand := cmd.NewToolsCommand(
"notifications",
"cd admin notifications",
applications,
settings.GetFactorySettingsForCLI(func() service.Service { return cdService }, "cd-notifications-secret", "cd-notifications-cm", false),
func(_ context.Context, clientConfig clientcmd.ClientConfig) {
k8sCfg, err := clientConfig.ClientConfig()
if err != nil {
log.Fatalf("Failed to parse k8s config: %v", err)
}
ns, _, err := clientConfig.Namespace()
if err != nil {
log.Fatalf("Failed to parse k8s config: %v", err)
}
tlsConfig, err := repoServerClientTLSConfigSrc()
if err != nil {
log.Fatalf("Failed to get repo-server client TLS configuration: %v", err)
}
tlsConfig.DisableTLS = cdRepoServerPlaintext
tlsConfig.StrictValidation = cdRepoServerStrictTLS || tlsConfig.StrictValidation
if !tlsConfig.DisableTLS && tlsConfig.StrictValidation && tlsConfig.Certificates == nil {
pool, err := tls.LoadX509CertPool(
env.StringFromEnv(common.EnvAppConfigPath, common.DefaultAppConfigPath)+"/reposerver/tls/tls.crt",
env.StringFromEnv(common.EnvAppConfigPath, common.DefaultAppConfigPath)+"/reposerver/tls/ca.crt",
)
if err != nil {
log.Fatalf("Failed to load tls certs: %v", err)
}
tlsConfig.Certificates = pool
}
repoClientset := apiclient.NewRepoServerClientset(cdRepoServer, 5, tlsConfig)
dynamicClient, err := dynamic.NewForConfig(k8sCfg)
if err != nil {
log.Fatalf("Failed to create dynamic client: %v", err)
}
cdService, err = service.NewArgoCDService(kubernetes.NewForConfigOrDie(k8sCfg), dynamicClient, ns, repoClientset)
if err != nil {
log.Fatalf("Failed to initialize Hanzo CD service: %v", err)
}
})
toolsCommand.PersistentFlags().StringVar(&cdRepoServer, "cd-repo-server", common.DefaultRepoServerAddr, "Hanzo CD repo server address")
toolsCommand.PersistentFlags().BoolVar(&cdRepoServerPlaintext, "cd-repo-server-plaintext", false, "Use a plaintext client (non-TLS) to connect to repository server")
toolsCommand.PersistentFlags().BoolVar(&cdRepoServerStrictTLS, "cd-repo-server-strict-tls", false, "Perform strict validation of TLS certificates when connecting to repo server")
if err := toolsCommand.PersistentFlags().MarkDeprecated("cd-repo-server-strict-tls", "use --cd-repo-server-ca-cert-path instead"); err != nil {
log.Fatal(err)
}
repoServerClientTLSConfigSrc = tls.AddClientTLSFlagsToCmd(toolsCommand)
return toolsCommand
}
-3
View File
@@ -16,7 +16,6 @@ import (
dex "github.com/hanzoai/deploy/cmd/cd-dex/commands"
gitaskpass "github.com/hanzoai/deploy/cmd/cd-git-ask-pass/commands"
k8sauth "github.com/hanzoai/deploy/cmd/cd-k8s-auth/commands"
notification "github.com/hanzoai/deploy/cmd/cd-notification/commands"
reposerver "github.com/hanzoai/deploy/cmd/cd-repo-server/commands"
apiserver "github.com/hanzoai/deploy/cmd/cd-server/commands"
cli "github.com/hanzoai/deploy/cmd/cd/commands"
@@ -60,8 +59,6 @@ func main() {
command = commitserver.NewCommand()
case common.CommandDex:
command = dex.NewCommand()
case common.CommandNotifications:
command = notification.NewCommand()
case common.CommandGitAskPass:
command = gitaskpass.NewCommand()
isArgocdCLI = true
-1
View File
@@ -29,7 +29,6 @@ const (
CommandCMPServer = "hanzocd-cmp-server"
CommandCommitServer = "hanzocd-commit-server"
CommandGitAskPass = "hanzocd-git-ask-pass"
CommandNotifications = "hanzocd-notifications"
CommandK8sAuth = "hanzocd-k8s-auth"
CommandDex = "hanzocd-dex"
CommandRepoServer = "hanzocd-repo-server"
-1
View File
@@ -14,7 +14,6 @@ require (
github.com/Masterminds/sprig/v3 v3.3.0
github.com/TomOnTime/utfutil v1.0.0
github.com/alicebob/miniredis/v2 v2.38.0
github.com/argoproj/notifications-engine v0.5.1-0.20260503100631-0cff13b8a717
github.com/aws/aws-sdk-go-v2 v1.42.1
github.com/aws/aws-sdk-go-v2/config v1.32.30
github.com/aws/aws-sdk-go-v2/credentials v1.19.29
-251
View File
@@ -1,251 +0,0 @@
package main
import (
"bytes"
"fmt"
"io"
"os"
"path"
"path/filepath"
"strings"
"github.com/argoproj/notifications-engine/pkg/services"
"github.com/argoproj/notifications-engine/pkg/triggers"
"github.com/argoproj/notifications-engine/pkg/util/misc"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/renderer"
"github.com/olekukonko/tablewriter/tw"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/cobra/doc"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/yaml"
"github.com/hanzoai/deploy/cmd/cd/commands/admin"
)
func main() {
command := &cobra.Command{
Use: "gen",
Run: func(c *cobra.Command, args []string) {
c.HelpFunc()(c, args)
},
}
command.AddCommand(newDocsCommand())
command.AddCommand(newCatalogCommand())
if err := command.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func newCatalogCommand() *cobra.Command {
return &cobra.Command{
Use: "catalog",
Run: func(_ *cobra.Command, _ []string) {
cm := corev1.ConfigMap{
TypeMeta: metav1.TypeMeta{
Kind: "ConfigMap",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "cd-notifications-cm",
},
Data: make(map[string]string),
}
wd, err := os.Getwd()
dieOnError(err, "Failed to get current working directory")
target := path.Join(wd, "notifications_catalog/install.yaml")
templatesDir := path.Join(wd, "notifications_catalog/templates")
triggersDir := path.Join(wd, "notifications_catalog/triggers")
templates, triggers, err := buildConfigFromFS(templatesDir, triggersDir)
dieOnError(err, "Failed to build catalog config")
misc.IterateStringKeyMap(triggers, func(name string) {
trigger := triggers[name]
t, err := yaml.Marshal(trigger)
dieOnError(err, "Failed to marshal trigger")
cm.Data["trigger."+name] = string(t)
})
misc.IterateStringKeyMap(templates, func(name string) {
template := templates[name]
t, err := yaml.Marshal(template)
dieOnError(err, "Failed to marshal template")
cm.Data["template."+name] = string(t)
})
d, err := yaml.Marshal(cm)
dieOnError(err, "Failed to marshal final configmap")
err = os.WriteFile(target, d, 0o644)
dieOnError(err, "Failed to write builtin configmap")
},
}
}
func newDocsCommand() *cobra.Command {
return &cobra.Command{
Use: "docs",
Run: func(_ *cobra.Command, _ []string) {
var builtItDocsData bytes.Buffer
wd, err := os.Getwd()
dieOnError(err, "Failed to get current working directory")
templatesDir := path.Join(wd, "notifications_catalog/templates")
triggersDir := path.Join(wd, "notifications_catalog/triggers")
notificationTemplates, notificationTriggers, err := buildConfigFromFS(templatesDir, triggersDir)
dieOnError(err, "Failed to build builtin config")
generateBuiltInTriggersDocs(&builtItDocsData, notificationTriggers, notificationTemplates)
if err := os.WriteFile("./docs/operator-manual/notifications/catalog.md", builtItDocsData.Bytes(), 0o644); err != nil {
log.Fatal(err)
}
var commandDocs bytes.Buffer
if err := generateCommandsDocs(&commandDocs); err != nil {
log.Fatal(err)
}
if err := os.WriteFile("./docs/operator-manual/notifications/troubleshooting-commands.md", commandDocs.Bytes(), 0o644); err != nil {
log.Fatal(err)
}
},
}
}
func generateBuiltInTriggersDocs(out io.Writer, triggers map[string][]triggers.Condition, templates map[string]services.Notification) {
_, _ = fmt.Fprintln(out, "# Triggers and Templates Catalog")
_, _ = fmt.Fprintln(out, "## Getting Started")
_, _ = fmt.Fprintln(out, "* Install Triggers and Templates from the catalog")
_, _ = fmt.Fprintln(out, " ```bash")
_, _ = fmt.Fprintln(out, " kubectl apply -n cd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/notifications_catalog/install.yaml")
_, _ = fmt.Fprintln(out, " ```")
_, _ = fmt.Fprintln(out, "## Triggers")
r := tw.Rendition{
Borders: tw.Border{Left: tw.On, Top: tw.Off, Right: tw.On, Bottom: tw.Off},
Symbols: tw.NewSymbolCustom("pipe-center").WithCenter("|").WithMidLeft("|").WithMidRight("|"),
}
c := tablewriter.NewConfigBuilder().WithRowAutoWrap(tw.WrapNone).Build()
table := tablewriter.NewTable(out, tablewriter.WithConfig(c), tablewriter.WithRenderer(renderer.NewBlueprint(r)))
table.Header("NAME", "DESCRIPTION", "TEMPLATE")
misc.IterateStringKeyMap(triggers, func(name string) {
t := triggers[name]
desc := ""
template := ""
if len(t) > 0 {
desc = t[0].Description
template = strings.Join(t[0].Send, ",")
}
err := table.Append([]string{name, desc, fmt.Sprintf("[%s](#%s)", template, template)})
if err != nil {
panic(err)
}
})
err := table.Render()
if err != nil {
panic(err)
}
_, _ = fmt.Fprintln(out, "")
_, _ = fmt.Fprintln(out, "## Templates")
misc.IterateStringKeyMap(templates, func(name string) {
t := templates[name]
yamlData, err := yaml.Marshal(t)
if err != nil {
panic(err)
}
_, _ = fmt.Fprintf(out, "### %s\n**definition**:\n```yaml\n%s\n```\n", name, string(yamlData))
})
}
func generateCommandsDocs(out io.Writer) error {
// create parents so that CommandPath() is correctly resolved
mainCmd := &cobra.Command{Use: "cd"}
adminCmd := &cobra.Command{Use: "admin"}
toolCmd := admin.NewNotificationsCommand()
adminCmd.AddCommand(toolCmd)
mainCmd.AddCommand(adminCmd)
for _, mainSubCommand := range mainCmd.Commands() {
for _, adminSubCommand := range mainSubCommand.Commands() {
for _, toolSubCommand := range adminSubCommand.Commands() {
for _, c := range toolSubCommand.Commands() {
var cmdDesc bytes.Buffer
if err := doc.GenMarkdown(c, &cmdDesc); err != nil {
return fmt.Errorf("error generating Markdown for command: %v : %w", c, err)
}
for line := range strings.SplitSeq(cmdDesc.String(), "\n") {
if strings.HasPrefix(line, "### SEE ALSO") {
break
}
_, _ = fmt.Fprintf(out, "%s\n", line)
}
}
}
}
}
return nil
}
func dieOnError(err error, msg string) {
if err != nil {
fmt.Printf("[ERROR] %s: %v", msg, err)
os.Exit(1)
}
}
func buildConfigFromFS(templatesDir string, triggersDir string) (map[string]services.Notification, map[string][]triggers.Condition, error) {
templatesCfg := map[string]services.Notification{}
err := filepath.Walk(templatesDir, func(p string, info os.FileInfo, e error) error {
if e != nil {
return fmt.Errorf("error navigating the templates dirctory: %s : %w", templatesDir, e)
}
if info.IsDir() {
return nil
}
data, err := os.ReadFile(p)
if err != nil {
return fmt.Errorf("error reading the template file: %s : %w", p, err)
}
name := strings.Split(path.Base(p), ".")[0]
var template services.Notification
if err := yaml.Unmarshal(data, &template); err != nil {
return fmt.Errorf("error unmarshaling the data from file: %s : %w", p, err)
}
templatesCfg[name] = template
return nil
})
if err != nil {
return nil, nil, err
}
triggersCfg := map[string][]triggers.Condition{}
err = filepath.Walk(triggersDir, func(p string, info os.FileInfo, e error) error {
if e != nil {
return fmt.Errorf("error navigating the triggers dirctory: %s : %w", triggersDir, e)
}
if info.IsDir() {
return nil
}
data, err := os.ReadFile(p)
if err != nil {
return fmt.Errorf("error reading the trigger file: %s : %w", p, err)
}
name := strings.Split(path.Base(p), ".")[0]
var trigger []triggers.Condition
if err := yaml.Unmarshal(data, &trigger); err != nil {
return fmt.Errorf("error unmarshaling the data from file: %s : %w", p, err)
}
triggersCfg[name] = trigger
return nil
})
if err != nil {
return nil, nil, err
}
return templatesCfg, triggersCfg, nil
}
-16
View File
@@ -3,7 +3,6 @@ package main
import (
"bytes"
"fmt"
"log"
"os"
"sort"
"strings"
@@ -11,26 +10,11 @@ import (
"gopkg.in/yaml.v2"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/argoproj/notifications-engine/pkg/docs"
)
func main() {
generateNotificationsDocs()
}
func generateNotificationsDocs() {
_ = os.RemoveAll("./docs/operator-manual/notifications/services")
_ = os.MkdirAll("./docs/operator-manual/notifications/services", 0o755)
files, err := docs.CopyServicesDocs("./docs/operator-manual/notifications/services")
if err != nil {
log.Fatal(err)
}
if files != nil {
if e := updateMkDocsNav("Operator Manual", "Notifications", "Notification Services", files); e != nil {
log.Fatal(e)
}
}
}
func updateMkDocsNav(parent string, child string, subchild string, files []string) error {
trimPrefixes(files, "docs/")
@@ -1,264 +0,0 @@
package controller
import (
"context"
"errors"
"fmt"
"time"
"github.com/hanzoai/deploy/util/glob"
"github.com/hanzoai/deploy/util/notification/k8s"
service "github.com/hanzoai/deploy/util/notification/cd"
argocert "github.com/hanzoai/deploy/util/cert"
"k8s.io/apimachinery/pkg/runtime/schema"
"github.com/hanzoai/deploy/util/notification/settings"
"github.com/argoproj/notifications-engine/pkg/api"
"github.com/argoproj/notifications-engine/pkg/controller"
"github.com/argoproj/notifications-engine/pkg/services"
"github.com/argoproj/notifications-engine/pkg/subscriptions"
httputil "github.com/argoproj/notifications-engine/pkg/util/http"
log "github.com/sirupsen/logrus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/cache"
"github.com/hanzoai/deploy/pkg/apis/application"
)
const (
resyncPeriod = 60 * time.Second
)
var (
applications = schema.GroupVersionResource{Group: application.Group, Version: "v1alpha1", Resource: application.ApplicationPlural}
appProjects = schema.GroupVersionResource{Group: application.Group, Version: "v1alpha1", Resource: application.AppProjectPlural}
)
func newAppProjClient(client dynamic.Interface, namespace string) dynamic.ResourceInterface {
resClient := client.Resource(appProjects).Namespace(namespace)
return resClient
}
type NotificationController interface {
Run(ctx context.Context, processors int)
Init(ctx context.Context) error
}
type notificationController struct {
ctrl controller.NotificationController
appInformer cache.SharedIndexInformer
appProjInformer cache.SharedIndexInformer
secretInformer cache.SharedIndexInformer
configMapInformer cache.SharedIndexInformer
}
func NewController(
k8sClient kubernetes.Interface,
client dynamic.Interface,
cdService service.Service,
namespace string,
applicationNamespaces []string,
appLabelSelector string,
registry *controller.MetricsRegistry,
secretName string,
configMapName string,
selfServiceNotificationEnabled bool,
) *notificationController {
var appClient dynamic.ResourceInterface
namespaceableAppClient := client.Resource(applications)
appClient = namespaceableAppClient
if len(applicationNamespaces) == 0 {
appClient = namespaceableAppClient.Namespace(namespace)
}
appInformer := newInformer(appClient, namespace, applicationNamespaces, appLabelSelector)
appProjInformer := newInformer(newAppProjClient(client, namespace), namespace, []string{namespace}, "")
var notificationConfigNamespace string
if selfServiceNotificationEnabled {
notificationConfigNamespace = metav1.NamespaceAll
} else {
notificationConfigNamespace = namespace
}
secretInformer := k8s.NewSecretInformer(k8sClient, notificationConfigNamespace, secretName)
configMapInformer := k8s.NewConfigMapInformer(k8sClient, notificationConfigNamespace, configMapName)
apiFactory := api.NewFactory(settings.GetFactorySettings(cdService, secretName, configMapName, selfServiceNotificationEnabled), namespace, secretInformer, configMapInformer)
res := &notificationController{
secretInformer: secretInformer,
configMapInformer: configMapInformer,
appInformer: appInformer,
appProjInformer: appProjInformer,
}
skipProcessingOpt := controller.WithSkipProcessing(func(obj metav1.Object) (bool, string) {
app, ok := (obj).(*unstructured.Unstructured)
if !ok {
return false, ""
}
if checkAppNotInAdditionalNamespaces(app, namespace, applicationNamespaces) {
return true, "app is not in one of the application-namespaces, nor the notification controller namespace"
}
return !isAppSyncStatusRefreshed(app, log.WithField("app", obj.GetName())), "sync status out of date"
})
metricsRegistryOpt := controller.WithMetricsRegistry(registry)
alterDestinationsOpt := controller.WithAlterDestinations(res.alterDestinations)
if !selfServiceNotificationEnabled {
res.ctrl = controller.NewController(namespaceableAppClient, appInformer, apiFactory,
skipProcessingOpt,
metricsRegistryOpt,
alterDestinationsOpt)
} else {
res.ctrl = controller.NewControllerWithNamespaceSupport(namespaceableAppClient, appInformer, apiFactory,
skipProcessingOpt,
metricsRegistryOpt,
alterDestinationsOpt)
}
return res
}
// Check if app is not in the namespace where the controller is in, and also app is not in one of the applicationNamespaces
func checkAppNotInAdditionalNamespaces(app *unstructured.Unstructured, namespace string, applicationNamespaces []string) bool {
return namespace != app.GetNamespace() && !glob.MatchStringInList(applicationNamespaces, app.GetNamespace(), glob.REGEXP)
}
func (c *notificationController) alterDestinations(obj metav1.Object, destinations services.Destinations, cfg api.Config) services.Destinations {
app, ok := (obj).(*unstructured.Unstructured)
if !ok {
return destinations
}
if proj := getAppProj(app, c.appProjInformer); proj != nil {
destinations.Merge(subscriptions.NewAnnotations(proj.GetAnnotations()).GetDestinations(cfg.DefaultTriggers, cfg.ServiceDefaultTriggers))
destinations.Merge(settings.GetLegacyDestinations(proj.GetAnnotations(), cfg.DefaultTriggers, cfg.ServiceDefaultTriggers))
}
return destinations
}
func newInformer(resClient dynamic.ResourceInterface, controllerNamespace string, applicationNamespaces []string, selector string) cache.SharedIndexInformer {
informer := cache.NewSharedIndexInformer(
&cache.ListWatch{
ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
// We are only interested in apps that exist in namespaces the
// user wants to be enabled.
options.LabelSelector = selector
appList, err := resClient.List(context.TODO(), options)
if err != nil {
return nil, fmt.Errorf("failed to list applications: %w", err)
}
newItems := []unstructured.Unstructured{}
for _, res := range appList.Items {
if controllerNamespace == res.GetNamespace() || glob.MatchStringInList(applicationNamespaces, res.GetNamespace(), glob.REGEXP) {
newItems = append(newItems, res)
}
}
appList.Items = newItems
return appList, nil
},
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
options.LabelSelector = selector
return resClient.Watch(context.TODO(), options)
},
},
&unstructured.Unstructured{},
resyncPeriod,
cache.Indexers{
cache.NamespaceIndex: cache.MetaNamespaceIndexFunc,
},
)
return informer
}
func (c *notificationController) Init(ctx context.Context) error {
// resolve certificates using injected "cd-tls-certs-cm" ConfigMap
httputil.SetCertResolver(argocert.GetCertificateForConnect)
go c.appInformer.Run(ctx.Done())
go c.appProjInformer.Run(ctx.Done())
go c.secretInformer.Run(ctx.Done())
go c.configMapInformer.Run(ctx.Done())
if !cache.WaitForCacheSync(ctx.Done(), c.appInformer.HasSynced, c.appProjInformer.HasSynced, c.secretInformer.HasSynced, c.configMapInformer.HasSynced) {
return errors.New("timed out waiting for caches to sync")
}
return nil
}
func (c *notificationController) Run(ctx context.Context, processors int) {
c.ctrl.Run(processors, ctx.Done())
}
func getAppProj(app *unstructured.Unstructured, appProjInformer cache.SharedIndexInformer) *unstructured.Unstructured {
projName, ok, err := unstructured.NestedString(app.Object, "spec", "project")
if !ok || err != nil {
return nil
}
projObj, ok, err := appProjInformer.GetIndexer().GetByKey(fmt.Sprintf("%s/%s", app.GetNamespace(), projName))
if !ok || err != nil {
return nil
}
proj, ok := projObj.(*unstructured.Unstructured)
if !ok {
return nil
}
if proj.GetAnnotations() == nil {
proj.SetAnnotations(map[string]string{})
}
return proj
}
// Checks if the application SyncStatus has been refreshed by Hanzo CD after an operation has completed
func isAppSyncStatusRefreshed(app *unstructured.Unstructured, logEntry *log.Entry) bool {
_, ok, err := unstructured.NestedMap(app.Object, "status", "operationState")
if !ok || err != nil {
logEntry.Debug("No OperationState found, SyncStatus is assumed to be up-to-date")
return true
}
phase, ok, err := unstructured.NestedString(app.Object, "status", "operationState", "phase")
if !ok || err != nil {
logEntry.Debug("No OperationPhase found, SyncStatus is assumed to be up-to-date")
return true
}
switch phase {
case "Failed", "Error", "Succeeded":
finishedAtRaw, ok, err := unstructured.NestedString(app.Object, "status", "operationState", "finishedAt")
if !ok || err != nil {
logEntry.Debugf("No FinishedAt found for completed phase '%s', SyncStatus is assumed to be out-of-date", phase)
return false
}
finishedAt, err := time.Parse(time.RFC3339, finishedAtRaw)
if err != nil {
logEntry.Warnf("Failed to parse FinishedAt '%s'", finishedAtRaw)
return false
}
var reconciledAt, observedAt time.Time
reconciledAtRaw, ok, err := unstructured.NestedString(app.Object, "status", "reconciledAt")
if ok && err == nil {
reconciledAt, _ = time.Parse(time.RFC3339, reconciledAtRaw)
}
observedAtRaw, ok, err := unstructured.NestedString(app.Object, "status", "observedAt")
if ok && err == nil {
observedAt, _ = time.Parse(time.RFC3339, observedAtRaw)
}
if finishedAt.After(reconciledAt) && finishedAt.After(observedAt) {
logEntry.Debugf("SyncStatus out-of-date (FinishedAt=%v, ReconciledAt=%v, Observed=%v", finishedAt, reconciledAt, observedAt)
return false
}
logEntry.Debugf("SyncStatus up-to-date (FinishedAt=%v, ReconciledAt=%v, Observed=%v", finishedAt, reconciledAt, observedAt)
default:
logEntry.Debugf("Found phase '%s', SyncStatus is assumed to be up-to-date", phase)
}
return true
}
@@ -1,194 +0,0 @@
package controller
import (
"context"
"testing"
"time"
"github.com/sirupsen/logrus/hooks/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/dynamic/fake"
k8sfake "k8s.io/client-go/kubernetes/fake"
"k8s.io/client-go/tools/cache"
"github.com/hanzoai/deploy/pkg/apis/application/v1alpha1"
)
func TestIsAppSyncStatusRefreshed(t *testing.T) {
logger, _ := test.NewNullLogger()
logEntry := logger.WithField("", "")
tests := []struct {
name string
app *unstructured.Unstructured
expectedValue bool
}{
{
name: "No OperationState",
app: &unstructured.Unstructured{
Object: map[string]any{
"status": map[string]any{},
},
},
expectedValue: true,
},
{
name: "No FinishedAt, Completed Phase",
app: &unstructured.Unstructured{
Object: map[string]any{
"status": map[string]any{
"operationState": map[string]any{
"phase": "Succeeded",
},
},
},
},
expectedValue: false,
},
{
name: "FinishedAt After ReconciledAt & ObservedAt",
app: &unstructured.Unstructured{
Object: map[string]any{
"status": map[string]any{
"operationState": map[string]any{
"finishedAt": "2021-01-01T01:05:00Z",
"phase": "Succeeded",
},
"reconciledAt": "2021-01-01T01:02:00Z",
"observedAt": "2021-01-01T01:04:00Z",
},
},
},
expectedValue: false,
},
{
name: "FinishedAt Before ReconciledAt & ObservedAt",
app: &unstructured.Unstructured{
Object: map[string]any{
"status": map[string]any{
"operationState": map[string]any{
"finishedAt": "2021-01-01T01:02:00Z",
"phase": "Succeeded",
},
"reconciledAt": "2021-01-01T01:04:00Z",
"observedAt": "2021-01-01T01:06:00Z",
},
},
},
expectedValue: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actualValue := isAppSyncStatusRefreshed(test.app, logEntry)
assert.Equal(t, test.expectedValue, actualValue)
})
}
}
func TestGetAppProj_invalidProjectNestedString(t *testing.T) {
app := &unstructured.Unstructured{
Object: map[string]any{
"spec": map[string]any{},
},
}
informer := cache.NewSharedIndexInformer(nil, nil, 0, nil)
proj := getAppProj(app, informer)
assert.Nil(t, proj)
}
func TestInit(t *testing.T) {
scheme := runtime.NewScheme()
err := v1alpha1.SchemeBuilder.AddToScheme(scheme)
require.NoErrorf(t, err, "Error registering the resource")
dynamicClient := fake.NewSimpleDynamicClient(scheme)
k8sClient := k8sfake.NewSimpleClientset()
appLabelSelector := "app=test"
selfServiceNotificationEnabledFlags := []bool{false, true}
for _, selfServiceNotificationEnabled := range selfServiceNotificationEnabledFlags {
nc := NewController(
k8sClient,
dynamicClient,
nil,
"default",
[]string{},
appLabelSelector,
nil,
"my-secret",
"my-configmap",
selfServiceNotificationEnabled,
)
assert.NotNil(t, nc)
ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
defer cancel()
err = nc.Init(ctx)
require.NoError(t, err)
}
}
func TestInitTimeout(t *testing.T) {
scheme := runtime.NewScheme()
err := v1alpha1.SchemeBuilder.AddToScheme(scheme)
require.NoErrorf(t, err, "Error registering the resource")
dynamicClient := fake.NewSimpleDynamicClient(scheme)
k8sClient := k8sfake.NewSimpleClientset()
appLabelSelector := "app=test"
nc := NewController(
k8sClient,
dynamicClient,
nil,
"default",
[]string{},
appLabelSelector,
nil,
"my-secret",
"my-configmap",
false,
)
assert.NotNil(t, nc)
// Use a short timeout to simulate a timeout during cache synchronization
ctx, cancel := context.WithTimeout(t.Context(), 1*time.Millisecond)
defer cancel()
err = nc.Init(ctx)
// Expect an error & add assertion for the error message
assert.EqualError(t, err, "timed out waiting for caches to sync")
}
func TestCheckAppNotInAdditionalNamespaces(t *testing.T) {
app := &unstructured.Unstructured{
Object: map[string]any{
"spec": map[string]any{},
},
}
namespace := "cd"
var applicationNamespaces []string
applicationNamespaces = append(applicationNamespaces, "namespace1")
applicationNamespaces = append(applicationNamespaces, "namespace2")
// app is in same namespace as controller's namespace
app.SetNamespace(namespace)
assert.False(t, checkAppNotInAdditionalNamespaces(app, namespace, applicationNamespaces))
// app is not in the namespace as controller's namespace, but it is in one of the applicationNamespaces
app.SetNamespace("namespace2")
assert.False(t, checkAppNotInAdditionalNamespaces(app, "", applicationNamespaces))
// app is not in the namespace as controller's namespace, and it is not in any of the applicationNamespaces
app.SetNamespace("namespace3")
assert.True(t, checkAppNotInAdditionalNamespaces(app, "", applicationNamespaces))
}
-68
View File
@@ -1,68 +0,0 @@
package notification
import (
"context"
"github.com/argoproj/notifications-engine/pkg/api"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"github.com/hanzoai/deploy/pkg/apiclient/notification"
)
// Server provides an Application service
type Server struct {
apiFactory api.Factory
}
// NewServer returns a new instance of the Application service
func NewServer(apiFactory api.Factory) notification.NotificationServiceServer {
s := &Server{apiFactory: apiFactory}
return s
}
// List returns list of notification triggers
func (s *Server) ListTriggers(_ context.Context, _ *notification.TriggersListRequest) (*notification.TriggerList, error) {
api, err := s.apiFactory.GetAPI()
if err != nil {
if apierrors.IsNotFound(err) {
return &notification.TriggerList{}, nil
}
}
triggers := []*notification.Trigger{}
for trigger := range api.GetConfig().Triggers {
triggers = append(triggers, &notification.Trigger{Name: new(trigger)})
}
return &notification.TriggerList{Items: triggers}, nil
}
// List returns list of notification services
func (s *Server) ListServices(_ context.Context, _ *notification.ServicesListRequest) (*notification.ServiceList, error) {
api, err := s.apiFactory.GetAPI()
if err != nil {
if apierrors.IsNotFound(err) {
return &notification.ServiceList{}, nil
}
return nil, err
}
services := []*notification.Service{}
for svc := range api.GetConfig().Services {
services = append(services, &notification.Service{Name: new(svc)})
}
return &notification.ServiceList{Items: services}, nil
}
// List returns list of notification templates
func (s *Server) ListTemplates(_ context.Context, _ *notification.TemplatesListRequest) (*notification.TemplateList, error) {
api, err := s.apiFactory.GetAPI()
if err != nil {
if apierrors.IsNotFound(err) {
return &notification.TemplateList{}, nil
}
return nil, err
}
templates := []*notification.Template{}
for tmpl := range api.GetConfig().Templates {
templates = append(templates, &notification.Template{Name: new(tmpl)})
}
return &notification.TemplateList{Items: templates}, nil
}
-58
View File
@@ -1,58 +0,0 @@
syntax = "proto2";
option go_package = "github.com/hanzoai/deploy/pkg/apiclient/notification";
// Notification Service
//
// Notification Service API performs query actions against notification resources
package notification;
import "google/api/annotations.proto";
message Trigger {
required string name = 1;
}
message TriggerList {
repeated Trigger items = 1;
}
message TriggersListRequest {}
message Service {
required string name = 1;
}
message ServiceList {
repeated Service items = 1;
}
message ServicesListRequest {}
message Template {
required string name = 1;
}
message TemplateList {
repeated Template items = 1;
}
message TemplatesListRequest {}
// NotificationService
service NotificationService {
// List returns list of triggers
rpc ListTriggers(TriggersListRequest) returns (TriggerList) {
option (google.api.http).get = "/api/v1/notifications/triggers";
}
// List returns list of services
rpc ListServices(ServicesListRequest) returns (ServiceList) {
option (google.api.http).get = "/api/v1/notifications/services";
}
// List returns list of templates
rpc ListTemplates(TemplatesListRequest) returns (TemplateList) {
option (google.api.http).get = "/api/v1/notifications/templates";
}
}
-104
View File
@@ -1,104 +0,0 @@
package notification
import (
"os"
"testing"
"github.com/argoproj/notifications-engine/pkg/api"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
"github.com/hanzoai/deploy/pkg/apiclient/notification"
"github.com/hanzoai/deploy/reposerver/apiclient/mocks"
service "github.com/hanzoai/deploy/util/notification/cd"
"github.com/hanzoai/deploy/util/notification/k8s"
"github.com/hanzoai/deploy/util/notification/settings"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
dynamicfake "k8s.io/client-go/dynamic/fake"
"k8s.io/client-go/kubernetes/fake"
k8scache "k8s.io/client-go/tools/cache"
"k8s.io/kubectl/pkg/scheme"
)
const testNamespace = "default"
func TestNotificationServer(t *testing.T) {
t.Parallel()
// catalogPath := path.Join(paths[1], "config", "notifications-catalog")
b, err := os.ReadFile("../../notifications_catalog/install.yaml")
require.NoError(t, err)
cm := &corev1.ConfigMap{}
_, _, err = scheme.Codecs.UniversalDeserializer().Decode(b, nil, cm)
require.NoError(t, err)
cm.Namespace = testNamespace
kubeclientset := fake.NewClientset(&corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Namespace: testNamespace,
Name: "cd-notifications-cm",
},
Data: map[string]string{
"service.webhook.test": "url: https://test.example.com",
"template.app-created": "email:\n subject: Application {{.app.metadata.name}} has been created.\nmessage: Application {{.app.metadata.name}} has been created.\nteams:\n title: Application {{.app.metadata.name}} has been created.\n",
"trigger.on-created": "- description: Application is created.\n oncePer: app.metadata.name\n send:\n - app-created\n when: \"true\"\n",
},
},
&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "cd-notifications-secret",
Namespace: testNamespace,
},
Data: map[string][]byte{},
})
ctx := t.Context()
secretInformer := k8s.NewSecretInformer(kubeclientset, testNamespace, "cd-notifications-secret")
configMapInformer := k8s.NewConfigMapInformer(kubeclientset, testNamespace, "cd-notifications-cm")
go secretInformer.Run(ctx.Done())
if !k8scache.WaitForCacheSync(ctx.Done(), secretInformer.HasSynced) {
panic("Timed out waiting for caches to sync")
}
go configMapInformer.Run(ctx.Done())
if !k8scache.WaitForCacheSync(ctx.Done(), configMapInformer.HasSynced) {
panic("Timed out waiting for caches to sync")
}
mockRepoClient := &mocks.Clientset{RepoServerServiceClient: &mocks.RepoServerServiceClient{}}
dynamicClient := dynamicfake.NewSimpleDynamicClient(runtime.NewScheme())
cdService, err := service.NewArgoCDService(kubeclientset, dynamicClient, testNamespace, mockRepoClient)
require.NoError(t, err)
t.Cleanup(cdService.Close)
apiFactory := api.NewFactory(settings.GetFactorySettings(cdService, "cd-notifications-secret", "cd-notifications-cm", false), testNamespace, secretInformer, configMapInformer)
t.Run("TestListServices", func(t *testing.T) {
t.Parallel()
server := NewServer(apiFactory)
services, err := server.ListServices(ctx, &notification.ServicesListRequest{})
require.NoError(t, err)
assert.Len(t, services.Items, 1)
assert.Equal(t, services.Items[0].Name, new("test"))
assert.NotEmpty(t, services.Items[0])
})
t.Run("TestListTriggers", func(t *testing.T) {
t.Parallel()
server := NewServer(apiFactory)
triggers, err := server.ListTriggers(ctx, &notification.TriggersListRequest{})
require.NoError(t, err)
assert.Len(t, triggers.Items, 1)
assert.Equal(t, triggers.Items[0].Name, new("on-created"))
assert.NotEmpty(t, triggers.Items[0])
})
t.Run("TestListTemplates", func(t *testing.T) {
t.Parallel()
server := NewServer(apiFactory)
templates, err := server.ListTemplates(ctx, &notification.TemplatesListRequest{})
require.NoError(t, err)
assert.Len(t, templates.Items, 1)
assert.Equal(t, templates.Items[0].Name, new("app-created"))
assert.NotEmpty(t, templates.Items[0])
})
}
-20
View File
@@ -27,7 +27,6 @@ import (
"syscall"
"time"
"github.com/argoproj/notifications-engine/pkg/api"
"github.com/hanzoai/deploy/util/vendored/sync"
"github.com/golang-jwt/jwt/v5"
golang_proto "github.com/golang/protobuf/proto" //nolint:staticcheck
@@ -78,7 +77,6 @@ import (
certificatepkg "github.com/hanzoai/deploy/pkg/apiclient/certificate"
clusterpkg "github.com/hanzoai/deploy/pkg/apiclient/cluster"
gpgkeypkg "github.com/hanzoai/deploy/pkg/apiclient/gpgkey"
notificationpkg "github.com/hanzoai/deploy/pkg/apiclient/notification"
projectpkg "github.com/hanzoai/deploy/pkg/apiclient/project"
repocredspkg "github.com/hanzoai/deploy/pkg/apiclient/repocreds"
repositorypkg "github.com/hanzoai/deploy/pkg/apiclient/repository"
@@ -102,7 +100,6 @@ import (
"github.com/hanzoai/deploy/server/gpgkey"
"github.com/hanzoai/deploy/server/logout"
"github.com/hanzoai/deploy/server/metrics"
"github.com/hanzoai/deploy/server/notification"
"github.com/hanzoai/deploy/server/project"
"github.com/hanzoai/deploy/server/rbacpolicy"
"github.com/hanzoai/deploy/server/repocreds"
@@ -124,9 +121,6 @@ import (
"github.com/hanzoai/deploy/util/io/files"
jwtutil "github.com/hanzoai/deploy/util/jwt"
kubeutil "github.com/hanzoai/deploy/util/kube"
service "github.com/hanzoai/deploy/util/notification/cd"
"github.com/hanzoai/deploy/util/notification/k8s"
settings_notif "github.com/hanzoai/deploy/util/notification/settings"
"github.com/hanzoai/deploy/util/oidc"
"github.com/hanzoai/deploy/util/rbac"
"github.com/hanzoai/deploy/util/security"
@@ -210,7 +204,6 @@ type ArgoCDServer struct {
indexData []byte
indexDataErr error
staticAssets http.FileSystem
apiFactory api.Factory
secretInformer cache.SharedIndexInformer
configMapInformer cache.SharedIndexInformer
serviceSet *ArgoCDServiceSet
@@ -371,13 +364,8 @@ func NewServer(ctx context.Context, opts ArgoCDServerOpts, appsetOpts Applicatio
staticFS = utilio.NewComposableFS(staticFS, root.FS())
}
cdService, err := service.NewArgoCDService(opts.KubeClientset, opts.DynamicClientset, opts.Namespace, opts.RepoClientset)
errorsutil.CheckError(err)
secretInformer := k8s.NewSecretInformer(opts.KubeClientset, opts.Namespace, "cd-notifications-secret")
configMapInformer := k8s.NewConfigMapInformer(opts.KubeClientset, opts.Namespace, "cd-notifications-cm")
apiFactory := api.NewFactory(settings_notif.GetFactorySettings(cdService, "cd-notifications-secret", "cd-notifications-cm", false), opts.Namespace, secretInformer, configMapInformer)
dbInstance := db.NewDB(opts.Namespace, settingsMgr, opts.KubeClientset)
logger := log.NewEntry(log.StandardLogger())
@@ -409,9 +397,6 @@ func NewServer(ctx context.Context, opts ArgoCDServerOpts, appsetOpts Applicatio
userStateStorage: userStateStorage,
staticAssets: http.FS(staticFS),
db: dbInstance,
apiFactory: apiFactory,
secretInformer: secretInformer,
configMapInformer: configMapInformer,
extensionManager: em,
Shutdown: noopShutdown,
stopCh: make(chan os.Signal, 1),
@@ -991,7 +976,6 @@ func (server *ArgoCDServer) newGRPCServer(prometheusRegistry *prometheus.Registr
clusterpkg.RegisterClusterServiceServer(grpcS, server.serviceSet.ClusterService)
applicationpkg.RegisterApplicationServiceServer(grpcS, server.serviceSet.ApplicationService)
applicationsetpkg.RegisterApplicationSetServiceServer(grpcS, server.serviceSet.ApplicationSetService)
notificationpkg.RegisterNotificationServiceServer(grpcS, server.serviceSet.NotificationService)
repositorypkg.RegisterRepositoryServiceServer(grpcS, server.serviceSet.RepoService)
repocredspkg.RegisterRepoCredsServiceServer(grpcS, server.serviceSet.RepoCredsService)
sessionpkg.RegisterSessionServiceServer(grpcS, server.serviceSet.SessionService)
@@ -1018,7 +1002,6 @@ type ArgoCDServiceSet struct {
ProjectService *project.Server
SettingsService *settings.Server
AccountService *account.Server
NotificationService notificationpkg.NotificationServiceServer
CertificateService *certificate.Server
GpgkeyService *gpgkey.Server
VersionService *version.Server
@@ -1084,7 +1067,6 @@ func newArgoCDServiceSet(a *ArgoCDServer) *ArgoCDServiceSet {
settingsService := settings.NewServer(a.settingsMgr, a.RepoClientset, a, a.DisableAuth, appsInAnyNamespaceEnabled, a.HydratorEnabled, a.SyncWithReplaceAllowed)
accountService := account.NewServer(a.sessionMgr, a.settingsMgr, a.enf, a.Namespace)
notificationService := notification.NewServer(a.apiFactory)
certificateService := certificate.NewServer(a.db, a.enf)
gpgkeyService := gpgkey.NewServer(a.db, a.enf)
versionService := version.NewServer(a, func() (bool, error) {
@@ -1109,7 +1091,6 @@ func newArgoCDServiceSet(a *ArgoCDServer) *ArgoCDServiceSet {
ProjectService: projectService,
SettingsService: settingsService,
AccountService: accountService,
NotificationService: notificationService,
CertificateService: certificateService,
GpgkeyService: gpgkeyService,
VersionService: versionService,
@@ -1237,7 +1218,6 @@ func (server *ArgoCDServer) newHTTPServer(ctx context.Context, port int, grpcWeb
mustRegisterGWHandler(ctx, clusterpkg.RegisterClusterServiceHandler, gwmux, conn)
mustRegisterGWHandler(ctx, applicationpkg.RegisterApplicationServiceHandler, gwmux, conn)
mustRegisterGWHandler(ctx, applicationsetpkg.RegisterApplicationSetServiceHandler, gwmux, conn)
mustRegisterGWHandler(ctx, notificationpkg.RegisterNotificationServiceHandler, gwmux, conn)
mustRegisterGWHandler(ctx, repositorypkg.RegisterRepositoryServiceHandler, gwmux, conn)
mustRegisterGWHandler(ctx, repocredspkg.RegisterRepoCredsServiceHandler, gwmux, conn)
mustRegisterGWHandler(ctx, sessionpkg.RegisterSessionServiceHandler, gwmux, conn)
-263
View File
@@ -1,263 +0,0 @@
// Code generated by mockery; DO NOT EDIT.
// github.com/vektra/mockery
// template: testify
package mocks
import (
"context"
"github.com/hanzoai/deploy/pkg/apis/application/v1alpha1"
"github.com/hanzoai/deploy/util/notification/expression/shared"
mock "github.com/stretchr/testify/mock"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
// NewService creates a new instance of Service. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewService(t interface {
mock.TestingT
Cleanup(func())
}) *Service {
mock := &Service{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
// Service is an autogenerated mock type for the Service type
type Service struct {
mock.Mock
}
type Service_Expecter struct {
mock *mock.Mock
}
func (_m *Service) EXPECT() *Service_Expecter {
return &Service_Expecter{mock: &_m.Mock}
}
// GetAppDetails provides a mock function for the type Service
func (_mock *Service) GetAppDetails(ctx context.Context, app *v1alpha1.Application) (*shared.AppDetail, error) {
ret := _mock.Called(ctx, app)
if len(ret) == 0 {
panic("no return value specified for GetAppDetails")
}
var r0 *shared.AppDetail
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, *v1alpha1.Application) (*shared.AppDetail, error)); ok {
return returnFunc(ctx, app)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, *v1alpha1.Application) *shared.AppDetail); ok {
r0 = returnFunc(ctx, app)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*shared.AppDetail)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, *v1alpha1.Application) error); ok {
r1 = returnFunc(ctx, app)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Service_GetAppDetails_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAppDetails'
type Service_GetAppDetails_Call struct {
*mock.Call
}
// GetAppDetails is a helper method to define mock.On call
// - ctx context.Context
// - app *v1alpha1.Application
func (_e *Service_Expecter) GetAppDetails(ctx any, app any) *Service_GetAppDetails_Call {
return &Service_GetAppDetails_Call{Call: _e.mock.On("GetAppDetails", ctx, app)}
}
func (_c *Service_GetAppDetails_Call) Run(run func(ctx context.Context, app *v1alpha1.Application)) *Service_GetAppDetails_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 *v1alpha1.Application
if args[1] != nil {
arg1 = args[1].(*v1alpha1.Application)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *Service_GetAppDetails_Call) Return(appDetail *shared.AppDetail, err error) *Service_GetAppDetails_Call {
_c.Call.Return(appDetail, err)
return _c
}
func (_c *Service_GetAppDetails_Call) RunAndReturn(run func(ctx context.Context, app *v1alpha1.Application) (*shared.AppDetail, error)) *Service_GetAppDetails_Call {
_c.Call.Return(run)
return _c
}
// GetAppProject provides a mock function for the type Service
func (_mock *Service) GetAppProject(ctx context.Context, projectName string, namespace string) (*unstructured.Unstructured, error) {
ret := _mock.Called(ctx, projectName, namespace)
if len(ret) == 0 {
panic("no return value specified for GetAppProject")
}
var r0 *unstructured.Unstructured
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) (*unstructured.Unstructured, error)); ok {
return returnFunc(ctx, projectName, namespace)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) *unstructured.Unstructured); ok {
r0 = returnFunc(ctx, projectName, namespace)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*unstructured.Unstructured)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
r1 = returnFunc(ctx, projectName, namespace)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Service_GetAppProject_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAppProject'
type Service_GetAppProject_Call struct {
*mock.Call
}
// GetAppProject is a helper method to define mock.On call
// - ctx context.Context
// - projectName string
// - namespace string
func (_e *Service_Expecter) GetAppProject(ctx any, projectName any, namespace any) *Service_GetAppProject_Call {
return &Service_GetAppProject_Call{Call: _e.mock.On("GetAppProject", ctx, projectName, namespace)}
}
func (_c *Service_GetAppProject_Call) Run(run func(ctx context.Context, projectName string, namespace string)) *Service_GetAppProject_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 string
if args[1] != nil {
arg1 = args[1].(string)
}
var arg2 string
if args[2] != nil {
arg2 = args[2].(string)
}
run(
arg0,
arg1,
arg2,
)
})
return _c
}
func (_c *Service_GetAppProject_Call) Return(unstructured1 *unstructured.Unstructured, err error) *Service_GetAppProject_Call {
_c.Call.Return(unstructured1, err)
return _c
}
func (_c *Service_GetAppProject_Call) RunAndReturn(run func(ctx context.Context, projectName string, namespace string) (*unstructured.Unstructured, error)) *Service_GetAppProject_Call {
_c.Call.Return(run)
return _c
}
// GetCommitMetadata provides a mock function for the type Service
func (_mock *Service) GetCommitMetadata(ctx context.Context, repoURL string, commitSHA string, project string) (*shared.CommitMetadata, error) {
ret := _mock.Called(ctx, repoURL, commitSHA, project)
if len(ret) == 0 {
panic("no return value specified for GetCommitMetadata")
}
var r0 *shared.CommitMetadata
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string) (*shared.CommitMetadata, error)); ok {
return returnFunc(ctx, repoURL, commitSHA, project)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string) *shared.CommitMetadata); ok {
r0 = returnFunc(ctx, repoURL, commitSHA, project)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*shared.CommitMetadata)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, string, string, string) error); ok {
r1 = returnFunc(ctx, repoURL, commitSHA, project)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Service_GetCommitMetadata_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetCommitMetadata'
type Service_GetCommitMetadata_Call struct {
*mock.Call
}
// GetCommitMetadata is a helper method to define mock.On call
// - ctx context.Context
// - repoURL string
// - commitSHA string
// - project string
func (_e *Service_Expecter) GetCommitMetadata(ctx any, repoURL any, commitSHA any, project any) *Service_GetCommitMetadata_Call {
return &Service_GetCommitMetadata_Call{Call: _e.mock.On("GetCommitMetadata", ctx, repoURL, commitSHA, project)}
}
func (_c *Service_GetCommitMetadata_Call) Run(run func(ctx context.Context, repoURL string, commitSHA string, project string)) *Service_GetCommitMetadata_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 string
if args[1] != nil {
arg1 = args[1].(string)
}
var arg2 string
if args[2] != nil {
arg2 = args[2].(string)
}
var arg3 string
if args[3] != nil {
arg3 = args[3].(string)
}
run(
arg0,
arg1,
arg2,
arg3,
)
})
return _c
}
func (_c *Service_GetCommitMetadata_Call) Return(commitMetadata *shared.CommitMetadata, err error) *Service_GetCommitMetadata_Call {
_c.Call.Return(commitMetadata, err)
return _c
}
func (_c *Service_GetCommitMetadata_Call) RunAndReturn(run func(ctx context.Context, repoURL string, commitSHA string, project string) (*shared.CommitMetadata, error)) *Service_GetCommitMetadata_Call {
_c.Call.Return(run)
return _c
}
-144
View File
@@ -1,144 +0,0 @@
package service
import (
"context"
"fmt"
"github.com/hanzoai/deploy/util/notification/expression/shared"
log "github.com/sirupsen/logrus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
"github.com/hanzoai/deploy/pkg/apis/application"
"github.com/hanzoai/deploy/pkg/apis/application/v1alpha1"
"github.com/hanzoai/deploy/reposerver/apiclient"
"github.com/hanzoai/deploy/util/db"
"github.com/hanzoai/deploy/util/settings"
)
type Service interface {
GetCommitMetadata(ctx context.Context, repoURL string, commitSHA string, project string) (*shared.CommitMetadata, error)
GetAppDetails(ctx context.Context, app *v1alpha1.Application) (*shared.AppDetail, error)
GetAppProject(ctx context.Context, projectName string, namespace string) (*unstructured.Unstructured, error)
}
func NewArgoCDService(clientset kubernetes.Interface, dynamicClient dynamic.Interface, namespace string, repoClientset apiclient.Clientset) (*argoCDService, error) {
ctx, cancel := context.WithCancel(context.Background())
settingsMgr := settings.NewSettingsManager(ctx, clientset, namespace)
closer, repoClient, err := repoClientset.NewRepoServerClient()
if err != nil {
cancel()
return nil, err
}
dispose := func() {
cancel()
if err := closer.Close(); err != nil {
log.Warnf("Failed to close repo server connection: %v", err)
}
}
return &argoCDService{clientset: clientset, dynamicClient: dynamicClient, settingsMgr: settingsMgr, namespace: namespace, repoServerClient: repoClient, dispose: dispose}, nil
}
type argoCDService struct {
clientset kubernetes.Interface
dynamicClient dynamic.Interface
namespace string
settingsMgr *settings.SettingsManager
repoServerClient apiclient.RepoServerServiceClient
dispose func()
}
func (svc *argoCDService) GetCommitMetadata(ctx context.Context, repoURL string, commitSHA string, project string) (*shared.CommitMetadata, error) {
cdDB := db.NewDB(svc.namespace, svc.settingsMgr, svc.clientset)
repo, err := cdDB.GetRepository(ctx, repoURL, project)
if err != nil {
return nil, err
}
metadata, err := svc.repoServerClient.GetRevisionMetadata(ctx, &apiclient.RepoServerRevisionMetadataRequest{
Repo: repo,
Revision: commitSHA,
})
if err != nil {
return nil, err
}
return &shared.CommitMetadata{
Message: metadata.Message,
Author: metadata.Author,
Date: metadata.Date.Time,
Tags: metadata.Tags,
}, nil
}
func (svc *argoCDService) GetAppDetails(ctx context.Context, app *v1alpha1.Application) (*shared.AppDetail, error) {
appSource := app.Spec.GetSourcePtrByIndex(0)
cdDB := db.NewDB(svc.namespace, svc.settingsMgr, svc.clientset)
repo, err := cdDB.GetRepository(ctx, appSource.RepoURL, app.Spec.Project)
if err != nil {
return nil, err
}
helmRepos, err := cdDB.ListHelmRepositories(ctx)
if err != nil {
return nil, err
}
kustomizeOptions, err := svc.settingsMgr.GetKustomizeSettings()
if err != nil {
return nil, err
}
helmOptions, err := svc.settingsMgr.GetHelmSettings()
if err != nil {
return nil, err
}
appDetail, err := svc.repoServerClient.GetAppDetails(ctx, &apiclient.RepoServerAppDetailsQuery{
AppName: app.Name,
Repo: repo,
Source: appSource,
Repos: helmRepos,
KustomizeOptions: kustomizeOptions,
HelmOptions: helmOptions,
})
if err != nil {
return nil, err
}
var has *shared.CustomHelmAppSpec
if appDetail.Helm != nil {
has = &shared.CustomHelmAppSpec{
HelmAppSpec: apiclient.HelmAppSpec{
Name: appDetail.Helm.Name,
ValueFiles: appDetail.Helm.ValueFiles,
Parameters: appDetail.Helm.Parameters,
Values: appDetail.Helm.Values,
FileParameters: appDetail.Helm.FileParameters,
},
HelmParameterOverrides: appSource.Helm.Parameters,
}
}
return &shared.AppDetail{
Type: appDetail.Type,
Helm: has,
Kustomize: appDetail.Kustomize,
Directory: appDetail.Directory,
}, nil
}
func (svc *argoCDService) GetAppProject(ctx context.Context, projectName string, namespace string) (*unstructured.Unstructured, error) {
if projectName == "" {
projectName = "default"
}
resource := v1alpha1.AppProjectSchemaGroupVersionKind.GroupVersion().WithResource(application.AppProjectPlural)
obj, err := svc.dynamicClient.Resource(resource).Namespace(namespace).Get(ctx, projectName, metav1.GetOptions{})
if err != nil {
return nil, fmt.Errorf("cannot get application project %w", err)
}
return obj, nil
}
func (svc *argoCDService) Close() {
svc.dispose()
}
-74
View File
@@ -1,74 +0,0 @@
package service
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
dynfake "k8s.io/client-go/dynamic/fake"
"k8s.io/client-go/kubernetes/fake"
"github.com/hanzoai/deploy/pkg/apis/application/v1alpha1"
"github.com/hanzoai/deploy/reposerver/apiclient/mocks"
)
func newTestService(t *testing.T, objects ...runtime.Object) (*argoCDService, *dynfake.FakeDynamicClient) {
t.Helper()
scheme := runtime.NewScheme()
require.NoError(t, v1alpha1.SchemeBuilder.AddToScheme(scheme))
dynamicClient := dynfake.NewSimpleDynamicClient(scheme, objects...)
k8sClient := fake.NewSimpleClientset()
mockRepoClient := &mocks.Clientset{RepoServerServiceClient: &mocks.RepoServerServiceClient{}}
svc, err := NewArgoCDService(k8sClient, dynamicClient, "default", mockRepoClient)
require.NoError(t, err)
t.Cleanup(svc.Close)
return svc, dynamicClient
}
func TestGetAppProject(t *testing.T) {
t.Parallel()
appProject := &v1alpha1.AppProject{
ObjectMeta: metav1.ObjectMeta{
Name: "my-project",
Namespace: "default",
},
Spec: v1alpha1.AppProjectSpec{
Description: "test project",
},
}
t.Run("returns AppProject when found", func(t *testing.T) {
t.Parallel()
svc, _ := newTestService(t, appProject)
result, err := svc.GetAppProject(t.Context(), "my-project", "default")
require.NoError(t, err)
assert.Equal(t, "my-project", result.GetName())
assert.Equal(t, "default", result.GetNamespace())
})
t.Run("defaults to 'default' project when name is empty", func(t *testing.T) {
t.Parallel()
defaultProject := &v1alpha1.AppProject{
ObjectMeta: metav1.ObjectMeta{
Name: "default",
Namespace: "default",
},
}
svc, _ := newTestService(t, defaultProject)
result, err := svc.GetAppProject(t.Context(), "", "default")
require.NoError(t, err)
assert.Equal(t, "default", result.GetName())
})
t.Run("returns error when AppProject not found", func(t *testing.T) {
t.Parallel()
svc, _ := newTestService(t)
result, err := svc.GetAppProject(t.Context(), "nonexistent", "default")
require.Error(t, err)
assert.Nil(t, result)
})
}
-36
View File
@@ -1,36 +0,0 @@
package expression
import (
"maps"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
service "github.com/hanzoai/deploy/util/notification/cd"
"github.com/hanzoai/deploy/util/notification/expression/repo"
"github.com/hanzoai/deploy/util/notification/expression/strings"
"github.com/hanzoai/deploy/util/notification/expression/time"
)
var helpers = map[string]any{}
func init() {
helpers = make(map[string]any)
register("time", time.NewExprs())
register("strings", strings.NewExprs())
}
func register(namespace string, entry map[string]any) {
helpers[namespace] = entry
}
func Spawn(app *unstructured.Unstructured, cdService service.Service, vars map[string]any) map[string]any {
clone := make(map[string]any)
for k := range vars {
clone[k] = vars[k]
}
maps.Copy(clone, helpers)
clone["repo"] = repo.NewExprs(cdService, app)
return clone
}
-22
View File
@@ -1,22 +0,0 @@
package expression
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestExpr(t *testing.T) {
t.Parallel()
namespaces := []string{
"time",
"repo",
"strings",
}
for _, ns := range namespaces {
helpers := Spawn(nil, nil, nil)
_, hasNamespace := helpers[ns]
assert.True(t, hasNamespace)
}
}
-118
View File
@@ -1,118 +0,0 @@
package repo
import (
"context"
"encoding/json"
"errors"
"net/url"
"regexp"
"strings"
service "github.com/hanzoai/deploy/util/notification/cd"
"github.com/hanzoai/deploy/util/notification/expression/shared"
"github.com/argoproj/notifications-engine/pkg/util/text"
giturls "github.com/chainguard-dev/git-urls"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/hanzoai/deploy/pkg/apis/application/v1alpha1"
)
var gitSuffix = regexp.MustCompile(`\.git$`)
func getApplication(obj *unstructured.Unstructured) (*v1alpha1.Application, error) {
data, err := json.Marshal(obj)
if err != nil {
return nil, err
}
application := &v1alpha1.Application{}
err = json.Unmarshal(data, application)
if err != nil {
return nil, err
}
return application, nil
}
func getAppDetails(un *unstructured.Unstructured, cdService service.Service) (*shared.AppDetail, error) {
app, err := getApplication(un)
if err != nil {
return nil, err
}
appDetail, err := cdService.GetAppDetails(context.Background(), app)
if err != nil {
return nil, err
}
return appDetail, nil
}
func getCommitMetadata(commitSHA string, app *unstructured.Unstructured, cdService service.Service) (*shared.CommitMetadata, error) {
repoURL, ok, err := unstructured.NestedString(app.Object, "spec", "source", "repoURL")
if err != nil {
return nil, err
}
if !ok {
panic(errors.New("failed to get application source repo URL"))
}
project, ok, err := unstructured.NestedString(app.Object, "spec", "project")
if err != nil {
return nil, err
}
if !ok {
panic(errors.New("failed to get application project"))
}
meta, err := cdService.GetCommitMetadata(context.Background(), repoURL, commitSHA, project)
if err != nil {
return nil, err
}
return meta, nil
}
func FullNameByRepoURL(rawURL string) string {
parsed, err := giturls.Parse(rawURL)
if err != nil {
panic(err)
}
path := gitSuffix.ReplaceAllString(parsed.Path, "")
if pathParts := text.SplitRemoveEmpty(path, "/"); len(pathParts) >= 2 {
return strings.Join(pathParts[:2], "/")
}
return path
}
func repoURLToHTTPS(rawURL string) string {
parsed, err := giturls.Parse(rawURL)
if err != nil {
panic(err)
}
parsed.Scheme = "https"
parsed.User = nil
return parsed.String()
}
func NewExprs(cdService service.Service, app *unstructured.Unstructured) map[string]any {
return map[string]any{
"RepoURLToHTTPS": repoURLToHTTPS,
"FullNameByRepoURL": FullNameByRepoURL,
"QueryEscape": url.QueryEscape,
"GetCommitMetadata": func(commitSHA string) any {
meta, err := getCommitMetadata(commitSHA, app, cdService)
if err != nil {
panic(err)
}
return *meta
},
"GetAppDetails": func() any {
appDetails, err := getAppDetails(app, cdService)
if err != nil {
panic(err)
}
return *appDetails
},
}
}
@@ -1,63 +0,0 @@
package shared
import (
"time"
"github.com/hanzoai/deploy/pkg/apis/application/v1alpha1"
"github.com/hanzoai/deploy/reposerver/apiclient"
)
type CommitMetadata struct {
// Commit message
Message string
// Commit author
Author string
// Commit creation date
Date time.Time
// Associated tags
Tags []string
}
type AppDetail struct {
// AppDetail Type
Type string
// Helm details
Helm *CustomHelmAppSpec
// Kustomize details
Kustomize *apiclient.KustomizeAppSpec
// Directory details
Directory *apiclient.DirectoryAppSpec
}
type CustomHelmAppSpec struct {
HelmAppSpec apiclient.HelmAppSpec
HelmParameterOverrides []v1alpha1.HelmParameter
}
func (has CustomHelmAppSpec) GetParameterValueByName(name string) string {
// Check in overrides first
for i := range has.HelmParameterOverrides {
if has.HelmParameterOverrides[i].Name == name {
return has.HelmParameterOverrides[i].Value
}
}
for i := range has.HelmAppSpec.Parameters {
if has.HelmAppSpec.Parameters[i].Name == name {
return has.HelmAppSpec.Parameters[i].Value
}
}
return ""
}
func (has CustomHelmAppSpec) GetFileParameterPathByName(name string) string {
var path string
for i := range has.HelmAppSpec.FileParameters {
if has.HelmAppSpec.FileParameters[i].Name == name {
path = has.HelmAppSpec.FileParameters[i].Path
break
}
}
return path
}
@@ -1,33 +0,0 @@
package shared
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/hanzoai/deploy/pkg/apis/application/v1alpha1"
"github.com/hanzoai/deploy/reposerver/apiclient"
)
func TestGetParameterValueByName(t *testing.T) {
t.Parallel()
helmAppSpec := CustomHelmAppSpec{
HelmAppSpec: apiclient.HelmAppSpec{
Parameters: []*v1alpha1.HelmParameter{
{
Name: "param1",
Value: "value1",
},
},
},
HelmParameterOverrides: []v1alpha1.HelmParameter{
{
Name: "param1",
Value: "value-override",
},
},
}
value := helmAppSpec.GetParameterValueByName("param1")
assert.Equal(t, "value-override", value)
}
@@ -1,25 +0,0 @@
package strings
import (
"strings"
)
func NewExprs() map[string]any {
return map[string]any{
"ReplaceAll": replaceAll,
"ToUpper": toUpper,
"ToLower": toLower,
}
}
func replaceAll(s, old, newV string) string {
return strings.ReplaceAll(s, old, newV)
}
func toUpper(s string) string {
return strings.ToUpper(s)
}
func toLower(s string) string {
return strings.ToLower(s)
}
@@ -1,64 +0,0 @@
package strings
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewExprs(t *testing.T) {
t.Parallel()
funcs := []string{
"ReplaceAll",
"ToUpper",
"ToLower",
}
for _, fn := range funcs {
stringsExprs := NewExprs()
_, hasFunc := stringsExprs[fn]
assert.True(t, hasFunc)
}
}
func TestReplaceAll(t *testing.T) {
t.Parallel()
exprs := NewExprs()
input := "test_replace"
expected := "test=replace"
replaceAllFn, ok := exprs["ReplaceAll"].(func(s, old, new string) string)
assert.True(t, ok)
actual := replaceAllFn(input, "_", "=")
assert.Equal(t, expected, actual)
}
func TestUpperAndLower(t *testing.T) {
t.Parallel()
testCases := []struct {
fn string
input string
expected string
}{
{
fn: "ToUpper",
input: "test",
expected: "TEST",
},
{
fn: "ToLower",
input: "TEST",
expected: "test",
},
}
exprs := NewExprs()
for _, testCase := range testCases {
t.Run("With success case: Func: "+testCase.fn, func(t *testing.T) {
t.Parallel()
toUpperFn, ok := exprs[testCase.fn].(func(s string) string)
assert.True(t, ok)
actual := toUpperFn(testCase.input)
assert.Equal(t, testCase.expected, actual)
})
}
}
-47
View File
@@ -1,47 +0,0 @@
package time
import (
"time"
)
var now = time.Now
func NewExprs() map[string]any {
return map[string]any{
// Functions
"Parse": parse,
"Now": now,
// Durations
"Nanosecond": time.Nanosecond,
"Microsecond": time.Microsecond,
"Millisecond": time.Millisecond,
"Second": time.Second,
"Minute": time.Minute,
"Hour": time.Hour,
// Timestamps
"Layout": time.Layout,
"ANSIC": time.ANSIC,
"UnixDate": time.UnixDate,
"RubyDate": time.RubyDate,
"RFC822": time.RFC822,
"RFC822Z": time.RFC822Z,
"RFC850": time.RFC850,
"RFC1123": time.RFC1123,
"RFC1123Z": time.RFC1123Z,
"RFC3339": time.RFC3339,
"RFC3339Nano": time.RFC3339Nano,
"Kitchen": time.Kitchen,
"Stamp": time.Stamp,
"StampMilli": time.StampMilli,
"StampMicro": time.StampMicro,
"StampNano": time.StampNano,
}
}
func parse(timestamp string) time.Time {
res, err := time.Parse(time.RFC3339, timestamp)
if err != nil {
panic(err)
}
return res
}
@@ -1,62 +0,0 @@
package time
import (
"testing"
"time"
"github.com/expr-lang/expr"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewTimeExprs(t *testing.T) {
t.Parallel()
funcs := []string{
"Parse",
"Now",
"Nanosecond",
"Microsecond",
"Millisecond",
"Second",
"Minute",
"Hour",
"Layout",
"ANSIC",
"UnixDate",
"RubyDate",
"RFC822",
"RFC822Z",
"RFC850",
"RFC1123",
"RFC1123Z",
"RFC3339",
"RFC3339Nano",
"Kitchen",
"Stamp",
"StampMilli",
"StampMicro",
"StampNano",
}
for _, fn := range funcs {
timeExprs := NewExprs()
_, exists := timeExprs[fn]
assert.True(t, exists)
}
}
func Test_NewExprs_Now(t *testing.T) {
defer func() { now = time.Now }()
fixedTime := time.Date(2022, 9, 26, 11, 30, 25, 0, time.UTC)
now = func() time.Time {
return fixedTime
}
vm, err := expr.Compile("time.Now().Truncate(time.Hour).Format(time.RFC3339)")
require.NoError(t, err)
val, err := expr.Run(vm, map[string]any{"time": NewExprs()})
require.NoError(t, err)
assert.Equal(t, "2022-09-26T11:00:00Z", val)
}
-26
View File
@@ -1,26 +0,0 @@
package k8s
import (
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
informersv1 "k8s.io/client-go/informers/core/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/cache"
)
const (
settingsResyncDuration = 3 * time.Minute
)
func NewSecretInformer(clientset kubernetes.Interface, namespace string, secretName string) cache.SharedIndexInformer {
return informersv1.NewFilteredSecretInformer(clientset, namespace, settingsResyncDuration, cache.Indexers{}, func(options *metav1.ListOptions) {
options.FieldSelector = "metadata.name=" + secretName
})
}
func NewConfigMapInformer(clientset kubernetes.Interface, namespace string, configMapName string) cache.SharedIndexInformer {
return informersv1.NewFilteredConfigMapInformer(clientset, namespace, settingsResyncDuration, cache.Indexers{}, func(options *metav1.ListOptions) {
options.FieldSelector = "metadata.name=" + configMapName
})
}
-235
View File
@@ -1,235 +0,0 @@
package settings
import (
"encoding/json"
"fmt"
"maps"
"strings"
"github.com/argoproj/notifications-engine/pkg/api"
"github.com/argoproj/notifications-engine/pkg/services"
"github.com/argoproj/notifications-engine/pkg/subscriptions"
"github.com/argoproj/notifications-engine/pkg/triggers"
"github.com/argoproj/notifications-engine/pkg/util/text"
jsonpatch "github.com/evanphx/json-patch"
log "github.com/sirupsen/logrus"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/yaml"
)
type legacyTemplate struct {
Name string `json:"name,omitempty"`
Title string `json:"subject,omitempty"`
Body string `json:"body,omitempty"`
services.Notification
}
type legacyTrigger struct {
Name string `json:"name,omitempty"`
Condition string `json:"condition,omitempty"`
Description string `json:"description,omitempty"`
Template string `json:"template,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
}
type legacyConfig struct {
Triggers []legacyTrigger `json:"triggers,omitempty"`
Templates []legacyTemplate `json:"templates,omitempty"`
Context map[string]string `json:"context,omitempty"`
Subscriptions subscriptions.DefaultSubscriptions `json:"subscriptions,omitempty"`
}
type legacyWebhookOptions struct {
services.WebhookOptions
Name string `json:"name"`
}
type legacyServicesConfig struct {
Email *services.EmailOptions `json:"email"`
Slack *services.SlackOptions `json:"slack"`
Opsgenie *services.OpsgenieOptions `json:"opsgenie"`
Grafana *services.GrafanaOptions `json:"grafana"`
Webhook []legacyWebhookOptions `json:"webhook"`
}
func mergePatch(orig any, other any) error {
origData, err := json.Marshal(orig)
if err != nil {
return fmt.Errorf("error marshaling json for original: %w", err)
}
otherData, err := json.Marshal(other)
if err != nil {
return fmt.Errorf("error marshaling json for patch: %w", err)
}
if string(otherData) == "null" {
return nil
}
mergedData, err := jsonpatch.MergePatch(origData, otherData)
if err != nil {
return fmt.Errorf("error creating merge patch: %w", err)
}
return json.Unmarshal(mergedData, orig)
}
func (legacy legacyConfig) merge(cfg *api.Config, context map[string]string) error {
if err := mergePatch(&context, &legacy.Context); err != nil {
return fmt.Errorf("error in merge: %w", err)
}
if err := mergePatch(&cfg.Subscriptions, &legacy.Subscriptions); err != nil {
return fmt.Errorf("error in merge config and legacy subscriptions: %w", err)
}
for _, template := range legacy.Templates {
t, ok := cfg.Templates[template.Name]
if ok {
if err := mergePatch(&t, &template.Notification); err != nil {
return err
}
}
if template.Title != "" {
if template.Email == nil {
template.Email = &services.EmailNotification{}
}
template.Email.Subject = template.Title
}
if template.Body != "" {
template.Message = template.Body
}
cfg.Templates[template.Name] = template.Notification
}
for _, trigger := range legacy.Triggers {
if trigger.Enabled != nil && *trigger.Enabled {
cfg.DefaultTriggers = append(cfg.DefaultTriggers, trigger.Name)
}
var firstCondition triggers.Condition
t, ok := cfg.Triggers[trigger.Name]
if !ok || len(t) == 0 {
t = []triggers.Condition{firstCondition}
} else {
firstCondition = t[0]
}
if trigger.Condition != "" {
firstCondition.When = trigger.Condition
}
if trigger.Template != "" {
firstCondition.Send = []string{trigger.Template}
}
if trigger.Description != "" {
firstCondition.Description = trigger.Description
}
t[0] = firstCondition
cfg.Triggers[trigger.Name] = t
}
return nil
}
func (c *legacyServicesConfig) merge(cfg *api.Config) {
if c.Email != nil {
cfg.Services["email"] = func() (services.NotificationService, error) {
return services.NewEmailService(*c.Email), nil
}
}
if c.Slack != nil {
cfg.Services["slack"] = func() (services.NotificationService, error) {
return services.NewSlackService(*c.Slack), nil
}
}
if c.Grafana != nil {
cfg.Services["grafana"] = func() (services.NotificationService, error) {
return services.NewGrafanaService(*c.Grafana), nil
}
}
if c.Opsgenie != nil {
cfg.Services["opsgenie"] = func() (services.NotificationService, error) {
return services.NewOpsgenieService(*c.Opsgenie), nil
}
}
for i := range c.Webhook {
opts := c.Webhook[i]
cfg.Services[opts.Name] = func() (services.NotificationService, error) {
return services.NewWebhookService(opts.WebhookOptions), nil
}
}
}
// ApplyLegacyConfig settings specified using deprecated config map and secret keys
func ApplyLegacyConfig(cfg *api.Config, context map[string]string, cm *corev1.ConfigMap, secret *corev1.Secret) error {
if notifiersData, ok := secret.Data["notifiers.yaml"]; ok && len(notifiersData) > 0 {
log.Warn("Key 'notifiers.yaml' in Secret is deprecated, please migrate to new settings")
legacyServices := &legacyServicesConfig{}
err := yaml.Unmarshal(notifiersData, legacyServices)
if err != nil {
return fmt.Errorf("error unmarshaling legacy services config: %w", err)
}
legacyServices.merge(cfg)
}
if configData, ok := cm.Data["config.yaml"]; ok && configData != "" {
log.Warn("Key 'config.yaml' in ConfigMap is deprecated, please migrate to new settings")
legacyCfg := &legacyConfig{}
err := yaml.Unmarshal([]byte(configData), legacyCfg)
if err != nil {
return fmt.Errorf("error in unmarshaling legacy config: %w", err)
}
err = legacyCfg.merge(cfg, context)
if err != nil {
return fmt.Errorf("error in ApplyLegacyConfig merging config map: %w", err)
}
}
return nil
}
const (
annotationKey = "recipients.cd-notifications.argoproj.io"
)
func GetLegacyDestinations(annotations map[string]string, defaultTriggers []string, serviceDefaultTriggers map[string][]string) services.Destinations {
dests := services.Destinations{}
for k, v := range annotations {
if !strings.HasSuffix(k, annotationKey) {
continue
}
var triggerNames []string
triggerName := strings.TrimRight(k[0:len(k)-len(annotationKey)], ".")
if triggerName == "" {
triggerNames = defaultTriggers
} else {
triggerNames = []string{triggerName}
}
for _, recipient := range text.SplitRemoveEmpty(v, ",") {
if recipient = strings.TrimSpace(recipient); recipient == "" {
continue
}
parts := strings.Split(recipient, ":")
dest := services.Destination{Service: parts[0]}
if len(parts) > 1 {
dest.Recipient = parts[1]
}
t := triggerNames
if v, ok := serviceDefaultTriggers[dest.Service]; ok {
t = v
}
for _, name := range t {
dests[name] = append(dests[name], dest)
}
}
}
return dests
}
// injectLegacyVar injects legacy variable into context
func injectLegacyVar(ctx map[string]string, serviceType string) map[string]string {
res := map[string]string{
"notificationType": serviceType,
}
maps.Copy(res, ctx)
return res
}
-162
View File
@@ -1,162 +0,0 @@
package settings
import (
"testing"
"github.com/argoproj/notifications-engine/pkg/api"
"github.com/argoproj/notifications-engine/pkg/services"
"github.com/argoproj/notifications-engine/pkg/subscriptions"
"github.com/argoproj/notifications-engine/pkg/triggers"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/labels"
)
func TestMergeLegacyConfig_DefaultTriggers(t *testing.T) {
t.Parallel()
cfg := api.Config{
Services: map[string]api.ServiceFactory{},
Triggers: map[string][]triggers.Condition{
"my-trigger1": {{
When: "true",
Send: []string{"my-template1"},
}},
"my-trigger2": {{
When: "false",
Send: []string{"my-template2"},
}},
},
}
context := map[string]string{}
configYAML := `
config.yaml:
triggers:
- name: my-trigger1
enabled: true
`
err := ApplyLegacyConfig(&cfg,
context,
&corev1.ConfigMap{Data: map[string]string{"config.yaml": configYAML}},
&corev1.Secret{Data: map[string][]byte{}},
)
require.NoError(t, err)
assert.Equal(t, []string{"my-trigger1"}, cfg.DefaultTriggers)
}
func TestMergeLegacyConfig(t *testing.T) {
t.Parallel()
cfg := api.Config{
Templates: map[string]services.Notification{"my-template1": {Message: "foo"}},
Triggers: map[string][]triggers.Condition{
"my-trigger1": {{
When: "true",
Send: []string{"my-template1"},
}},
},
Services: map[string]api.ServiceFactory{},
Subscriptions: []subscriptions.DefaultSubscription{{Triggers: []string{"my-trigger1"}}},
}
context := map[string]string{"some": "value"}
configYAML := `
triggers:
- name: my-trigger1
enabled: true
- name: my-trigger2
condition: false
template: my-template2
enabled: true
templates:
- name: my-template1
body: bar
- name: my-template2
body: foo
context:
other: value2
subscriptions:
- triggers:
- my-trigger2
selector: test=true
`
notifiersYAML := `
slack:
token: my-token
`
err := ApplyLegacyConfig(&cfg, context,
&corev1.ConfigMap{Data: map[string]string{"config.yaml": configYAML}},
&corev1.Secret{Data: map[string][]byte{"notifiers.yaml": []byte(notifiersYAML)}},
)
require.NoError(t, err)
assert.Equal(t, map[string]services.Notification{
"my-template1": {Message: "bar"},
"my-template2": {Message: "foo"},
}, cfg.Templates)
assert.Equal(t, []triggers.Condition{{
When: "true",
Send: []string{"my-template1"},
}}, cfg.Triggers["my-trigger1"])
assert.Equal(t, []triggers.Condition{{
When: "false",
Send: []string{"my-template2"},
}}, cfg.Triggers["my-trigger2"])
label, err := labels.Parse("test=true")
require.NoError(t, err)
assert.Equal(t, subscriptions.DefaultSubscriptions([]subscriptions.DefaultSubscription{
{Triggers: []string{"my-trigger2"}, Selector: label},
}), cfg.Subscriptions)
assert.NotNil(t, cfg.Services["slack"])
}
func TestGetDestinations(t *testing.T) {
t.Parallel()
res := GetLegacyDestinations(map[string]string{
"my-trigger.recipients.cd-notifications.argoproj.io": "slack:my-channel",
}, []string{}, nil)
assert.Equal(t, services.Destinations{
"my-trigger": []services.Destination{
{
Recipient: "my-channel",
Service: "slack",
},
},
}, res)
}
func TestGetDestinations_DefaultTrigger(t *testing.T) {
t.Parallel()
res := GetLegacyDestinations(map[string]string{
annotationKey: "slack:my-channel",
}, []string{"my-trigger"}, nil)
assert.Equal(t, services.Destinations{
"my-trigger": []services.Destination{{
Recipient: "my-channel",
Service: "slack",
}},
}, res)
}
func TestGetDestinations_ServiceDefaultTriggers(t *testing.T) {
t.Parallel()
res := GetLegacyDestinations(map[string]string{
annotationKey: "slack:my-channel",
}, []string{}, map[string][]string{
"slack": {
"trigger-a",
"trigger-b",
},
})
assert.Equal(t, services.Destinations{
"trigger-a": []services.Destination{{
Recipient: "my-channel",
Service: "slack",
}},
"trigger-b": []services.Destination{{
Recipient: "my-channel",
Service: "slack",
}},
}, res)
}
-155
View File
@@ -1,155 +0,0 @@
package settings
import (
"context"
"errors"
"time"
"github.com/argoproj/notifications-engine/pkg/api"
"github.com/argoproj/notifications-engine/pkg/services"
log "github.com/sirupsen/logrus"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"sigs.k8s.io/yaml"
"github.com/hanzoai/deploy/util/notification/expression"
service "github.com/hanzoai/deploy/util/notification/cd"
)
func GetFactorySettings(cdService service.Service, secretName, configMapName string, selfServiceNotificationEnabled bool) api.Settings {
return api.Settings{
SecretName: secretName,
ConfigMapName: configMapName,
InitGetVars: func(cfg *api.Config, configMap *corev1.ConfigMap, secret *corev1.Secret) (api.GetVars, error) {
if selfServiceNotificationEnabled {
return initGetVarsWithoutSecret(cdService, cfg, configMap, secret)
}
return initGetVars(cdService, cfg, configMap, secret)
},
}
}
// GetFactorySettingsForCLI allows the initialization of cdService to be deferred until it is used, when InitGetVars is called.
func GetFactorySettingsForCLI(serviceGetter func() service.Service, secretName, configMapName string, selfServiceNotificationEnabled bool) api.Settings {
return api.Settings{
SecretName: secretName,
ConfigMapName: configMapName,
InitGetVars: func(cfg *api.Config, configMap *corev1.ConfigMap, secret *corev1.Secret) (api.GetVars, error) {
cdService := serviceGetter()
if cdService == nil {
return nil, errors.New("cdService is not initialized")
}
if selfServiceNotificationEnabled {
return initGetVarsWithoutSecret(cdService, cfg, configMap, secret)
}
return initGetVars(cdService, cfg, configMap, secret)
},
}
}
func getContext(cfg *api.Config, configMap *corev1.ConfigMap, secret *corev1.Secret) (map[string]string, error) {
context := map[string]string{}
if contextYaml, ok := configMap.Data["context"]; ok {
if err := yaml.Unmarshal([]byte(contextYaml), &context); err != nil {
return nil, err
}
}
if err := ApplyLegacyConfig(cfg, context, configMap, secret); err != nil {
return nil, err
}
return context, nil
}
func initGetVarsWithoutSecret(cdService service.Service, cfg *api.Config, configMap *corev1.ConfigMap, secret *corev1.Secret) (api.GetVars, error) {
context, err := getContext(cfg, configMap, secret)
if err != nil {
return nil, err
}
return func(obj map[string]any, dest services.Destination) map[string]any {
vars := map[string]any{
"app": obj,
"context": injectLegacyVar(context, dest.Service),
}
// Add AppProject to template variables
if appProject := getAppProjectForTemplate(cdService, obj); appProject != nil {
vars["appProject"] = appProject
} else {
vars["appProject"] = map[string]any{}
}
return expression.Spawn(&unstructured.Unstructured{Object: obj}, cdService, vars)
}, nil
}
func initGetVars(cdService service.Service, cfg *api.Config, configMap *corev1.ConfigMap, secret *corev1.Secret) (api.GetVars, error) {
context, err := getContext(cfg, configMap, secret)
if err != nil {
return nil, err
}
return func(obj map[string]any, dest services.Destination) map[string]any {
vars := map[string]any{
"app": obj,
"context": injectLegacyVar(context, dest.Service),
"secrets": secret.Data,
}
// Add AppProject to template variables
if appProject := getAppProjectForTemplate(cdService, obj); appProject != nil {
vars["appProject"] = appProject
} else {
vars["appProject"] = map[string]any{}
}
return expression.Spawn(&unstructured.Unstructured{Object: obj}, cdService, vars)
}, nil
}
// getAppProjectForTemplate retrieves the AppProject as an unstructured object for an Application object
// Returns nil if the project cannot be found or an error occurs
func getAppProjectForTemplate(cdService service.Service, obj map[string]any) map[string]any {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Extract project name from app.spec.project
spec, ok := obj["spec"].(map[string]any)
if !ok {
return nil
}
projectName, ok := spec["project"].(string)
if !ok || projectName == "" {
projectName = "default"
}
// Extract namespace from app.metadata.namespace
metadata, ok := obj["metadata"].(map[string]any)
if !ok {
return nil
}
namespace, ok := metadata["namespace"].(string)
if !ok || namespace == "" {
return nil
}
// Extract app name for logging context
appName, _ := metadata["name"].(string)
// Fetch the AppProject
appProjectObj, err := cdService.GetAppProject(ctx, projectName, namespace)
if err != nil {
log.WithFields(log.Fields{
"app": appName,
"project": projectName,
"namespace": namespace,
}).Warnf("Failed to get AppProject for notification template: %v", err)
return nil
}
return appProjectObj.Object
}
-188
View File
@@ -1,188 +0,0 @@
package settings
import (
"fmt"
"testing"
"github.com/argoproj/notifications-engine/pkg/api"
"github.com/argoproj/notifications-engine/pkg/services"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
dynamicfake "k8s.io/client-go/dynamic/fake"
"k8s.io/client-go/kubernetes/fake"
"github.com/hanzoai/deploy/pkg/apis/application/v1alpha1"
"github.com/hanzoai/deploy/reposerver/apiclient/mocks"
service "github.com/hanzoai/deploy/util/notification/cd"
)
const (
testNamespace = "default"
testContextKey = "test-context-key"
testContextKeyValue = "test-context-key-value"
)
func TestInitGetVars(t *testing.T) {
t.Parallel()
notificationsCm := corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Namespace: testNamespace,
Name: "cd-notifications-cm",
},
Data: map[string]string{
"context": fmt.Sprintf("%s: %s", testContextKey, testContextKeyValue),
"service.webhook.test": "url: https://test.example.com",
"template.app-created": "email:\n subject: Application {{.app.metadata.name}} has been created.\nmessage: Application {{.app.metadata.name}} has been created.\nteams:\n title: Application {{.app.metadata.name}} has been created.\n",
"trigger.on-created": "- description: Application is created.\n oncePer: app.metadata.name\n send:\n - app-created\n when: \"true\"\n",
},
}
notificationsSecret := corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "cd-notifications-secret",
Namespace: testNamespace,
},
Data: map[string][]byte{
"notification-secret": []byte("secret-value"),
},
}
kubeclientset := fake.NewClientset(&corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Namespace: testNamespace,
Name: "cd-notifications-cm",
},
Data: notificationsCm.Data,
},
&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "cd-notifications-secret",
Namespace: testNamespace,
},
Data: notificationsSecret.Data,
})
mockRepoClient := &mocks.Clientset{RepoServerServiceClient: &mocks.RepoServerServiceClient{}}
dynamicClient := dynamicfake.NewSimpleDynamicClient(runtime.NewScheme())
cdService, err := service.NewArgoCDService(kubeclientset, dynamicClient, testNamespace, mockRepoClient)
require.NoError(t, err)
t.Cleanup(cdService.Close)
config := api.Config{}
testDestination := services.Destination{
Service: "webhook",
}
emptyAppData := map[string]any{}
varsProvider, _ := initGetVars(cdService, &config, &notificationsCm, &notificationsSecret)
t.Run("Vars provider serves Application data on app key", func(t *testing.T) {
t.Parallel()
appData := map[string]any{
"name": "app-name",
}
result := varsProvider(appData, testDestination)
assert.NotNil(t, t, result["app"])
assert.Equal(t, result["app"], appData)
})
t.Run("Vars provider serves notification context data on context key", func(t *testing.T) {
t.Parallel()
expectedContext := map[string]string{
testContextKey: testContextKeyValue,
"notificationType": testDestination.Service,
}
result := varsProvider(emptyAppData, testDestination)
assert.NotNil(t, result["context"])
assert.Equal(t, expectedContext, result["context"])
})
t.Run("Vars provider serves notification secrets on secrets key", func(t *testing.T) {
t.Parallel()
result := varsProvider(emptyAppData, testDestination)
assert.NotNil(t, result["secrets"])
assert.Equal(t, result["secrets"], notificationsSecret.Data)
})
t.Run("Vars provider serves empty appProject when AppProject not found", func(t *testing.T) {
t.Parallel()
appData := map[string]any{
"spec": map[string]any{
"project": "nonexistent-project",
},
"metadata": map[string]any{
"namespace": testNamespace,
"name": "my-app",
},
}
result := varsProvider(appData, testDestination)
assert.NotNil(t, result["appProject"])
assert.Equal(t, map[string]any{}, result["appProject"])
})
}
func TestInitGetVarsAppProject(t *testing.T) {
t.Parallel()
notificationsCm := corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Namespace: testNamespace,
Name: "cd-notifications-cm",
},
Data: map[string]string{},
}
notificationsSecret := corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "cd-notifications-secret",
Namespace: testNamespace,
},
}
kubeclientset := fake.NewClientset(&notificationsCm, &notificationsSecret)
mockRepoClient := &mocks.Clientset{RepoServerServiceClient: &mocks.RepoServerServiceClient{}}
scheme := runtime.NewScheme()
require.NoError(t, v1alpha1.SchemeBuilder.AddToScheme(scheme))
appProject := &v1alpha1.AppProject{
ObjectMeta: metav1.ObjectMeta{
Name: "my-project",
Namespace: testNamespace,
},
Spec: v1alpha1.AppProjectSpec{
Description: "test project description",
},
}
dynamicClient := dynamicfake.NewSimpleDynamicClient(scheme, appProject)
cdService, err := service.NewArgoCDService(kubeclientset, dynamicClient, testNamespace, mockRepoClient)
require.NoError(t, err)
t.Cleanup(cdService.Close)
config := api.Config{}
testDestination := services.Destination{Service: "webhook"}
varsProvider, _ := initGetVars(cdService, &config, &notificationsCm, &notificationsSecret)
appData := map[string]any{
"spec": map[string]any{
"project": "my-project",
},
"metadata": map[string]any{
"namespace": testNamespace,
"name": "my-app",
},
}
t.Run("Vars provider serves AppProject data on appProject key", func(t *testing.T) {
t.Parallel()
result := varsProvider(appData, testDestination)
assert.NotNil(t, result["appProject"])
proj, ok := result["appProject"].(map[string]any)
require.True(t, ok)
projMeta, ok := proj["metadata"].(map[string]any)
require.True(t, ok)
assert.Equal(t, "my-project", projMeta["name"])
assert.Equal(t, testNamespace, projMeta["namespace"])
})
t.Run("Vars provider appProject key is always present", func(t *testing.T) {
// Even with empty app data, appProject key always be set
t.Parallel()
result := varsProvider(map[string]any{}, testDestination)
_, exists := result["appProject"]
assert.True(t, exists)
})
}