feat: add notifications API (#10279)

* add notifications API

we allow to list triggers, templates and services

Signed-off-by: Pavel <aborilov@gmail.com>

* fix: add notification manifest to tests

Signed-off-by: Pavel <aborilov@gmail.com>

* fix: add sleep to fix integration tests

for some reason notification configmap has old data, trying to fix with
the sleep

Signed-off-by: Pavel <aborilov@gmail.com>

* add proposal

Signed-off-by: Pavel <aborilov@gmail.com>

* more info to proposal

Signed-off-by: Pavel <aborilov@gmail.com>

* use struct for notifications objects instead of just strings

to be able easily extend API in the future return list of
trigger/template/service as list of structs

Signed-off-by: Pavel <aborilov@gmail.com>

Signed-off-by: Pavel <aborilov@gmail.com>
This commit is contained in:
Pavel
2022-08-26 02:04:14 +03:00
committed by GitHub
parent a23bfc3aca
commit 37ad433759
21 changed files with 2908 additions and 61 deletions

View File

@@ -2139,6 +2139,75 @@
}
}
},
"/api/v1/notifications/services": {
"get": {
"tags": [
"NotificationService"
],
"summary": "List returns list of services",
"operationId": "NotificationService_ListServices",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/notificationServiceList"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/runtimeError"
}
}
}
}
},
"/api/v1/notifications/templates": {
"get": {
"tags": [
"NotificationService"
],
"summary": "List returns list of templates",
"operationId": "NotificationService_ListTemplates",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/notificationTemplateList"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/runtimeError"
}
}
}
}
},
"/api/v1/notifications/triggers": {
"get": {
"tags": [
"NotificationService"
],
"summary": "List returns list of triggers",
"operationId": "NotificationService_ListTriggers",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/notificationTriggerList"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/runtimeError"
}
}
}
}
},
"/api/v1/projects": {
"get": {
"tags": [
@@ -3979,6 +4048,63 @@
"type": "object",
"title": "Generic (empty) response for GPG public key CRUD requests"
},
"notificationService": {
"type": "object",
"properties": {
"name": {
"type": "string"
}
}
},
"notificationServiceList": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/notificationService"
}
}
}
},
"notificationTemplate": {
"type": "object",
"properties": {
"name": {
"type": "string"
}
}
},
"notificationTemplateList": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/notificationTemplate"
}
}
}
},
"notificationTrigger": {
"type": "object",
"properties": {
"name": {
"type": "string"
}
}
},
"notificationTriggerList": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/notificationTrigger"
}
}
}
},
"oidcClaim": {
"type": "object",
"properties": {

View File

@@ -7,9 +7,12 @@ import (
"strings"
"github.com/argoproj/argo-cd/v2/common"
"github.com/argoproj/argo-cd/v2/reposerver/apiclient"
"github.com/argoproj/argo-cd/v2/util/env"
"github.com/argoproj/argo-cd/v2/util/errors"
service "github.com/argoproj/argo-cd/v2/util/notification/argocd"
"github.com/argoproj/argo-cd/v2/util/tls"
notificationscontroller "github.com/argoproj/argo-cd/v2/notification_controller/controller"
@@ -105,7 +108,22 @@ func NewCommand() *cobra.Command {
return fmt.Errorf("Unknown log format '%s'", logFormat)
}
argocdService, err := service.NewArgoCDService(k8sClient, namespace, argocdRepoServer, argocdRepoServerPlaintext, argocdRepoServerStrictTLS)
tlsConfig := apiclient.TLSConfiguration{
DisableTLS: argocdRepoServerPlaintext,
StrictValidation: argocdRepoServerStrictTLS,
}
if !tlsConfig.DisableTLS && tlsConfig.StrictValidation {
pool, err := tls.LoadX509CertPool(
fmt.Sprintf("%s/reposerver/tls/tls.crt", env.StringFromEnv(common.EnvAppConfigPath, common.DefaultAppConfigPath)),
fmt.Sprintf("%s/reposerver/tls/ca.crt", env.StringFromEnv(common.EnvAppConfigPath, common.DefaultAppConfigPath)),
)
if err != nil {
return err
}
tlsConfig.Certificates = pool
}
repoClientset := apiclient.NewRepoServerClientset(argocdRepoServer, 5, tlsConfig)
argocdService, err := service.NewArgoCDService(k8sClient, namespace, repoClientset)
if err != nil {
return err
}

View File

@@ -1,14 +1,19 @@
package admin
import (
"fmt"
"log"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
"github.com/argoproj/argo-cd/v2/common"
"github.com/argoproj/argo-cd/v2/reposerver/apiclient"
"github.com/argoproj/argo-cd/v2/util/env"
service "github.com/argoproj/argo-cd/v2/util/notification/argocd"
settings "github.com/argoproj/argo-cd/v2/util/notification/settings"
"github.com/argoproj/argo-cd/v2/util/tls"
"github.com/argoproj/notifications-engine/pkg/cmd"
"github.com/spf13/cobra"
@@ -39,7 +44,22 @@ func NewNotificationsCommand() *cobra.Command {
if err != nil {
log.Fatalf("Failed to parse k8s config: %v", err)
}
argocdService, err = service.NewArgoCDService(kubernetes.NewForConfigOrDie(k8sCfg), ns, argocdRepoServer, argocdRepoServerPlaintext, argocdRepoServerStrictTLS)
tlsConfig := apiclient.TLSConfiguration{
DisableTLS: argocdRepoServerPlaintext,
StrictValidation: argocdRepoServerStrictTLS,
}
if !tlsConfig.DisableTLS && tlsConfig.StrictValidation {
pool, err := tls.LoadX509CertPool(
fmt.Sprintf("%s/reposerver/tls/tls.crt", env.StringFromEnv(common.EnvAppConfigPath, common.DefaultAppConfigPath)),
fmt.Sprintf("%s/reposerver/tls/ca.crt", env.StringFromEnv(common.EnvAppConfigPath, common.DefaultAppConfigPath)),
)
if err != nil {
log.Fatalf("Failed to load tls certs: %v", err)
}
tlsConfig.Certificates = pool
}
repoClientset := apiclient.NewRepoServerClientset(argocdRepoServer, 5, tlsConfig)
argocdService, err = service.NewArgoCDService(kubernetes.NewForConfigOrDie(k8sCfg), ns, repoClientset)
if err != nil {
log.Fatalf("Failed to initialize Argo CD service: %v", err)
}

View File

@@ -21,9 +21,11 @@ const (
// Kubernetes ConfigMap and Secret resource names which hold Argo CD settings
const (
ArgoCDConfigMapName = "argocd-cm"
ArgoCDSecretName = "argocd-secret"
ArgoCDRBACConfigMapName = "argocd-rbac-cm"
ArgoCDConfigMapName = "argocd-cm"
ArgoCDSecretName = "argocd-secret"
ArgoCDNotificationsConfigMapName = "argocd-notifications-cm"
ArgoCDNotificationsSecretName = "argocd-notifications-secret"
ArgoCDRBACConfigMapName = "argocd-rbac-cm"
// Contains SSH known hosts data for connecting repositories. Will get mounted as volume to pods
ArgoCDKnownHostsConfigMapName = "argocd-ssh-known-hosts-cm"
// Contains TLS certificate data for connecting repositories. Will get mounted as volume to pods

View File

@@ -0,0 +1,99 @@
---
title: Subscribe to a notification from the Application Details page
authors:
- "@aborilov"
sponsors:
- TBD
reviewers:
- "@alexmt"
- TBD
approvers:
- "@alexmt"
- TBD
creation-date: 2022-08-16
last-updated: 2022-08-16
---
# Subscribe to a notification from the Application Details page
Provide the ability to subscribe to a notification from the Application Details page
## Summary
Allow users to subscribe application with a notification from the Application Details page
using provided instruments for selecting available triggers and services.
## Motivation
It is already possible to subscribe to notifications by modifying annotations however this is a pretty
poor user experience. Users have to understand annotation structure and have to find available services and triggers in configmap.
### Goals
Be able to subscribe to a notification from the Application Details page without forcing users to read the notification configmap.
### Non-Goals
We provide only ability to select existing services and triggers, we don't provide instruments to add/edit/delete notification services and triggers.
## Proposal
Two changes are required:
* Implement notifications API that would expose a list of configured triggers and services
* Implement UI that leverages notifications API and helps users to create a correct annotation.
### Use cases
Add a list of detailed use cases this enhancement intends to take care of.
#### Use case 1:
As a user, I would like to be able to subscribe application to a notification from the Application Details Page
without reading knowing of annotation format and reading notification configmap.
### Implementation Details/Notes/Constraints [optional]
Three read-only API endpoints will be added to provide a list of notification services, triggers, and templates.
```
message Triggers { repeated string triggers = 1; }
message TriggersListRequest {}
message Services { repeated string services = 1; }
message ServicesListRequest {}
message Templates { repeated string templates = 1; }
message TemplatesListRequest {}
service NotificationService {
rpc ListTriggers(TriggersListRequest) returns (Triggers) {
option (google.api.http).get = "/api/v1/notifications/triggers";
}
rpc ListServices(ServicesListRequest) returns (Services) {
option (google.api.http).get = "/api/v1/notifications/services";
}
rpc ListTemplates(TemplatesListRequest) returns (Templates) {
option (google.api.http).get = "/api/v1/notifications/templates";
}
}
```
### Detailed examples
### Security Considerations
New API endpoints are available only for authenticated users. API endpoints response does not contain any sensitive data.
### Risks and Mitigations
TBD
### Upgrade / Downgrade Strategy
By default, we don't have a notification configmap in the system; in that case, API should return an empty list instead of erroring.
## Drawbacks
## Alternatives
Continue to do that manually.

View File

@@ -11,6 +11,7 @@ set -o pipefail
PROJECT_ROOT=$(cd $(dirname ${BASH_SOURCE})/..; pwd)
PATH="${PROJECT_ROOT}/dist:${PATH}"
GOPATH=$(go env GOPATH)
# output tool versions
protoc --version
@@ -121,7 +122,7 @@ clean_swagger() {
}
echo "If additional types are added, the number of expected collisions may need to be increased"
EXPECTED_COLLISION_COUNT=62
EXPECTED_COLLISION_COUNT=64
collect_swagger server ${EXPECTED_COLLISION_COUNT}
clean_swagger server
clean_swagger reposerver

View File

@@ -36,6 +36,7 @@ import (
certificatepkg "github.com/argoproj/argo-cd/v2/pkg/apiclient/certificate"
clusterpkg "github.com/argoproj/argo-cd/v2/pkg/apiclient/cluster"
gpgkeypkg "github.com/argoproj/argo-cd/v2/pkg/apiclient/gpgkey"
notificationpkg "github.com/argoproj/argo-cd/v2/pkg/apiclient/notification"
projectpkg "github.com/argoproj/argo-cd/v2/pkg/apiclient/project"
repocredspkg "github.com/argoproj/argo-cd/v2/pkg/apiclient/repocreds"
repositorypkg "github.com/argoproj/argo-cd/v2/pkg/apiclient/repository"
@@ -87,6 +88,8 @@ type Client interface {
NewGPGKeyClientOrDie() (io.Closer, gpgkeypkg.GPGKeyServiceClient)
NewApplicationClient() (io.Closer, applicationpkg.ApplicationServiceClient, error)
NewApplicationClientOrDie() (io.Closer, applicationpkg.ApplicationServiceClient)
NewNotificationClient() (io.Closer, notificationpkg.NotificationServiceClient, error)
NewNotificationClientOrDie() (io.Closer, notificationpkg.NotificationServiceClient)
NewSessionClient() (io.Closer, sessionpkg.SessionServiceClient, error)
NewSessionClientOrDie() (io.Closer, sessionpkg.SessionServiceClient)
NewSettingsClient() (io.Closer, settingspkg.SettingsServiceClient, error)
@@ -669,11 +672,28 @@ func (c *client) NewApplicationClient() (io.Closer, applicationpkg.ApplicationSe
}
func (c *client) NewApplicationClientOrDie() (io.Closer, applicationpkg.ApplicationServiceClient) {
conn, repoIf, err := c.NewApplicationClient()
conn, appIf, err := c.NewApplicationClient()
if err != nil {
log.Fatalf("Failed to establish connection to %s: %v", c.ServerAddr, err)
}
return conn, repoIf
return conn, appIf
}
func (c *client) NewNotificationClient() (io.Closer, notificationpkg.NotificationServiceClient, error) {
conn, closer, err := c.newConn()
if err != nil {
return nil, nil, err
}
notifIf := notificationpkg.NewNotificationServiceClient(conn)
return closer, notifIf, nil
}
func (c *client) NewNotificationClientOrDie() (io.Closer, notificationpkg.NotificationServiceClient) {
conn, notifIf, err := c.NewNotificationClient()
if err != nil {
log.Fatalf("Failed to establish connection to %s: %v", c.ServerAddr, err)
}
return conn, notifIf
}
func (c *client) NewSessionClient() (io.Closer, sessionpkg.SessionServiceClient, error) {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,283 @@
// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT.
// source: server/notification/notification.proto
/*
Package notification is a reverse proxy.
It translates gRPC into RESTful JSON APIs.
*/
package notification
import (
"context"
"io"
"net/http"
"github.com/golang/protobuf/descriptor"
"github.com/golang/protobuf/proto"
"github.com/grpc-ecosystem/grpc-gateway/runtime"
"github.com/grpc-ecosystem/grpc-gateway/utilities"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/grpclog"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
// Suppress "imported and not used" errors
var _ codes.Code
var _ io.Reader
var _ status.Status
var _ = runtime.String
var _ = utilities.NewDoubleArray
var _ = descriptor.ForMessage
var _ = metadata.Join
func request_NotificationService_ListTriggers_0(ctx context.Context, marshaler runtime.Marshaler, client NotificationServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq TriggersListRequest
var metadata runtime.ServerMetadata
msg, err := client.ListTriggers(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_NotificationService_ListTriggers_0(ctx context.Context, marshaler runtime.Marshaler, server NotificationServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq TriggersListRequest
var metadata runtime.ServerMetadata
msg, err := server.ListTriggers(ctx, &protoReq)
return msg, metadata, err
}
func request_NotificationService_ListServices_0(ctx context.Context, marshaler runtime.Marshaler, client NotificationServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ServicesListRequest
var metadata runtime.ServerMetadata
msg, err := client.ListServices(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_NotificationService_ListServices_0(ctx context.Context, marshaler runtime.Marshaler, server NotificationServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ServicesListRequest
var metadata runtime.ServerMetadata
msg, err := server.ListServices(ctx, &protoReq)
return msg, metadata, err
}
func request_NotificationService_ListTemplates_0(ctx context.Context, marshaler runtime.Marshaler, client NotificationServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq TemplatesListRequest
var metadata runtime.ServerMetadata
msg, err := client.ListTemplates(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_NotificationService_ListTemplates_0(ctx context.Context, marshaler runtime.Marshaler, server NotificationServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq TemplatesListRequest
var metadata runtime.ServerMetadata
msg, err := server.ListTemplates(ctx, &protoReq)
return msg, metadata, err
}
// RegisterNotificationServiceHandlerServer registers the http handlers for service NotificationService to "mux".
// UnaryRPC :call NotificationServiceServer directly.
// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterNotificationServiceHandlerFromEndpoint instead.
func RegisterNotificationServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server NotificationServiceServer) error {
mux.Handle("GET", pattern_NotificationService_ListTriggers_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_NotificationService_ListTriggers_0(rctx, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_NotificationService_ListTriggers_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_NotificationService_ListServices_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_NotificationService_ListServices_0(rctx, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_NotificationService_ListServices_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_NotificationService_ListTemplates_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_NotificationService_ListTemplates_0(rctx, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_NotificationService_ListTemplates_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
// RegisterNotificationServiceHandlerFromEndpoint is same as RegisterNotificationServiceHandler but
// automatically dials to "endpoint" and closes the connection when "ctx" gets done.
func RegisterNotificationServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) {
conn, err := grpc.Dial(endpoint, opts...)
if err != nil {
return err
}
defer func() {
if err != nil {
if cerr := conn.Close(); cerr != nil {
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
}
return
}
go func() {
<-ctx.Done()
if cerr := conn.Close(); cerr != nil {
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
}
}()
}()
return RegisterNotificationServiceHandler(ctx, mux, conn)
}
// RegisterNotificationServiceHandler registers the http handlers for service NotificationService to "mux".
// The handlers forward requests to the grpc endpoint over "conn".
func RegisterNotificationServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {
return RegisterNotificationServiceHandlerClient(ctx, mux, NewNotificationServiceClient(conn))
}
// RegisterNotificationServiceHandlerClient registers the http handlers for service NotificationService
// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "NotificationServiceClient".
// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "NotificationServiceClient"
// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in
// "NotificationServiceClient" to call the correct interceptors.
func RegisterNotificationServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client NotificationServiceClient) error {
mux.Handle("GET", pattern_NotificationService_ListTriggers_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_NotificationService_ListTriggers_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_NotificationService_ListTriggers_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_NotificationService_ListServices_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_NotificationService_ListServices_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_NotificationService_ListServices_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_NotificationService_ListTemplates_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_NotificationService_ListTemplates_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_NotificationService_ListTemplates_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
var (
pattern_NotificationService_ListTriggers_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"api", "v1", "notifications", "triggers"}, "", runtime.AssumeColonVerbOpt(true)))
pattern_NotificationService_ListServices_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"api", "v1", "notifications", "services"}, "", runtime.AssumeColonVerbOpt(true)))
pattern_NotificationService_ListTemplates_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"api", "v1", "notifications", "templates"}, "", runtime.AssumeColonVerbOpt(true)))
)
var (
forward_NotificationService_ListTriggers_0 = runtime.ForwardResponseMessage
forward_NotificationService_ListServices_0 = runtime.ForwardResponseMessage
forward_NotificationService_ListTemplates_0 = runtime.ForwardResponseMessage
)

View File

@@ -0,0 +1,68 @@
package notification
import (
"context"
"github.com/argoproj/argo-cd/v2/pkg/apiclient/notification"
"github.com/argoproj/notifications-engine/pkg/api"
apierr "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/utils/pointer"
)
// 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(ctx context.Context, q *notification.TriggersListRequest) (*notification.TriggerList, error) {
api, err := s.apiFactory.GetAPI()
if err != nil {
if apierr.IsNotFound(err) {
return &notification.TriggerList{}, nil
}
}
triggers := []*notification.Trigger{}
for trigger := range api.GetConfig().Triggers {
triggers = append(triggers, &notification.Trigger{Name: pointer.String(trigger)})
}
return &notification.TriggerList{Items: triggers}, nil
}
// List returns list of notification services
func (s *Server) ListServices(ctx context.Context, q *notification.ServicesListRequest) (*notification.ServiceList, error) {
api, err := s.apiFactory.GetAPI()
if err != nil {
if apierr.IsNotFound(err) {
return &notification.ServiceList{}, nil
}
return nil, err
}
services := []*notification.Service{}
for svc := range api.GetConfig().Services {
services = append(services, &notification.Service{Name: pointer.String(svc)})
}
return &notification.ServiceList{Items: services}, nil
}
// List returns list of notification templates
func (s *Server) ListTemplates(ctx context.Context, q *notification.TemplatesListRequest) (*notification.TemplateList, error) {
api, err := s.apiFactory.GetAPI()
if err != nil {
if apierr.IsNotFound(err) {
return &notification.TemplateList{}, nil
}
return nil, err
}
templates := []*notification.Template{}
for tmpl := range api.GetConfig().Templates {
templates = append(templates, &notification.Template{Name: pointer.String(tmpl)})
}
return &notification.TemplateList{Items: templates}, nil
}

View File

@@ -0,0 +1,58 @@
syntax = "proto2";
option go_package = "github.com/argoproj/argo-cd/v2/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";
}
}

View File

@@ -0,0 +1,99 @@
package notification
import (
"context"
"os"
"testing"
"github.com/argoproj/argo-cd/v2/pkg/apiclient/notification"
"github.com/argoproj/argo-cd/v2/reposerver/apiclient/mocks"
service "github.com/argoproj/argo-cd/v2/util/notification/argocd"
"github.com/argoproj/argo-cd/v2/util/notification/k8s"
"github.com/argoproj/argo-cd/v2/util/notification/settings"
"github.com/argoproj/notifications-engine/pkg/api"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
"k8s.io/utils/pointer"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"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) {
// 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.NewSimpleClientset(&corev1.ConfigMap{
ObjectMeta: v1.ObjectMeta{
Namespace: testNamespace,
Name: "argocd-notifications-cm",
},
Data: map[string]string{
"service.webhook.test": "url: https://test.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: v1.ObjectMeta{
Name: "argocd-notifications-secret",
Namespace: testNamespace,
},
Data: map[string][]byte{},
})
ctx := context.Background()
secretInformer := k8s.NewSecretInformer(kubeclientset, testNamespace, "argocd-notifications-secret")
configMapInformer := k8s.NewConfigMapInformer(kubeclientset, testNamespace, "argocd-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{}}
argocdService, err := service.NewArgoCDService(kubeclientset, testNamespace, mockRepoClient)
require.NoError(t, err)
defer argocdService.Close()
apiFactory := api.NewFactory(settings.GetFactorySettings(argocdService, "argocd-notifications-secret", "argocd-notifications-cm"), testNamespace, secretInformer, configMapInformer)
t.Run("TestListServices", func(t *testing.T) {
server := NewServer(apiFactory)
services, err := server.ListServices(ctx, &notification.ServicesListRequest{})
assert.NoError(t, err)
assert.Len(t, services.Items, 1)
assert.Equal(t, services.Items[0].Name, pointer.String("test"))
assert.NotEmpty(t, services.Items[0])
})
t.Run("TestListTriggers", func(t *testing.T) {
server := NewServer(apiFactory)
triggers, err := server.ListTriggers(ctx, &notification.TriggersListRequest{})
assert.NoError(t, err)
assert.Len(t, triggers.Items, 1)
assert.Equal(t, triggers.Items[0].Name, pointer.String("on-created"))
assert.NotEmpty(t, triggers.Items[0])
})
t.Run("TestListTemplates", func(t *testing.T) {
server := NewServer(apiFactory)
templates, err := server.ListTemplates(ctx, &notification.TemplatesListRequest{})
assert.NoError(t, err)
assert.Len(t, templates.Items, 1)
assert.Equal(t, templates.Items[0].Name, pointer.String("app-created"))
assert.NotEmpty(t, templates.Items[0])
})
}

View File

@@ -26,6 +26,7 @@ import (
netCtx "context"
"github.com/argoproj/notifications-engine/pkg/api"
"github.com/argoproj/pkg/sync"
"github.com/go-redis/redis/v8"
"github.com/golang-jwt/jwt/v4"
@@ -62,6 +63,7 @@ import (
certificatepkg "github.com/argoproj/argo-cd/v2/pkg/apiclient/certificate"
clusterpkg "github.com/argoproj/argo-cd/v2/pkg/apiclient/cluster"
gpgkeypkg "github.com/argoproj/argo-cd/v2/pkg/apiclient/gpgkey"
notificationpkg "github.com/argoproj/argo-cd/v2/pkg/apiclient/notification"
projectpkg "github.com/argoproj/argo-cd/v2/pkg/apiclient/project"
repocredspkg "github.com/argoproj/argo-cd/v2/pkg/apiclient/repocreds"
repositorypkg "github.com/argoproj/argo-cd/v2/pkg/apiclient/repository"
@@ -83,6 +85,7 @@ import (
"github.com/argoproj/argo-cd/v2/server/gpgkey"
"github.com/argoproj/argo-cd/v2/server/logout"
"github.com/argoproj/argo-cd/v2/server/metrics"
"github.com/argoproj/argo-cd/v2/server/notification"
"github.com/argoproj/argo-cd/v2/server/project"
"github.com/argoproj/argo-cd/v2/server/rbacpolicy"
"github.com/argoproj/argo-cd/v2/server/repocreds"
@@ -105,6 +108,9 @@ import (
"github.com/argoproj/argo-cd/v2/util/io/files"
jwtutil "github.com/argoproj/argo-cd/v2/util/jwt"
kubeutil "github.com/argoproj/argo-cd/v2/util/kube"
service "github.com/argoproj/argo-cd/v2/util/notification/argocd"
"github.com/argoproj/argo-cd/v2/util/notification/k8s"
settings_notif "github.com/argoproj/argo-cd/v2/util/notification/settings"
"github.com/argoproj/argo-cd/v2/util/oidc"
"github.com/argoproj/argo-cd/v2/util/rbac"
util_session "github.com/argoproj/argo-cd/v2/util/session"
@@ -171,12 +177,15 @@ type ArgoCDServer struct {
db db.ArgoDB
// stopCh is the channel which when closed, will shutdown the Argo CD server
stopCh chan struct{}
userStateStorage util_session.UserStateStorage
indexDataInit gosync.Once
indexData []byte
indexDataErr error
staticAssets http.FileSystem
stopCh chan struct{}
userStateStorage util_session.UserStateStorage
indexDataInit gosync.Once
indexData []byte
indexDataErr error
staticAssets http.FileSystem
apiFactory api.Factory
secretInformer cache.SharedIndexInformer
configMapInformer cache.SharedIndexInformer
}
type ArgoCDServerOpts struct {
@@ -261,21 +270,32 @@ func NewServer(ctx context.Context, opts ArgoCDServerOpts) *ArgoCDServer {
staticFS = io.NewComposableFS(staticFS, os.DirFS(opts.StaticAssetsDir))
}
argocdService, err := service.NewArgoCDService(opts.KubeClientset, opts.Namespace, opts.RepoClientset)
errors.CheckError(err)
secretInformer := k8s.NewSecretInformer(opts.KubeClientset, opts.Namespace, "argocd-notifications-secret")
configMapInformer := k8s.NewConfigMapInformer(opts.KubeClientset, opts.Namespace, "argocd-notifications-cm")
apiFactory := api.NewFactory(settings_notif.GetFactorySettings(argocdService, "argocd-notifications-secret", "argocd-notifications-cm"), opts.Namespace, secretInformer, configMapInformer)
return &ArgoCDServer{
ArgoCDServerOpts: opts,
log: log.NewEntry(log.StandardLogger()),
settings: settings,
sessionMgr: sessionMgr,
settingsMgr: settingsMgr,
enf: enf,
projInformer: projInformer,
projLister: projLister,
appInformer: appInformer,
appLister: appLister,
policyEnforcer: policyEnf,
userStateStorage: userStateStorage,
staticAssets: http.FS(staticFS),
db: db.NewDB(opts.Namespace, settingsMgr, opts.KubeClientset),
ArgoCDServerOpts: opts,
log: log.NewEntry(log.StandardLogger()),
settings: settings,
sessionMgr: sessionMgr,
settingsMgr: settingsMgr,
enf: enf,
projInformer: projInformer,
projLister: projLister,
appInformer: appInformer,
appLister: appLister,
policyEnforcer: policyEnf,
userStateStorage: userStateStorage,
staticAssets: http.FS(staticFS),
db: db.NewDB(opts.Namespace, settingsMgr, opts.KubeClientset),
apiFactory: apiFactory,
secretInformer: secretInformer,
configMapInformer: configMapInformer,
}
}
@@ -379,6 +399,8 @@ func (a *ArgoCDServer) Listen() (*Listeners, error) {
func (a *ArgoCDServer) Init(ctx context.Context) {
go a.projInformer.Run(ctx.Done())
go a.appInformer.Run(ctx.Done())
go a.configMapInformer.Run(ctx.Done())
go a.secretInformer.Run(ctx.Done())
}
// Run runs the API Server
@@ -700,6 +722,8 @@ func (a *ArgoCDServer) newGRPCServer() (*grpc.Server, application.AppResourceTre
projectService := project.NewServer(a.Namespace, a.KubeClientset, a.AppClientset, a.enf, projectLock, a.sessionMgr, a.policyEnforcer, a.projInformer, a.settingsMgr, a.db)
settingsService := settings.NewServer(a.settingsMgr, a, a.DisableAuth)
accountService := account.NewServer(a.sessionMgr, a.settingsMgr, a.enf)
notificationService := notification.NewServer(a.apiFactory)
certificateService := certificate.NewServer(a.RepoClientset, a.db, a.enf)
gpgkeyService := gpgkey.NewServer(a.RepoClientset, a.db, a.enf)
versionpkg.RegisterVersionServiceServer(grpcS, version.NewServer(a, func() (bool, error) {
@@ -714,6 +738,7 @@ func (a *ArgoCDServer) newGRPCServer() (*grpc.Server, application.AppResourceTre
}))
clusterpkg.RegisterClusterServiceServer(grpcS, clusterService)
applicationpkg.RegisterApplicationServiceServer(grpcS, applicationService)
notificationpkg.RegisterNotificationServiceServer(grpcS, notificationService)
repositorypkg.RegisterRepositoryServiceServer(grpcS, repoService)
repocredspkg.RegisterRepoCredsServiceServer(grpcS, repoCredsService)
sessionpkg.RegisterSessionServiceServer(grpcS, sessionService)
@@ -859,6 +884,7 @@ func (a *ArgoCDServer) newHTTPServer(ctx context.Context, port int, grpcWebHandl
mustRegisterGWHandler(versionpkg.RegisterVersionServiceHandler, ctx, gwmux, conn)
mustRegisterGWHandler(clusterpkg.RegisterClusterServiceHandler, ctx, gwmux, conn)
mustRegisterGWHandler(applicationpkg.RegisterApplicationServiceHandler, ctx, gwmux, conn)
mustRegisterGWHandler(notificationpkg.RegisterNotificationServiceHandler, ctx, gwmux, conn)
mustRegisterGWHandler(repositorypkg.RegisterRepositoryServiceHandler, ctx, gwmux, conn)
mustRegisterGWHandler(repocredspkg.RegisterRepoCredsServiceHandler, ctx, gwmux, conn)
mustRegisterGWHandler(sessionpkg.RegisterSessionServiceHandler, ctx, gwmux, conn)

View File

@@ -25,6 +25,7 @@ import (
"github.com/argoproj/argo-cd/v2/pkg/apiclient/session"
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
apps "github.com/argoproj/argo-cd/v2/pkg/client/clientset/versioned/fake"
"github.com/argoproj/argo-cd/v2/reposerver/apiclient/mocks"
servercache "github.com/argoproj/argo-cd/v2/server/cache"
"github.com/argoproj/argo-cd/v2/server/rbacpolicy"
"github.com/argoproj/argo-cd/v2/test"
@@ -42,6 +43,8 @@ func fakeServer() (*ArgoCDServer, func()) {
appClientSet := apps.NewSimpleClientset()
redis, closer := test.NewInMemoryRedis()
port, err := test.GetFreePort()
mockRepoClient := &mocks.Clientset{RepoServerServiceClient: &mocks.RepoServerServiceClient{}}
if err != nil {
panic(err)
}
@@ -64,7 +67,8 @@ func fakeServer() (*ArgoCDServer, func()) {
1*time.Minute,
1*time.Minute,
),
RedisClient: redis,
RedisClient: redis,
RepoClientset: mockRepoClient,
}
srv := NewServer(context.Background(), argoCDOpts)
return srv, closer
@@ -98,9 +102,10 @@ func TestEnforceProjectToken(t *testing.T) {
cm := test.NewFakeConfigMap()
secret := test.NewFakeSecret()
kubeclientset := fake.NewSimpleClientset(cm, secret)
mockRepoClient := &mocks.Clientset{RepoServerServiceClient: &mocks.RepoServerServiceClient{}}
t.Run("TestEnforceProjectTokenSuccessful", func(t *testing.T) {
s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj)})
s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj), RepoClientset: mockRepoClient})
cancel := test.StartInformer(s.projInformer)
defer cancel()
claims := jwt.MapClaims{"sub": defaultSub, "iat": defaultIssuedAt}
@@ -109,21 +114,21 @@ func TestEnforceProjectToken(t *testing.T) {
})
t.Run("TestEnforceProjectTokenWithDiffCreateAtFailure", func(t *testing.T) {
s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj)})
s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj), RepoClientset: mockRepoClient})
diffCreateAt := defaultIssuedAt + 1
claims := jwt.MapClaims{"sub": defaultSub, "iat": diffCreateAt}
assert.False(t, s.enf.Enforce(claims, "applications", "get", defaultTestObject))
})
t.Run("TestEnforceProjectTokenIncorrectSubFormatFailure", func(t *testing.T) {
s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj)})
s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj), RepoClientset: mockRepoClient})
invalidSub := "proj:test"
claims := jwt.MapClaims{"sub": invalidSub, "iat": defaultIssuedAt}
assert.False(t, s.enf.Enforce(claims, "applications", "get", defaultTestObject))
})
t.Run("TestEnforceProjectTokenNoTokenFailure", func(t *testing.T) {
s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj)})
s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj), RepoClientset: mockRepoClient})
nonExistentToken := "fake-token"
invalidSub := fmt.Sprintf(subFormat, projectName, nonExistentToken)
claims := jwt.MapClaims{"sub": invalidSub, "iat": defaultIssuedAt}
@@ -133,7 +138,7 @@ func TestEnforceProjectToken(t *testing.T) {
t.Run("TestEnforceProjectTokenNotJWTTokenFailure", func(t *testing.T) {
proj := existingProj.DeepCopy()
proj.Spec.Roles[0].JWTTokens = nil
s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(proj)})
s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(proj), RepoClientset: mockRepoClient})
claims := jwt.MapClaims{"sub": defaultSub, "iat": defaultIssuedAt}
assert.False(t, s.enf.Enforce(claims, "applications", "get", defaultTestObject))
})
@@ -146,7 +151,7 @@ func TestEnforceProjectToken(t *testing.T) {
proj := existingProj.DeepCopy()
proj.Spec.Roles[0] = role
s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(proj)})
s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(proj), RepoClientset: mockRepoClient})
cancel := test.StartInformer(s.projInformer)
defer cancel()
claims := jwt.MapClaims{"sub": defaultSub, "iat": defaultIssuedAt}
@@ -157,7 +162,7 @@ func TestEnforceProjectToken(t *testing.T) {
})
t.Run("TestEnforceProjectTokenWithIdSuccessful", func(t *testing.T) {
s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj)})
s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj), RepoClientset: mockRepoClient})
cancel := test.StartInformer(s.projInformer)
defer cancel()
claims := jwt.MapClaims{"sub": defaultSub, "jti": defaultId}
@@ -166,7 +171,7 @@ func TestEnforceProjectToken(t *testing.T) {
})
t.Run("TestEnforceProjectTokenWithInvalidIdFailure", func(t *testing.T) {
s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj)})
s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj), RepoClientset: mockRepoClient})
invalidId := "invalidId"
claims := jwt.MapClaims{"sub": defaultSub, "jti": defaultId}
res := s.enf.Enforce(claims, "applications", "get", invalidId)
@@ -242,10 +247,13 @@ func TestInitializingExistingDefaultProject(t *testing.T) {
}
appClientSet := apps.NewSimpleClientset(defaultProj)
mockRepoClient := &mocks.Clientset{RepoServerServiceClient: &mocks.RepoServerServiceClient{}}
argoCDOpts := ArgoCDServerOpts{
Namespace: test.FakeArgoCDNamespace,
KubeClientset: kubeclientset,
AppClientset: appClientSet,
RepoClientset: mockRepoClient,
}
argocd := NewServer(context.Background(), argoCDOpts)
@@ -262,11 +270,13 @@ func TestInitializingNotExistingDefaultProject(t *testing.T) {
secret := test.NewFakeSecret()
kubeclientset := fake.NewSimpleClientset(cm, secret)
appClientSet := apps.NewSimpleClientset()
mockRepoClient := &mocks.Clientset{RepoServerServiceClient: &mocks.RepoServerServiceClient{}}
argoCDOpts := ArgoCDServerOpts{
Namespace: test.FakeArgoCDNamespace,
KubeClientset: kubeclientset,
AppClientset: appClientSet,
RepoClientset: mockRepoClient,
}
argocd := NewServer(context.Background(), argoCDOpts)
@@ -309,8 +319,9 @@ func TestEnforceProjectGroups(t *testing.T) {
},
},
}
mockRepoClient := &mocks.Clientset{RepoServerServiceClient: &mocks.RepoServerServiceClient{}}
kubeclientset := fake.NewSimpleClientset(test.NewFakeConfigMap(), test.NewFakeSecret())
s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj)})
s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj), RepoClientset: mockRepoClient})
cancel := test.StartInformer(s.projInformer)
defer cancel()
claims := jwt.MapClaims{
@@ -344,6 +355,7 @@ func TestRevokedToken(t *testing.T) {
defaultSub := fmt.Sprintf(subFormat, projectName, roleName)
defaultPolicy := fmt.Sprintf(policyTemplate, defaultSub, projectName, defaultObject, defaultEffect)
kubeclientset := fake.NewSimpleClientset(test.NewFakeConfigMap(), test.NewFakeSecret())
mockRepoClient := &mocks.Clientset{RepoServerServiceClient: &mocks.RepoServerServiceClient{}}
jwtTokenByRole := make(map[string]v1alpha1.JWTTokens)
jwtTokenByRole[roleName] = v1alpha1.JWTTokens{Items: []v1alpha1.JWTToken{{IssuedAt: defaultIssuedAt}}}
@@ -371,7 +383,7 @@ func TestRevokedToken(t *testing.T) {
},
}
s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj)})
s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj), RepoClientset: mockRepoClient})
cancel := test.StartInformer(s.projInformer)
defer cancel()
claims := jwt.MapClaims{"sub": defaultSub, "iat": defaultIssuedAt}
@@ -419,10 +431,12 @@ func TestAuthenticate(t *testing.T) {
secret := test.NewFakeSecret()
kubeclientset := fake.NewSimpleClientset(cm, secret)
appClientSet := apps.NewSimpleClientset()
mockRepoClient := &mocks.Clientset{RepoServerServiceClient: &mocks.RepoServerServiceClient{}}
argoCDOpts := ArgoCDServerOpts{
Namespace: test.FakeArgoCDNamespace,
KubeClientset: kubeclientset,
AppClientset: appClientSet,
RepoClientset: mockRepoClient,
}
argocd := NewServer(context.Background(), argoCDOpts)
ctx := context.Background()
@@ -535,10 +549,12 @@ connectors:
secret := test.NewFakeSecret()
kubeclientset := fake.NewSimpleClientset(cm, secret)
appClientSet := apps.NewSimpleClientset()
mockRepoClient := &mocks.Clientset{RepoServerServiceClient: &mocks.RepoServerServiceClient{}}
argoCDOpts := ArgoCDServerOpts{
Namespace: test.FakeArgoCDNamespace,
KubeClientset: kubeclientset,
AppClientset: appClientSet,
RepoClientset: mockRepoClient,
}
if withFakeSSO {
argoCDOpts.DexServerAddr = ts.URL
@@ -852,6 +868,7 @@ func TestTranslateGrpcCookieHeader(t *testing.T) {
Namespace: test.FakeArgoCDNamespace,
KubeClientset: fake.NewSimpleClientset(test.NewFakeConfigMap(), test.NewFakeSecret()),
AppClientset: apps.NewSimpleClientset(),
RepoClientset: &mocks.Clientset{RepoServerServiceClient: &mocks.RepoServerServiceClient{}},
}
argocd := NewServer(context.Background(), argoCDOpts)
@@ -891,6 +908,7 @@ func TestInitializeDefaultProject_ProjectDoesNotExist(t *testing.T) {
Namespace: test.FakeArgoCDNamespace,
KubeClientset: fake.NewSimpleClientset(test.NewFakeConfigMap(), test.NewFakeSecret()),
AppClientset: apps.NewSimpleClientset(),
RepoClientset: &mocks.Clientset{RepoServerServiceClient: &mocks.RepoServerServiceClient{}},
}
err := initializeDefaultProject(argoCDOpts)
@@ -928,6 +946,7 @@ func TestInitializeDefaultProject_ProjectAlreadyInitialized(t *testing.T) {
Namespace: test.FakeArgoCDNamespace,
KubeClientset: fake.NewSimpleClientset(test.NewFakeConfigMap(), test.NewFakeSecret()),
AppClientset: apps.NewSimpleClientset(&existingDefaultProject),
RepoClientset: &mocks.Clientset{RepoServerServiceClient: &mocks.RepoServerServiceClient{}},
}
err := initializeDefaultProject(argoCDOpts)

View File

@@ -317,6 +317,11 @@ func updateSettingConfigMap(updater func(cm *corev1.ConfigMap) error) {
updateGenericConfigMap(common.ArgoCDConfigMapName, updater)
}
// Convenience wrapper for updating argocd-notifications-cm
func updateNotificationsConfigMap(updater func(cm *corev1.ConfigMap) error) {
updateGenericConfigMap(common.ArgoCDNotificationsConfigMapName, updater)
}
// Convenience wrapper for updating argocd-cm-rbac
func updateRBACConfigMap(updater func(cm *corev1.ConfigMap) error) {
updateGenericConfigMap(common.ArgoCDRBACConfigMapName, updater)
@@ -502,6 +507,13 @@ func SetParamInSettingConfigMap(key, value string) {
})
}
func SetParamInNotificationsConfigMap(key, value string) {
updateNotificationsConfigMap(func(cm *corev1.ConfigMap) error {
cm.Data[key] = value
return nil
})
}
func EnsureCleanState(t *testing.T) {
// In large scenarios, we can skip tests that already run
SkipIfAlreadyRun(t)
@@ -543,6 +555,11 @@ func EnsureCleanState(t *testing.T) {
return nil
})
updateNotificationsConfigMap(func(cm *corev1.ConfigMap) error {
cm.Data = map[string]string{}
return nil
})
// reset rbac
updateRBACConfigMap(func(cm *corev1.ConfigMap) error {
cm.Data = map[string]string{}

View File

@@ -0,0 +1,27 @@
package notification
import (
"time"
"github.com/argoproj/argo-cd/v2/test/e2e/fixture"
)
// this implements the "when" part of given/when/then
//
// none of the func implement error checks, and that is complete intended, you should check for errors
// using the Then()
type Actions struct {
context *Context
}
func (a *Actions) SetParamInNotificationConfigMap(key, value string) *Actions {
fixture.SetParamInNotificationsConfigMap(key, value)
return a
}
func (a *Actions) Then() *Consequences {
a.context.t.Helper()
// in case any settings have changed, pause for 1s, not great, but fine
time.Sleep(1 * time.Second)
return &Consequences{a.context, a}
}

View File

@@ -0,0 +1,55 @@
package notification
import (
"context"
"github.com/argoproj/argo-cd/v2/pkg/apiclient/notification"
"github.com/argoproj/argo-cd/v2/test/e2e/fixture"
)
// this implements the "then" part of given/when/then
type Consequences struct {
context *Context
actions *Actions
}
func (c *Consequences) Services(block func(services *notification.ServiceList, err error)) *Consequences {
c.context.t.Helper()
block(c.listServices())
return c
}
func (c *Consequences) Triggers(block func(services *notification.TriggerList, err error)) *Consequences {
c.context.t.Helper()
block(c.listTriggers())
return c
}
func (c *Consequences) Templates(block func(services *notification.TemplateList, err error)) *Consequences {
c.context.t.Helper()
block(c.listTemplates())
return c
}
func (c *Consequences) listServices() (*notification.ServiceList, error) {
_, notifClient, _ := fixture.ArgoCDClientset.NewNotificationClient()
return notifClient.ListServices(context.Background(), &notification.ServicesListRequest{})
}
func (c *Consequences) listTriggers() (*notification.TriggerList, error) {
_, notifClient, _ := fixture.ArgoCDClientset.NewNotificationClient()
return notifClient.ListTriggers(context.Background(), &notification.TriggersListRequest{})
}
func (c *Consequences) listTemplates() (*notification.TemplateList, error) {
_, notifClient, _ := fixture.ArgoCDClientset.NewNotificationClient()
return notifClient.ListTemplates(context.Background(), &notification.TemplatesListRequest{})
}
func (c *Consequences) When() *Actions {
return c.actions
}
func (c *Consequences) Given() *Context {
return c.context
}

View File

@@ -0,0 +1,26 @@
package notification
import (
"testing"
"github.com/argoproj/argo-cd/v2/test/e2e/fixture"
)
// this implements the "given" part of given/when/then
type Context struct {
t *testing.T
}
func Given(t *testing.T) *Context {
fixture.EnsureCleanState(t)
return &Context{t: t}
}
func (c *Context) And(block func()) *Context {
block()
return c
}
func (c *Context) When() *Actions {
return &Actions{context: c}
}

View File

@@ -0,0 +1,40 @@
package e2e
import (
"testing"
"github.com/argoproj/argo-cd/v2/pkg/apiclient/notification"
notifFixture "github.com/argoproj/argo-cd/v2/test/e2e/fixture/notification"
"github.com/stretchr/testify/assert"
"k8s.io/utils/pointer"
)
func TestNotificationsListServices(t *testing.T) {
ctx := notifFixture.Given(t)
ctx.When().
SetParamInNotificationConfigMap("service.webhook.test", "url: https://test.com").
Then().Services(func(services *notification.ServiceList, err error) {
assert.Nil(t, err)
assert.Equal(t, []*notification.Service{&notification.Service{Name: pointer.String("test")}}, services.Items)
})
}
func TestNotificationsListTemplates(t *testing.T) {
ctx := notifFixture.Given(t)
ctx.When().
SetParamInNotificationConfigMap("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").
Then().Templates(func(templates *notification.TemplateList, err error) {
assert.Nil(t, err)
assert.Equal(t, []*notification.Template{&notification.Template{Name: pointer.String("app-created")}}, templates.Items)
})
}
func TestNotificationsListTriggers(t *testing.T) {
ctx := notifFixture.Given(t)
ctx.When().
SetParamInNotificationConfigMap("trigger.on-created", "- description: Application is created.\n oncePer: app.metadata.name\n send:\n - app-created\n when: \"true\"\n").
Then().Triggers(func(triggers *notification.TriggerList, err error) {
assert.Nil(t, err)
assert.Equal(t, []*notification.Trigger{&notification.Trigger{Name: pointer.String("on-created")}}, triggers.Items)
})
}

View File

@@ -5,6 +5,7 @@ bases:
- ../../../manifests/crds
- ../../../manifests/base/config
- ../../../manifests/cluster-rbac
- ../../../manifests/base/notification
patchesStrategicMerge:
- patches.yaml
- patches.yaml

View File

@@ -2,20 +2,17 @@ package service
import (
"context"
"fmt"
"github.com/argoproj/argo-cd/v2/util/notification/expression/shared"
log "github.com/sirupsen/logrus"
"k8s.io/client-go/kubernetes"
"github.com/argoproj/argo-cd/v2/common"
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v2/reposerver/apiclient"
repoapiclient "github.com/argoproj/argo-cd/v2/reposerver/apiclient"
"github.com/argoproj/argo-cd/v2/util/db"
"github.com/argoproj/argo-cd/v2/util/env"
"github.com/argoproj/argo-cd/v2/util/settings"
"github.com/argoproj/argo-cd/v2/util/tls"
)
//go:generate mockgen -destination=./mocks/service.go -package=mocks github.com/argoproj-labs/argocd-notifications/shared/argocd Service
@@ -25,25 +22,9 @@ type Service interface {
GetAppDetails(ctx context.Context, appSource *v1alpha1.ApplicationSource) (*shared.AppDetail, error)
}
func NewArgoCDService(clientset kubernetes.Interface, namespace string, repoServerAddress string, disableTLS bool, strictValidation bool) (*argoCDService, error) {
func NewArgoCDService(clientset kubernetes.Interface, namespace string, repoClientset repoapiclient.Clientset) (*argoCDService, error) {
ctx, cancel := context.WithCancel(context.Background())
settingsMgr := settings.NewSettingsManager(ctx, clientset, namespace)
tlsConfig := apiclient.TLSConfiguration{
DisableTLS: disableTLS,
StrictValidation: strictValidation,
}
if !disableTLS && strictValidation {
pool, err := tls.LoadX509CertPool(
fmt.Sprintf("%s/reposerver/tls/tls.crt", env.StringFromEnv(common.EnvAppConfigPath, common.DefaultAppConfigPath)),
fmt.Sprintf("%s/reposerver/tls/ca.crt", env.StringFromEnv(common.EnvAppConfigPath, common.DefaultAppConfigPath)),
)
if err != nil {
cancel()
return nil, err
}
tlsConfig.Certificates = pool
}
repoClientset := apiclient.NewRepoServerClientset(repoServerAddress, 5, tlsConfig)
closer, repoClient, err := repoClientset.NewRepoServerClient()
if err != nil {
cancel()