mirror of
https://github.com/hanzoai/deploy.git
synced 2026-10-07 08:44:50 +00:00
merge: delete the notification subsystem, argoproj deps now zero
Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -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)
|
||||
}
|
||||
@@ -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())
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 := ¬ificationController{
|
||||
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))
|
||||
}
|
||||
@@ -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 ¬ification.TriggerList{}, nil
|
||||
}
|
||||
}
|
||||
triggers := []*notification.Trigger{}
|
||||
for trigger := range api.GetConfig().Triggers {
|
||||
triggers = append(triggers, ¬ification.Trigger{Name: new(trigger)})
|
||||
}
|
||||
return ¬ification.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 ¬ification.ServiceList{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
services := []*notification.Service{}
|
||||
for svc := range api.GetConfig().Services {
|
||||
services = append(services, ¬ification.Service{Name: new(svc)})
|
||||
}
|
||||
return ¬ification.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 ¬ification.TemplateList{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
templates := []*notification.Template{}
|
||||
for tmpl := range api.GetConfig().Templates {
|
||||
templates = append(templates, ¬ification.Template{Name: new(tmpl)})
|
||||
}
|
||||
return ¬ification.TemplateList{Items: templates}, nil
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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, ¬ification.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, ¬ification.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, ¬ification.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])
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Generated
-263
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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, ¬ificationsCm, ¬ificationsSecret)
|
||||
|
||||
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(¬ificationsCm, ¬ificationsSecret)
|
||||
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, ¬ificationsCm, ¬ificationsSecret)
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user