mirror of
https://github.com/argoproj/argo-cd
synced 2026-04-21 17:07:16 +00:00
Merge branch 'master' into master
This commit is contained in:
commit
901095bf81
59 changed files with 2310 additions and 885 deletions
4
.github/workflows/image-reuse.yaml
vendored
4
.github/workflows/image-reuse.yaml
vendored
|
|
@ -74,9 +74,9 @@ jobs:
|
|||
go-version: ${{ inputs.go-version }}
|
||||
|
||||
- name: Install cosign
|
||||
uses: sigstore/cosign-installer@11086d25041f77fe8fe7b9ea4e48e3b9192b8f19 # v3.1.2
|
||||
uses: sigstore/cosign-installer@1fc5bd396d372bee37d608f955b336615edf79c8 # v3.2.0
|
||||
with:
|
||||
cosign-release: 'v2.0.2'
|
||||
cosign-release: 'v2.2.1'
|
||||
|
||||
- uses: docker/setup-qemu-action@2b82ce82d56a2a04d2637cd93a637ae1b359c0a7 # v2.2.0
|
||||
- uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0
|
||||
|
|
|
|||
3
Procfile
3
Procfile
|
|
@ -9,4 +9,5 @@ git-server: test/fixture/testrepos/start-git.sh
|
|||
helm-registry: test/fixture/testrepos/start-helm-registry.sh
|
||||
dev-mounter: [[ "$ARGOCD_E2E_TEST" != "true" ]] && go run hack/dev-mounter/main.go --configmap argocd-ssh-known-hosts-cm=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} --configmap argocd-tls-certs-cm=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} --configmap argocd-gpg-keys-cm=${ARGOCD_GPG_DATA_PATH:-/tmp/argocd-local/gpg/source}
|
||||
applicationset-controller: [ "$BIN_MODE" = 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "FORCE_LOG_COLORS=4 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} ARGOCD_BINARY_NAME=argocd-applicationset-controller $COMMAND --loglevel debug --metrics-addr localhost:12345 --probe-addr localhost:12346 --argocd-repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081}"
|
||||
notification: [ "$BIN_MODE" = 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "FORCE_LOG_COLORS=4 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_BINARY_NAME=argocd-notifications $COMMAND --loglevel debug"
|
||||
notification: [ "$BIN_MODE" = 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "FORCE_LOG_COLORS=4 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_BINARY_NAME=argocd-notifications $COMMAND --loglevel debug --application-namespaces=${ARGOCD_APPLICATION_NAMESPACES:-''} --self-service-notification-enabled=${ARGOCD_NOTIFICATION_CONTROLLER_SELF_SERVICE_NOTIFICATION_ENABLED:-'false'}"
|
||||
|
||||
|
|
|
|||
|
|
@ -524,6 +524,7 @@ func (r *ApplicationSetReconciler) generateApplications(logCtx *log.Entry, appli
|
|||
|
||||
for _, p := range a.Params {
|
||||
app, err := r.Renderer.RenderTemplateParams(tmplApplication, applicationSetInfo.Spec.SyncPolicy, p, applicationSetInfo.Spec.GoTemplate, applicationSetInfo.Spec.GoTemplateOptions)
|
||||
|
||||
if err != nil {
|
||||
logCtx.WithError(err).WithField("params", a.Params).WithField("generator", requestedGenerator).
|
||||
Error("error generating application from params")
|
||||
|
|
@ -534,6 +535,24 @@ func (r *ApplicationSetReconciler) generateApplications(logCtx *log.Entry, appli
|
|||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if applicationSetInfo.Spec.TemplatePatch != nil {
|
||||
patchedApplication, err := r.applyTemplatePatch(app, applicationSetInfo, p)
|
||||
|
||||
if err != nil {
|
||||
log.WithError(err).WithField("params", a.Params).WithField("generator", requestedGenerator).
|
||||
Error("error generating application from params")
|
||||
|
||||
if firstError == nil {
|
||||
firstError = err
|
||||
applicationSetReason = argov1alpha1.ApplicationSetReasonRenderTemplateParamsError
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
app = patchedApplication
|
||||
}
|
||||
|
||||
res = append(res, *app)
|
||||
}
|
||||
}
|
||||
|
|
@ -545,6 +564,16 @@ func (r *ApplicationSetReconciler) generateApplications(logCtx *log.Entry, appli
|
|||
return res, applicationSetReason, firstError
|
||||
}
|
||||
|
||||
func (r *ApplicationSetReconciler) applyTemplatePatch(app *argov1alpha1.Application, applicationSetInfo argov1alpha1.ApplicationSet, params map[string]interface{}) (*argov1alpha1.Application, error) {
|
||||
replacedTemplate, err := r.Renderer.Replace(*applicationSetInfo.Spec.TemplatePatch, params, applicationSetInfo.Spec.GoTemplate, applicationSetInfo.Spec.GoTemplateOptions)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error replacing values in templatePatch: %w", err)
|
||||
}
|
||||
|
||||
return applyTemplatePatch(app, replacedTemplate)
|
||||
}
|
||||
|
||||
func ignoreNotAllowedNamespaces(namespaces []string) predicate.Predicate {
|
||||
return predicate.Funcs{
|
||||
CreateFunc: func(e event.CreateEvent) bool {
|
||||
|
|
@ -619,6 +648,8 @@ func (r *ApplicationSetReconciler) createOrUpdateInCluster(ctx context.Context,
|
|||
var firstError error
|
||||
// Creates or updates the application in appList
|
||||
for _, generatedApp := range desiredApplications {
|
||||
// The app's namespace must be the same as the AppSet's namespace to preserve the appsets-in-any-namespace
|
||||
// security boundary.
|
||||
generatedApp.Namespace = applicationSet.Namespace
|
||||
|
||||
appLog := logCtx.WithFields(log.Fields{"app": generatedApp.QualifiedName()})
|
||||
|
|
|
|||
|
|
@ -86,6 +86,12 @@ func (g *generatorMock) GenerateParams(appSetGenerator *v1alpha1.ApplicationSetG
|
|||
return args.Get(0).([]map[string]interface{}), args.Error(1)
|
||||
}
|
||||
|
||||
func (g *generatorMock) Replace(tmpl string, replaceMap map[string]interface{}, useGoTemplate bool, goTemplateOptions []string) (string, error) {
|
||||
args := g.Called(tmpl, replaceMap, useGoTemplate, goTemplateOptions)
|
||||
|
||||
return args.Get(0).(string), args.Error(1)
|
||||
}
|
||||
|
||||
type rendererMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
|
@ -107,6 +113,12 @@ func (r *rendererMock) RenderTemplateParams(tmpl *v1alpha1.Application, syncPoli
|
|||
|
||||
}
|
||||
|
||||
func (r *rendererMock) Replace(tmpl string, replaceMap map[string]interface{}, useGoTemplate bool, goTemplateOptions []string) (string, error) {
|
||||
args := r.Called(tmpl, replaceMap, useGoTemplate, goTemplateOptions)
|
||||
|
||||
return args.Get(0).(string), args.Error(1)
|
||||
}
|
||||
|
||||
func TestExtractApplications(t *testing.T) {
|
||||
scheme := runtime.NewScheme()
|
||||
err := v1alpha1.AddToScheme(scheme)
|
||||
|
|
|
|||
46
applicationset/controllers/templatePatch.go
Normal file
46
applicationset/controllers/templatePatch.go
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/strategicpatch"
|
||||
|
||||
"github.com/argoproj/argo-cd/v2/applicationset/utils"
|
||||
appv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
|
||||
)
|
||||
|
||||
func applyTemplatePatch(app *appv1.Application, templatePatch string) (*appv1.Application, error) {
|
||||
|
||||
appString, err := json.Marshal(app)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while marhsalling Application %w", err)
|
||||
}
|
||||
|
||||
convertedTemplatePatch, err := utils.ConvertYAMLToJSON(templatePatch)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while converting template to json %q: %w", convertedTemplatePatch, err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(convertedTemplatePatch), &appv1.Application{}); err != nil {
|
||||
return nil, fmt.Errorf("invalid templatePatch %q: %w", convertedTemplatePatch, err)
|
||||
}
|
||||
|
||||
data, err := strategicpatch.StrategicMergePatch(appString, []byte(convertedTemplatePatch), appv1.Application{})
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while applying templatePatch template to json %q: %w", convertedTemplatePatch, err)
|
||||
}
|
||||
|
||||
finalApp := appv1.Application{}
|
||||
err = json.Unmarshal(data, &finalApp)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while unmarhsalling patched application: %w", err)
|
||||
}
|
||||
|
||||
// Prevent changes to the `project` field. This helps prevent malicious template patches
|
||||
finalApp.Spec.Project = app.Spec.Project
|
||||
|
||||
return &finalApp, nil
|
||||
}
|
||||
249
applicationset/controllers/templatePatch_test.go
Normal file
249
applicationset/controllers/templatePatch_test.go
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
appv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
|
||||
)
|
||||
|
||||
func Test_ApplyTemplatePatch(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
appTemplate *appv1.Application
|
||||
templatePatch string
|
||||
expectedApp *appv1.Application
|
||||
}{
|
||||
{
|
||||
name: "patch with JSON",
|
||||
appTemplate: &appv1.Application{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Application",
|
||||
APIVersion: "argoproj.io/v1alpha1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "my-cluster-guestbook",
|
||||
Namespace: "namespace",
|
||||
Finalizers: []string{"resources-finalizer.argocd.argoproj.io"},
|
||||
},
|
||||
Spec: appv1.ApplicationSpec{
|
||||
Project: "default",
|
||||
Source: &appv1.ApplicationSource{
|
||||
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
|
||||
TargetRevision: "HEAD",
|
||||
Path: "guestbook",
|
||||
},
|
||||
Destination: appv1.ApplicationDestination{
|
||||
Server: "https://kubernetes.default.svc",
|
||||
Namespace: "guestbook",
|
||||
},
|
||||
},
|
||||
},
|
||||
templatePatch: `{
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
"annotation-some-key": "annotation-some-value"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"source": {
|
||||
"helm": {
|
||||
"valueFiles": [
|
||||
"values.test.yaml",
|
||||
"values.big.yaml"
|
||||
]
|
||||
}
|
||||
},
|
||||
"syncPolicy": {
|
||||
"automated": {
|
||||
"prune": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
expectedApp: &appv1.Application{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Application",
|
||||
APIVersion: "argoproj.io/v1alpha1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "my-cluster-guestbook",
|
||||
Namespace: "namespace",
|
||||
Finalizers: []string{"resources-finalizer.argocd.argoproj.io"},
|
||||
Annotations: map[string]string{
|
||||
"annotation-some-key": "annotation-some-value",
|
||||
},
|
||||
},
|
||||
Spec: appv1.ApplicationSpec{
|
||||
Project: "default",
|
||||
Source: &appv1.ApplicationSource{
|
||||
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
|
||||
TargetRevision: "HEAD",
|
||||
Path: "guestbook",
|
||||
Helm: &appv1.ApplicationSourceHelm{
|
||||
ValueFiles: []string{
|
||||
"values.test.yaml",
|
||||
"values.big.yaml",
|
||||
},
|
||||
},
|
||||
},
|
||||
Destination: appv1.ApplicationDestination{
|
||||
Server: "https://kubernetes.default.svc",
|
||||
Namespace: "guestbook",
|
||||
},
|
||||
SyncPolicy: &appv1.SyncPolicy{
|
||||
Automated: &appv1.SyncPolicyAutomated{
|
||||
Prune: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "patch with YAML",
|
||||
appTemplate: &appv1.Application{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Application",
|
||||
APIVersion: "argoproj.io/v1alpha1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "my-cluster-guestbook",
|
||||
Namespace: "namespace",
|
||||
Finalizers: []string{"resources-finalizer.argocd.argoproj.io"},
|
||||
},
|
||||
Spec: appv1.ApplicationSpec{
|
||||
Project: "default",
|
||||
Source: &appv1.ApplicationSource{
|
||||
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
|
||||
TargetRevision: "HEAD",
|
||||
Path: "guestbook",
|
||||
},
|
||||
Destination: appv1.ApplicationDestination{
|
||||
Server: "https://kubernetes.default.svc",
|
||||
Namespace: "guestbook",
|
||||
},
|
||||
},
|
||||
},
|
||||
templatePatch: `
|
||||
metadata:
|
||||
annotations:
|
||||
annotation-some-key: annotation-some-value
|
||||
spec:
|
||||
source:
|
||||
helm:
|
||||
valueFiles:
|
||||
- values.test.yaml
|
||||
- values.big.yaml
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true`,
|
||||
expectedApp: &appv1.Application{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Application",
|
||||
APIVersion: "argoproj.io/v1alpha1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "my-cluster-guestbook",
|
||||
Namespace: "namespace",
|
||||
Finalizers: []string{"resources-finalizer.argocd.argoproj.io"},
|
||||
Annotations: map[string]string{
|
||||
"annotation-some-key": "annotation-some-value",
|
||||
},
|
||||
},
|
||||
Spec: appv1.ApplicationSpec{
|
||||
Project: "default",
|
||||
Source: &appv1.ApplicationSource{
|
||||
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
|
||||
TargetRevision: "HEAD",
|
||||
Path: "guestbook",
|
||||
Helm: &appv1.ApplicationSourceHelm{
|
||||
ValueFiles: []string{
|
||||
"values.test.yaml",
|
||||
"values.big.yaml",
|
||||
},
|
||||
},
|
||||
},
|
||||
Destination: appv1.ApplicationDestination{
|
||||
Server: "https://kubernetes.default.svc",
|
||||
Namespace: "guestbook",
|
||||
},
|
||||
SyncPolicy: &appv1.SyncPolicy{
|
||||
Automated: &appv1.SyncPolicyAutomated{
|
||||
Prune: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "project field isn't overwritten",
|
||||
appTemplate: &appv1.Application{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Application",
|
||||
APIVersion: "argoproj.io/v1alpha1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "my-cluster-guestbook",
|
||||
Namespace: "namespace",
|
||||
},
|
||||
Spec: appv1.ApplicationSpec{
|
||||
Project: "default",
|
||||
Source: &appv1.ApplicationSource{
|
||||
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
|
||||
TargetRevision: "HEAD",
|
||||
Path: "guestbook",
|
||||
},
|
||||
Destination: appv1.ApplicationDestination{
|
||||
Server: "https://kubernetes.default.svc",
|
||||
Namespace: "guestbook",
|
||||
},
|
||||
},
|
||||
},
|
||||
templatePatch: `
|
||||
spec:
|
||||
project: my-project`,
|
||||
expectedApp: &appv1.Application{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Application",
|
||||
APIVersion: "argoproj.io/v1alpha1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "my-cluster-guestbook",
|
||||
Namespace: "namespace",
|
||||
},
|
||||
Spec: appv1.ApplicationSpec{
|
||||
Project: "default",
|
||||
Source: &appv1.ApplicationSource{
|
||||
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
|
||||
TargetRevision: "HEAD",
|
||||
Path: "guestbook",
|
||||
},
|
||||
Destination: appv1.ApplicationDestination{
|
||||
Server: "https://kubernetes.default.svc",
|
||||
Namespace: "guestbook",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tcc := tc
|
||||
t.Run(tcc.name, func(t *testing.T) {
|
||||
result, err := applyTemplatePatch(tcc.appTemplate, tcc.templatePatch)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, *tcc.expectedApp, *result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestError(t *testing.T) {
|
||||
app := &appv1.Application{}
|
||||
|
||||
result, err := applyTemplatePatch(app, "hello world")
|
||||
require.Error(t, err)
|
||||
require.Nil(t, result)
|
||||
}
|
||||
|
|
@ -41,6 +41,7 @@ func init() {
|
|||
|
||||
type Renderer interface {
|
||||
RenderTemplateParams(tmpl *argoappsv1.Application, syncPolicy *argoappsv1.ApplicationSetSyncPolicy, params map[string]interface{}, useGoTemplate bool, goTemplateOptions []string) (*argoappsv1.Application, error)
|
||||
Replace(tmpl string, replaceMap map[string]interface{}, useGoTemplate bool, goTemplateOptions []string) (string, error)
|
||||
}
|
||||
|
||||
type Render struct {
|
||||
|
|
|
|||
|
|
@ -6143,6 +6143,9 @@
|
|||
},
|
||||
"template": {
|
||||
"$ref": "#/definitions/v1alpha1ApplicationSetTemplate"
|
||||
},
|
||||
"templatePatch": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -43,19 +43,20 @@ func addK8SFlagsToCmd(cmd *cobra.Command) clientcmd.ClientConfig {
|
|||
|
||||
func NewCommand() *cobra.Command {
|
||||
var (
|
||||
clientConfig clientcmd.ClientConfig
|
||||
processorsCount int
|
||||
namespace string
|
||||
appLabelSelector string
|
||||
logLevel string
|
||||
logFormat string
|
||||
metricsPort int
|
||||
argocdRepoServer string
|
||||
argocdRepoServerPlaintext bool
|
||||
argocdRepoServerStrictTLS bool
|
||||
configMapName string
|
||||
secretName string
|
||||
applicationNamespaces []string
|
||||
clientConfig clientcmd.ClientConfig
|
||||
processorsCount int
|
||||
namespace string
|
||||
appLabelSelector string
|
||||
logLevel string
|
||||
logFormat string
|
||||
metricsPort int
|
||||
argocdRepoServer string
|
||||
argocdRepoServerPlaintext bool
|
||||
argocdRepoServerStrictTLS bool
|
||||
configMapName string
|
||||
secretName string
|
||||
applicationNamespaces []string
|
||||
selfServiceNotificationEnabled bool
|
||||
)
|
||||
var command = cobra.Command{
|
||||
Use: "controller",
|
||||
|
|
@ -139,7 +140,7 @@ func NewCommand() *cobra.Command {
|
|||
log.Infof("serving metrics on port %d", metricsPort)
|
||||
log.Infof("loading configuration %d", metricsPort)
|
||||
|
||||
ctrl := notificationscontroller.NewController(k8sClient, dynamicClient, argocdService, namespace, applicationNamespaces, appLabelSelector, registry, secretName, configMapName)
|
||||
ctrl := notificationscontroller.NewController(k8sClient, dynamicClient, argocdService, namespace, applicationNamespaces, appLabelSelector, registry, secretName, configMapName, selfServiceNotificationEnabled)
|
||||
err = ctrl.Init(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize controller: %w", err)
|
||||
|
|
@ -163,5 +164,6 @@ func NewCommand() *cobra.Command {
|
|||
command.Flags().StringVar(&configMapName, "config-map-name", "argocd-notifications-cm", "Set notifications ConfigMap name")
|
||||
command.Flags().StringVar(&secretName, "secret-name", "argocd-notifications-secret", "Set notifications Secret name")
|
||||
command.Flags().StringSliceVar(&applicationNamespaces, "application-namespaces", env.StringsFromEnv("ARGOCD_APPLICATION_NAMESPACES", []string{}, ","), "List of additional namespaces that this controller should send notifications for")
|
||||
command.Flags().BoolVar(&selfServiceNotificationEnabled, "self-service-notification-enabled", env.ParseBoolFromEnv("ARGOCD_NOTIFICATION_CONTROLLER_SELF_SERVICE_NOTIFICATION_ENABLED", false), "Allows the Argo CD notification controller to pull notification config from the namespace that the resource is in. This is useful for self-service notification.")
|
||||
return &command
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,15 @@ func NewClusterCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clientc
|
|||
var command = &cobra.Command{
|
||||
Use: "cluster",
|
||||
Short: "Manage clusters configuration",
|
||||
Example: `
|
||||
#Generate declarative config for a cluster
|
||||
argocd admin cluster generate-spec my-cluster -o yaml
|
||||
|
||||
#Generate a kubeconfig for a cluster named "my-cluster" and display it in the console
|
||||
argocd admin cluster kubeconfig my-cluster
|
||||
|
||||
#Print information namespaces which Argo CD manages in each cluster
|
||||
argocd admin cluster namespaces my-cluster `,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
c.HelpFunc()(c, args)
|
||||
},
|
||||
|
|
@ -460,6 +469,15 @@ func NewClusterStatsCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comma
|
|||
var command = cobra.Command{
|
||||
Use: "stats",
|
||||
Short: "Prints information cluster statistics and inferred shard number",
|
||||
Example: `
|
||||
#Display stats and shards for clusters
|
||||
argocd admin cluster stats
|
||||
|
||||
#Display Cluster Statistics for a Specific Shard
|
||||
argocd admin cluster stats --shard=1
|
||||
|
||||
#In a multi-cluster environment to print stats for a specific cluster say(target-cluster)
|
||||
argocd admin cluster stats target-cluster`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
ctx := cmd.Context()
|
||||
|
||||
|
|
@ -510,6 +528,18 @@ func NewClusterConfig() *cobra.Command {
|
|||
Use: "kubeconfig CLUSTER_URL OUTPUT_PATH",
|
||||
Short: "Generates kubeconfig for the specified cluster",
|
||||
DisableAutoGenTag: true,
|
||||
Example: `
|
||||
#Generate a kubeconfig for a cluster named "my-cluster" on console
|
||||
argocd admin cluster kubeconfig my-cluster
|
||||
|
||||
#Listing available kubeconfigs for clusters managed by argocd
|
||||
argocd admin cluster kubeconfig
|
||||
|
||||
#Removing a specific kubeconfig file
|
||||
argocd admin cluster kubeconfig my-cluster --delete
|
||||
|
||||
#Generate a Kubeconfig for a Cluster with TLS Verification Disabled
|
||||
argocd admin cluster kubeconfig https://cluster-api-url:6443 /path/to/output/kubeconfig.yaml --insecure-skip-tls-verify`,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
ctx := c.Context()
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ func NewNotificationsCommand() *cobra.Command {
|
|||
"notifications",
|
||||
"argocd admin notifications",
|
||||
applications,
|
||||
settings.GetFactorySettings(argocdService, "argocd-notifications-secret", "argocd-notifications-cm"), func(clientConfig clientcmd.ClientConfig) {
|
||||
settings.GetFactorySettings(argocdService, "argocd-notifications-secret", "argocd-notifications-cm", false), func(clientConfig clientcmd.ClientConfig) {
|
||||
k8sCfg, err := clientConfig.ClientConfig()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse k8s config: %v", err)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,18 @@ func NewProjectWindowsCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com
|
|||
roleCommand := &cobra.Command{
|
||||
Use: "windows",
|
||||
Short: "Manage a project's sync windows",
|
||||
Example: `
|
||||
#Add a sync window to a project
|
||||
argocd proj windows add my-project \
|
||||
--schedule "0 0 * * 1-5" \
|
||||
--duration 3600 \
|
||||
--prune
|
||||
|
||||
#Delete a sync window from a project
|
||||
argocd proj windows delete <project-name> <window-id>
|
||||
|
||||
#List project sync windows
|
||||
argocd proj windows list <project-name>`,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
|
|
@ -42,6 +54,12 @@ func NewProjectWindowsDisableManualSyncCommand(clientOpts *argocdclient.ClientOp
|
|||
Use: "disable-manual-sync PROJECT ID",
|
||||
Short: "Disable manual sync for a sync window",
|
||||
Long: "Disable manual sync for a sync window. Requires ID which can be found by running \"argocd proj windows list PROJECT\"",
|
||||
Example: `
|
||||
#Disable manual sync for a sync window for the Project
|
||||
argocd proj windows disable-manual-sync PROJECT ID
|
||||
|
||||
#Disbaling manual sync for a windows set on the default project with Id 0
|
||||
argocd proj windows disable-manual-sync default 0`,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
ctx := c.Context()
|
||||
|
||||
|
|
@ -79,6 +97,15 @@ func NewProjectWindowsEnableManualSyncCommand(clientOpts *argocdclient.ClientOpt
|
|||
Use: "enable-manual-sync PROJECT ID",
|
||||
Short: "Enable manual sync for a sync window",
|
||||
Long: "Enable manual sync for a sync window. Requires ID which can be found by running \"argocd proj windows list PROJECT\"",
|
||||
Example: `
|
||||
#Enabling manual sync for a general case
|
||||
argocd proj windows enable-manual-sync PROJECT ID
|
||||
|
||||
#Enabling manual sync for a windows set on the default project with Id 2
|
||||
argocd proj windows enable-manual-sync default 2
|
||||
|
||||
#Enabling manual sync with a custom message
|
||||
argocd proj windows enable-manual-sync my-app-project --message "Manual sync initiated by admin`,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
ctx := c.Context()
|
||||
|
||||
|
|
@ -125,14 +152,15 @@ func NewProjectWindowsAddWindowCommand(clientOpts *argocdclient.ClientOptions) *
|
|||
var command = &cobra.Command{
|
||||
Use: "add PROJECT",
|
||||
Short: "Add a sync window to a project",
|
||||
Example: `# Add a 1 hour allow sync window
|
||||
Example: `
|
||||
#Add a 1 hour allow sync window
|
||||
argocd proj windows add PROJECT \
|
||||
--kind allow \
|
||||
--schedule "0 22 * * *" \
|
||||
--duration 1h \
|
||||
--applications "*"
|
||||
|
||||
# Add a deny sync window with the ability to manually sync.
|
||||
#Add a deny sync window with the ability to manually sync.
|
||||
argocd proj windows add PROJECT \
|
||||
--kind deny \
|
||||
--schedule "30 10 * * *" \
|
||||
|
|
@ -180,6 +208,12 @@ func NewProjectWindowsDeleteCommand(clientOpts *argocdclient.ClientOptions) *cob
|
|||
var command = &cobra.Command{
|
||||
Use: "delete PROJECT ID",
|
||||
Short: "Delete a sync window from a project. Requires ID which can be found by running \"argocd proj windows list PROJECT\"",
|
||||
Example: `
|
||||
#Delete a sync window from a project (default) with ID 0
|
||||
argocd proj windows delete default 0
|
||||
|
||||
#Delete a sync window from a project (new-project) with ID 1
|
||||
argocd proj windows delete new-project 1`,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
ctx := c.Context()
|
||||
|
||||
|
|
@ -274,12 +308,15 @@ func NewProjectWindowsListCommand(clientOpts *argocdclient.ClientOptions) *cobra
|
|||
var command = &cobra.Command{
|
||||
Use: "list PROJECT",
|
||||
Short: "List project sync windows",
|
||||
Example: `# List project windows
|
||||
Example: `
|
||||
#List project windows
|
||||
argocd proj windows list PROJECT
|
||||
|
||||
# List project windows in yaml format
|
||||
|
||||
#List project windows in yaml format
|
||||
argocd proj windows list PROJECT -o yaml
|
||||
`,
|
||||
|
||||
#List project windows info for a project name (test-project)
|
||||
argocd proj windows list test-project`,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
ctx := c.Context()
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,10 @@ Some manual steps will need to be performed by the Argo CD administrator in orde
|
|||
|
||||
!!! note
|
||||
This feature is considered beta as of now. Some of the implementation details may change over the course of time until it is promoted to a stable status. We will be happy if early adopters use this feature and provide us with bug reports and feedback.
|
||||
|
||||
|
||||
|
||||
One additional advantage of adopting applications in any namespace is to allow end-users to configure notifications for their Argo CD application in the namespace where Argo CD application is running in. See notifications [namespace based configuration](notifications/index.md#namespace-based-configuration) page for more information.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Cluster-scoped Argo CD installation
|
||||
|
|
|
|||
|
|
@ -108,3 +108,69 @@ spec:
|
|||
(*The full example can be found [here](https://github.com/argoproj/argo-cd/tree/master/applicationset/examples/template-override).*)
|
||||
|
||||
In this example, the ApplicationSet controller will generate an `Application` resource using the `path` generated by the List generator, rather than the `path` value defined in `.spec.template`.
|
||||
|
||||
## Template Patch
|
||||
|
||||
Templating is only available on string type. However, some uses cases may require to apply templating on other types.
|
||||
|
||||
Example:
|
||||
|
||||
- Set the automated sync policy
|
||||
- Switch prune boolean to true
|
||||
- Add multiple helm value files
|
||||
|
||||
Argo CD has a `templatePatch` feature to allow advanced templating. It supports both json and yaml.
|
||||
|
||||
|
||||
```yaml
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: ApplicationSet
|
||||
metadata:
|
||||
name: guestbook
|
||||
spec:
|
||||
goTemplate: true
|
||||
generators:
|
||||
- list:
|
||||
elements:
|
||||
- cluster: engineering-dev
|
||||
url: https://kubernetes.default.svc
|
||||
autoSync: true
|
||||
prune: true
|
||||
valueFiles:
|
||||
- values.large.yaml
|
||||
- values.debug.yaml
|
||||
template:
|
||||
metadata:
|
||||
name: '{{.cluster}}-deployment'
|
||||
spec:
|
||||
project: "default"
|
||||
source:
|
||||
repoURL: https://github.com/infra-team/cluster-deployments.git
|
||||
targetRevision: HEAD
|
||||
path: guestbook/{{ .cluster }}
|
||||
destination:
|
||||
server: '{{.url}}'
|
||||
namespace: guestbook
|
||||
templatePatch: |
|
||||
spec:
|
||||
source:
|
||||
helm:
|
||||
valueFiles:
|
||||
{{- range $valueFile := .valueFiles }}
|
||||
- {{ $valueFile | toJson }}
|
||||
{{- end }}
|
||||
{{- if .autoSync }}
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: {{ .prune | toJson }}
|
||||
{{- end }}
|
||||
```
|
||||
|
||||
!!! important
|
||||
The `templatePatch` can apply arbitrary changes to the template. If parameters include untrustworthy user input, it
|
||||
may be possible to inject malicious changes into the template. It is recommended to use `templatePatch` only with
|
||||
trusted input or to carefully escape the input before using it in the template. Piping input to `toJson` should help
|
||||
prevent, for example, a user from successfully injecting a string with newlines.
|
||||
|
||||
The `spec.project` field is not supported in `templatePatch`. If you need to change the project, you can use the
|
||||
`spec.project` field in the `template` field.
|
||||
|
|
|
|||
|
|
@ -210,3 +210,5 @@ data:
|
|||
notificationscontroller.log.level: "info"
|
||||
# Set the logging format. One of: text|json (default "text")
|
||||
notificationscontroller.log.format: "text"
|
||||
# Enable self-service notifications config. Used in conjunction with apps-in-any-namespace. (default "false")
|
||||
notificationscontroller.selfservice.enabled: "false"
|
||||
|
|
|
|||
|
|
@ -45,3 +45,71 @@ So you can just use them instead of reinventing new ones.
|
|||
```
|
||||
|
||||
Try syncing an application to get notified when the sync is completed.
|
||||
|
||||
## Namespace based configuration
|
||||
|
||||
A common installation method for Argo CD Notifications is to install it in a dedicated namespace to manage a whole cluster. In this case, the administrator is the only
|
||||
person who can configure notifications in that namespace generally. However, in some cases, it is required to allow end-users to configure notifications
|
||||
for their Argo CD applications. For example, the end-user can configure notifications for their Argo CD application in the namespace where they have access to and their Argo CD application is running in.
|
||||
|
||||
This feature is based on applications in any namespace. See [applications in any namespace](../app-any-namespace.md) page for more information.
|
||||
|
||||
In order to enable this feature, the Argo CD administrator must reconfigure the argocd-notification-controller workloads to add `--application-namespaces` and `--self-service-notification-enabled` parameters to the container's startup command.
|
||||
`--application-namespaces` controls the list of namespaces that Argo CD applications are in. `--self-service-notification-enabled` turns on this feature.
|
||||
|
||||
The startup parameters for both can also be conveniently set up and kept in sync by specifying
|
||||
the `application.namespaces` and `notificationscontroller.selfservice.enabled` in the argocd-cmd-params-cm ConfigMap instead of changing the manifests for the respective workloads. For example:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: argocd-cmd-params-cm
|
||||
data:
|
||||
application.namespaces: app-team-one, app-team-two
|
||||
notificationscontroller.selfservice.enabled: true
|
||||
```
|
||||
|
||||
To use this feature, you can deploy configmap named `argocd-notifications-cm` and possibly a secret `argocd-notifications-secret` in the namespace where the Argo CD application lives.
|
||||
|
||||
When it is configured this way the controller will send notifications using both the controller level configuration (the configmap located in the same namespaces as the controller) as well as
|
||||
the configuration located in the same namespace where the Argo CD application is at.
|
||||
|
||||
Example: Application team wants to receive notifications using PagerDutyV2, when the controller level configuration is only supporting Slack.
|
||||
|
||||
The following two resources are deployed in the namespace where the Argo CD application lives.
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: argocd-notifications-cm
|
||||
data:
|
||||
service.pagerdutyv2: |
|
||||
serviceKeys:
|
||||
my-service: $pagerduty-key-my-service
|
||||
...
|
||||
```
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: argo-cd-notification-secret
|
||||
type: Opaque
|
||||
data:
|
||||
pagerduty-key-my-service: <pd-integration-key>
|
||||
```
|
||||
|
||||
When an Argo CD application has the following subscriptions, user receives application sync failure message from pager duty.
|
||||
```yaml
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
annotations:
|
||||
notifications.argoproj.io/subscribe.on-sync-failed.pagerdutyv2: "<serviceID for Pagerduty>"
|
||||
```
|
||||
|
||||
!!! note
|
||||
When the same notification service and trigger are defined in controller level configuration and application level configuration,
|
||||
both notifications will be sent according to its own configuration.
|
||||
|
||||
[Defining and using secrets within notification templates](templates.md/#defining-and-using-secrets-within-notification-templates) function is not available when flag `--self-service-notification-enable` is on.
|
||||
|
|
|
|||
|
|
@ -8,6 +8,20 @@ Manage clusters configuration
|
|||
argocd admin cluster [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
|
||||
#Generate declarative config for a cluster
|
||||
argocd admin cluster generate-spec my-cluster -o yaml
|
||||
|
||||
#Generate a kubeconfig for a cluster named "my-cluster" and display it in the console
|
||||
argocd admin cluster kubeconfig my-cluster
|
||||
|
||||
#Print information namespaces which Argo CD manages in each cluster
|
||||
argocd admin cluster namespaces my-cluster
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -8,6 +8,23 @@ Generates kubeconfig for the specified cluster
|
|||
argocd admin cluster kubeconfig CLUSTER_URL OUTPUT_PATH [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
|
||||
#Generate a kubeconfig for a cluster named "my-cluster" on console
|
||||
argocd admin cluster kubeconfig my-cluster
|
||||
|
||||
#Listing available kubeconfigs for clusters managed by argocd
|
||||
argocd admin cluster kubeconfig
|
||||
|
||||
#Removing a specific kubeconfig file
|
||||
argocd admin cluster kubeconfig my-cluster --delete
|
||||
|
||||
#Generate a Kubeconfig for a Cluster with TLS Verification Disabled
|
||||
argocd admin cluster kubeconfig https://cluster-api-url:6443 /path/to/output/kubeconfig.yaml --insecure-skip-tls-verify
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -8,6 +8,20 @@ Prints information cluster statistics and inferred shard number
|
|||
argocd admin cluster stats [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
|
||||
#Display stats and shards for clusters
|
||||
argocd admin cluster stats
|
||||
|
||||
#Display Cluster Statistics for a Specific Shard
|
||||
argocd admin cluster stats --shard=1
|
||||
|
||||
#In a multi-cluster environment to print stats for a specific cluster say(target-cluster)
|
||||
argocd admin cluster stats target-cluster
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -8,6 +8,23 @@ Manage a project's sync windows
|
|||
argocd proj windows [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
|
||||
#Add a sync window to a project
|
||||
argocd proj windows add my-project \
|
||||
--schedule "0 0 * * 1-5" \
|
||||
--duration 3600 \
|
||||
--prune
|
||||
|
||||
#Delete a sync window from a project
|
||||
argocd proj windows delete <project-name> <window-id>
|
||||
|
||||
#List project sync windows
|
||||
argocd proj windows list <project-name>
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -11,14 +11,15 @@ argocd proj windows add PROJECT [flags]
|
|||
### Examples
|
||||
|
||||
```
|
||||
# Add a 1 hour allow sync window
|
||||
|
||||
#Add a 1 hour allow sync window
|
||||
argocd proj windows add PROJECT \
|
||||
--kind allow \
|
||||
--schedule "0 22 * * *" \
|
||||
--duration 1h \
|
||||
--applications "*"
|
||||
|
||||
# Add a deny sync window with the ability to manually sync.
|
||||
#Add a deny sync window with the ability to manually sync.
|
||||
argocd proj windows add PROJECT \
|
||||
--kind deny \
|
||||
--schedule "30 10 * * *" \
|
||||
|
|
|
|||
|
|
@ -8,6 +8,17 @@ Delete a sync window from a project. Requires ID which can be found by running "
|
|||
argocd proj windows delete PROJECT ID [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
|
||||
#Delete a sync window from a project (default) with ID 0
|
||||
argocd proj windows delete default 0
|
||||
|
||||
#Delete a sync window from a project (new-project) with ID 1
|
||||
argocd proj windows delete new-project 1
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -12,6 +12,17 @@ Disable manual sync for a sync window. Requires ID which can be found by running
|
|||
argocd proj windows disable-manual-sync PROJECT ID [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
|
||||
#Disable manual sync for a sync window for the Project
|
||||
argocd proj windows disable-manual-sync PROJECT ID
|
||||
|
||||
#Disbaling manual sync for a windows set on the default project with Id 0
|
||||
argocd proj windows disable-manual-sync default 0
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -12,6 +12,20 @@ Enable manual sync for a sync window. Requires ID which can be found by running
|
|||
argocd proj windows enable-manual-sync PROJECT ID [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
|
||||
#Enabling manual sync for a general case
|
||||
argocd proj windows enable-manual-sync PROJECT ID
|
||||
|
||||
#Enabling manual sync for a windows set on the default project with Id 2
|
||||
argocd proj windows enable-manual-sync default 2
|
||||
|
||||
#Enabling manual sync with a custom message
|
||||
argocd proj windows enable-manual-sync my-app-project --message "Manual sync initiated by admin
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -11,12 +11,15 @@ argocd proj windows list PROJECT [flags]
|
|||
### Examples
|
||||
|
||||
```
|
||||
# List project windows
|
||||
|
||||
#List project windows
|
||||
argocd proj windows list PROJECT
|
||||
|
||||
# List project windows in yaml format
|
||||
|
||||
#List project windows in yaml format
|
||||
argocd proj windows list PROJECT -o yaml
|
||||
|
||||
#List project windows info for a project name (test-project)
|
||||
argocd proj windows list test-project
|
||||
```
|
||||
|
||||
### Options
|
||||
|
|
|
|||
|
|
@ -16,4 +16,13 @@ rules:
|
|||
- list
|
||||
- watch
|
||||
- update
|
||||
- patch
|
||||
- patch
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- secrets
|
||||
- configmaps
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
|
|
@ -54,6 +54,12 @@ spec:
|
|||
key: application.namespaces
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_NOTIFICATION_CONTROLLER_SELF_SERVICE_NOTIFICATION_ENABLED
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: notificationscontroller.selfservice.enabled
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
workingDir: /app
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
|
|
|
|||
|
|
@ -20123,6 +20123,8 @@ spec:
|
|||
- metadata
|
||||
- spec
|
||||
type: object
|
||||
templatePatch:
|
||||
type: string
|
||||
required:
|
||||
- generators
|
||||
- template
|
||||
|
|
|
|||
|
|
@ -15183,6 +15183,8 @@ spec:
|
|||
- metadata
|
||||
- spec
|
||||
type: object
|
||||
templatePatch:
|
||||
type: string
|
||||
required:
|
||||
- generators
|
||||
- template
|
||||
|
|
|
|||
|
|
@ -20123,6 +20123,8 @@ spec:
|
|||
- metadata
|
||||
- spec
|
||||
type: object
|
||||
templatePatch:
|
||||
type: string
|
||||
required:
|
||||
- generators
|
||||
- template
|
||||
|
|
@ -22472,6 +22474,12 @@ spec:
|
|||
key: application.namespaces
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_NOTIFICATION_CONTROLLER_SELF_SERVICE_NOTIFICATION_ENABLED
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: notificationscontroller.selfservice.enabled
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:latest
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
|
|
|
|||
|
|
@ -1859,6 +1859,12 @@ spec:
|
|||
key: application.namespaces
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_NOTIFICATION_CONTROLLER_SELF_SERVICE_NOTIFICATION_ENABLED
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: notificationscontroller.selfservice.enabled
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:latest
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
|
|
|
|||
|
|
@ -20123,6 +20123,8 @@ spec:
|
|||
- metadata
|
||||
- spec
|
||||
type: object
|
||||
templatePatch:
|
||||
type: string
|
||||
required:
|
||||
- generators
|
||||
- template
|
||||
|
|
@ -21567,6 +21569,12 @@ spec:
|
|||
key: application.namespaces
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_NOTIFICATION_CONTROLLER_SELF_SERVICE_NOTIFICATION_ENABLED
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: notificationscontroller.selfservice.enabled
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:latest
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
|
|
|
|||
|
|
@ -954,6 +954,12 @@ spec:
|
|||
key: application.namespaces
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_NOTIFICATION_CONTROLLER_SELF_SERVICE_NOTIFICATION_ENABLED
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: notificationscontroller.selfservice.enabled
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:latest
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
|
|
|
|||
|
|
@ -63,19 +63,27 @@ func NewController(
|
|||
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}, "")
|
||||
secretInformer := k8s.NewSecretInformer(k8sClient, namespace, secretName)
|
||||
configMapInformer := k8s.NewConfigMapInformer(k8sClient, namespace, configMapName)
|
||||
apiFactory := api.NewFactory(settings.GetFactorySettings(argocdService, secretName, configMapName), namespace, secretInformer, configMapInformer)
|
||||
var notificationConfigNamespace string
|
||||
if selfServiceNotificationEnabled {
|
||||
notificationConfigNamespace = v1.NamespaceAll
|
||||
} else {
|
||||
notificationConfigNamespace = namespace
|
||||
}
|
||||
secretInformer := k8s.NewSecretInformer(k8sClient, notificationConfigNamespace, secretName)
|
||||
configMapInformer := k8s.NewConfigMapInformer(k8sClient, notificationConfigNamespace, configMapName)
|
||||
apiFactory := api.NewFactory(settings.GetFactorySettings(argocdService, secretName, configMapName, selfServiceNotificationEnabled), namespace, secretInformer, configMapInformer)
|
||||
|
||||
res := ¬ificationController{
|
||||
secretInformer: secretInformer,
|
||||
|
|
@ -83,19 +91,30 @@ func NewController(
|
|||
appInformer: appInformer,
|
||||
appProjInformer: appProjInformer,
|
||||
apiFactory: apiFactory}
|
||||
res.ctrl = controller.NewController(namespaceableAppClient, appInformer, apiFactory,
|
||||
controller.WithSkipProcessing(func(obj v1.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"
|
||||
}),
|
||||
controller.WithMetricsRegistry(registry),
|
||||
controller.WithAlterDestinations(res.alterDestinations))
|
||||
skipProcessingOpt := controller.WithSkipProcessing(func(obj v1.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
|
||||
}
|
||||
|
||||
|
|
@ -118,6 +137,7 @@ func (c *notificationController) alterDestinations(obj v1.Object, destinations s
|
|||
}
|
||||
|
||||
func newInformer(resClient dynamic.ResourceInterface, controllerNamespace string, applicationNamespaces []string, selector string) cache.SharedIndexInformer {
|
||||
|
||||
informer := cache.NewSharedIndexInformer(
|
||||
&cache.ListWatch{
|
||||
ListFunc: func(options v1.ListOptions) (runtime.Object, error) {
|
||||
|
|
|
|||
|
|
@ -110,26 +110,30 @@ func TestInit(t *testing.T) {
|
|||
k8sClient := k8sfake.NewSimpleClientset()
|
||||
appLabelSelector := "app=test"
|
||||
|
||||
nc := NewController(
|
||||
k8sClient,
|
||||
dynamicClient,
|
||||
nil,
|
||||
"default",
|
||||
[]string{},
|
||||
appLabelSelector,
|
||||
nil,
|
||||
"my-secret",
|
||||
"my-configmap",
|
||||
)
|
||||
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)
|
||||
assert.NotNil(t, nc)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err = nc.Init(ctx)
|
||||
err = nc.Init(ctx)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitTimeout(t *testing.T) {
|
||||
|
|
@ -152,6 +156,7 @@ func TestInitTimeout(t *testing.T) {
|
|||
nil,
|
||||
"my-secret",
|
||||
"my-configmap",
|
||||
false,
|
||||
)
|
||||
|
||||
assert.NotNil(t, nc)
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ type ApplicationSetSpec struct {
|
|||
// ApplyNestedSelectors enables selectors defined within the generators of two level-nested matrix or merge generators
|
||||
ApplyNestedSelectors bool `json:"applyNestedSelectors,omitempty" protobuf:"bytes,8,name=applyNestedSelectors"`
|
||||
IgnoreApplicationDifferences ApplicationSetIgnoreDifferences `json:"ignoreApplicationDifferences,omitempty" protobuf:"bytes,9,name=ignoreApplicationDifferences"`
|
||||
TemplatePatch *string `json:"templatePatch,omitempty" protobuf:"bytes,10,name=templatePatch"`
|
||||
}
|
||||
|
||||
type ApplicationPreservedFields struct {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -318,6 +318,8 @@ message ApplicationSetSpec {
|
|||
optional bool applyNestedSelectors = 8;
|
||||
|
||||
repeated ApplicationSetResourceIgnoreDifferences ignoreApplicationDifferences = 9;
|
||||
|
||||
optional string templatePatch = 10;
|
||||
}
|
||||
|
||||
// ApplicationSetStatus defines the observed state of ApplicationSet
|
||||
|
|
|
|||
|
|
@ -1287,6 +1287,12 @@ func schema_pkg_apis_application_v1alpha1_ApplicationSetSpec(ref common.Referenc
|
|||
},
|
||||
},
|
||||
},
|
||||
"templatePatch": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"generators", "template"},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -733,6 +733,11 @@ func (in *ApplicationSetSpec) DeepCopyInto(out *ApplicationSetSpec) {
|
|||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.TemplatePatch != nil {
|
||||
in, out := &in.TemplatePatch, &out.TemplatePatch
|
||||
*out = new(string)
|
||||
**out = **in
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
71
reposerver/cache/mocks/reposervercache.go
vendored
Normal file
71
reposerver/cache/mocks/reposervercache.go
vendored
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
package mocks
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
cacheutil "github.com/argoproj/argo-cd/v2/util/cache"
|
||||
cacheutilmocks "github.com/argoproj/argo-cd/v2/util/cache/mocks"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
type MockCacheType int
|
||||
|
||||
const (
|
||||
MockCacheTypeRedis MockCacheType = iota
|
||||
MockCacheTypeInMem
|
||||
)
|
||||
|
||||
type MockRepoCache struct {
|
||||
mock.Mock
|
||||
RedisClient *cacheutilmocks.MockCacheClient
|
||||
StopRedisCallback func()
|
||||
}
|
||||
|
||||
type MockCacheOptions struct {
|
||||
RepoCacheExpiration time.Duration
|
||||
RevisionCacheExpiration time.Duration
|
||||
ReadDelay time.Duration
|
||||
WriteDelay time.Duration
|
||||
}
|
||||
|
||||
type CacheCallCounts struct {
|
||||
ExternalSets int
|
||||
ExternalGets int
|
||||
ExternalDeletes int
|
||||
}
|
||||
|
||||
// Checks that the cache was called the expected number of times
|
||||
func (mockCache *MockRepoCache) AssertCacheCalledTimes(t *testing.T, calls *CacheCallCounts) {
|
||||
mockCache.RedisClient.AssertNumberOfCalls(t, "Get", calls.ExternalGets)
|
||||
mockCache.RedisClient.AssertNumberOfCalls(t, "Set", calls.ExternalSets)
|
||||
mockCache.RedisClient.AssertNumberOfCalls(t, "Delete", calls.ExternalDeletes)
|
||||
}
|
||||
|
||||
func (mockCache *MockRepoCache) ConfigureDefaultCallbacks() {
|
||||
mockCache.RedisClient.On("Get", mock.Anything, mock.Anything).Return(nil)
|
||||
mockCache.RedisClient.On("Set", mock.Anything).Return(nil)
|
||||
mockCache.RedisClient.On("Delete", mock.Anything).Return(nil)
|
||||
}
|
||||
|
||||
func NewInMemoryRedis() (*redis.Client, func()) {
|
||||
cacheutil.NewInMemoryCache(5 * time.Second)
|
||||
mr, err := miniredis.Run()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return redis.NewClient(&redis.Options{Addr: mr.Addr()}), mr.Close
|
||||
}
|
||||
|
||||
func NewMockRepoCache(cacheOpts *MockCacheOptions) *MockRepoCache {
|
||||
redisClient, stopRedis := NewInMemoryRedis()
|
||||
redisCacheClient := &cacheutilmocks.MockCacheClient{
|
||||
ReadDelay: cacheOpts.ReadDelay,
|
||||
WriteDelay: cacheOpts.WriteDelay,
|
||||
BaseCache: cacheutil.NewRedisCache(redisClient, cacheOpts.RepoCacheExpiration, cacheutil.RedisCompressionNone)}
|
||||
newMockCache := &MockRepoCache{RedisClient: redisCacheClient, StopRedisCallback: stopRedis}
|
||||
newMockCache.ConfigureDefaultCallbacks()
|
||||
return newMockCache
|
||||
}
|
||||
|
|
@ -300,6 +300,7 @@ func (s *Service) runRepoOperation(
|
|||
var gitClient git.Client
|
||||
var helmClient helm.Client
|
||||
var err error
|
||||
gitClientOpts := git.WithCache(s.cache, !settings.noRevisionCache && !settings.noCache)
|
||||
revision = textutils.FirstNonEmpty(revision, source.TargetRevision)
|
||||
unresolvedRevision := revision
|
||||
if source.IsHelm() {
|
||||
|
|
@ -308,13 +309,13 @@ func (s *Service) runRepoOperation(
|
|||
return err
|
||||
}
|
||||
} else {
|
||||
gitClient, revision, err = s.newClientResolveRevision(repo, revision, git.WithCache(s.cache, !settings.noRevisionCache && !settings.noCache))
|
||||
gitClient, revision, err = s.newClientResolveRevision(repo, revision, gitClientOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
repoRefs, err := resolveReferencedSources(hasMultipleSources, source.Helm, refSources, s.newClientResolveRevision)
|
||||
repoRefs, err := resolveReferencedSources(hasMultipleSources, source.Helm, refSources, s.newClientResolveRevision, gitClientOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -463,7 +464,7 @@ type gitClientGetter func(repo *v1alpha1.Repository, revision string, opts ...gi
|
|||
//
|
||||
// Much of this logic is duplicated in runManifestGenAsync. If making changes here, check whether runManifestGenAsync
|
||||
// should be updated.
|
||||
func resolveReferencedSources(hasMultipleSources bool, source *v1alpha1.ApplicationSourceHelm, refSources map[string]*v1alpha1.RefTarget, newClientResolveRevision gitClientGetter) (map[string]string, error) {
|
||||
func resolveReferencedSources(hasMultipleSources bool, source *v1alpha1.ApplicationSourceHelm, refSources map[string]*v1alpha1.RefTarget, newClientResolveRevision gitClientGetter, gitClientOpts git.ClientOpts) (map[string]string, error) {
|
||||
repoRefs := make(map[string]string)
|
||||
if !hasMultipleSources || source == nil {
|
||||
return repoRefs, nil
|
||||
|
|
@ -490,7 +491,7 @@ func resolveReferencedSources(hasMultipleSources bool, source *v1alpha1.Applicat
|
|||
normalizedRepoURL := git.NormalizeGitURL(refSourceMapping.Repo.Repo)
|
||||
_, ok = repoRefs[normalizedRepoURL]
|
||||
if !ok {
|
||||
_, referencedCommitSHA, err := newClientResolveRevision(&refSourceMapping.Repo, refSourceMapping.TargetRevision)
|
||||
_, referencedCommitSHA, err := newClientResolveRevision(&refSourceMapping.Repo, refSourceMapping.TargetRevision, gitClientOpts)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get git client for repo %s: %v", refSourceMapping.Repo.Repo, err)
|
||||
return nil, fmt.Errorf("failed to get git client for repo %s", refSourceMapping.Repo.Repo)
|
||||
|
|
@ -728,7 +729,7 @@ func (s *Service) runManifestGenAsync(ctx context.Context, repoRoot, commitSHA,
|
|||
return
|
||||
}
|
||||
} else {
|
||||
gitClient, referencedCommitSHA, err := s.newClientResolveRevision(&refSourceMapping.Repo, refSourceMapping.TargetRevision)
|
||||
gitClient, referencedCommitSHA, err := s.newClientResolveRevision(&refSourceMapping.Repo, refSourceMapping.TargetRevision, git.WithCache(s.cache, !q.NoRevisionCache && !q.NoCache))
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get git client for repo %s: %v", refSourceMapping.Repo.Repo, err)
|
||||
ch.errCh <- fmt.Errorf("failed to get git client for repo %s", refSourceMapping.Repo.Repo)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
|
@ -17,6 +18,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
cacheutil "github.com/argoproj/argo-cd/v2/util/cache"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
|
||||
|
|
@ -28,13 +30,14 @@ import (
|
|||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
|
||||
argoappv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/v2/reposerver/apiclient"
|
||||
"github.com/argoproj/argo-cd/v2/reposerver/cache"
|
||||
repositorymocks "github.com/argoproj/argo-cd/v2/reposerver/cache/mocks"
|
||||
"github.com/argoproj/argo-cd/v2/reposerver/metrics"
|
||||
fileutil "github.com/argoproj/argo-cd/v2/test/fixture/path"
|
||||
"github.com/argoproj/argo-cd/v2/util/argo"
|
||||
cacheutil "github.com/argoproj/argo-cd/v2/util/cache"
|
||||
dbmocks "github.com/argoproj/argo-cd/v2/util/db/mocks"
|
||||
"github.com/argoproj/argo-cd/v2/util/git"
|
||||
gitmocks "github.com/argoproj/argo-cd/v2/util/git/mocks"
|
||||
|
|
@ -51,12 +54,49 @@ gpg: Good signature from "GitHub (web-flow commit signing) <noreply@github.com>"
|
|||
|
||||
type clientFunc func(*gitmocks.Client, *helmmocks.Client, *iomocks.TempPaths)
|
||||
|
||||
func newServiceWithMocks(root string, signed bool) (*Service, *gitmocks.Client) {
|
||||
type repoCacheMocks struct {
|
||||
mock.Mock
|
||||
cacheutilCache *cacheutil.Cache
|
||||
cache *cache.Cache
|
||||
mockCache *repositorymocks.MockRepoCache
|
||||
}
|
||||
|
||||
type newGitRepoHelmChartOptions struct {
|
||||
chartName string
|
||||
chartVersion string
|
||||
// valuesFiles is a map of the values file name to the key/value pairs to be written to the file
|
||||
valuesFiles map[string]map[string]string
|
||||
}
|
||||
|
||||
type newGitRepoOptions struct {
|
||||
path string
|
||||
createPath bool
|
||||
remote string
|
||||
addEmptyCommit bool
|
||||
helmChartOptions newGitRepoHelmChartOptions
|
||||
}
|
||||
|
||||
func newCacheMocks() *repoCacheMocks {
|
||||
mockRepoCache := repositorymocks.NewMockRepoCache(&repositorymocks.MockCacheOptions{
|
||||
RepoCacheExpiration: 1 * time.Minute,
|
||||
RevisionCacheExpiration: 1 * time.Minute,
|
||||
ReadDelay: 0,
|
||||
WriteDelay: 0,
|
||||
})
|
||||
cacheutilCache := cacheutil.NewCache(mockRepoCache.RedisClient)
|
||||
return &repoCacheMocks{
|
||||
cacheutilCache: cacheutilCache,
|
||||
cache: cache.NewCache(cacheutilCache, 1*time.Minute, 1*time.Minute),
|
||||
mockCache: mockRepoCache,
|
||||
}
|
||||
}
|
||||
|
||||
func newServiceWithMocks(t *testing.T, root string, signed bool) (*Service, *gitmocks.Client, *repoCacheMocks) {
|
||||
root, err := filepath.Abs(root)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return newServiceWithOpt(func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
|
||||
return newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
|
||||
gitClient.On("Init").Return(nil)
|
||||
gitClient.On("Fetch", mock.Anything).Return(nil)
|
||||
gitClient.On("Checkout", mock.Anything, mock.Anything).Return(nil)
|
||||
|
|
@ -73,7 +113,7 @@ func newServiceWithMocks(root string, signed bool) (*Service, *gitmocks.Client)
|
|||
chart := "my-chart"
|
||||
oobChart := "out-of-bounds-chart"
|
||||
version := "1.1.0"
|
||||
helmClient.On("GetIndex", true).Return(&helm.Index{Entries: map[string]helm.Entries{
|
||||
helmClient.On("GetIndex", mock.AnythingOfType("bool")).Return(&helm.Index{Entries: map[string]helm.Entries{
|
||||
chart: {{Version: "1.0.0"}, {Version: version}},
|
||||
oobChart: {{Version: "1.0.0"}, {Version: version}},
|
||||
}}, nil)
|
||||
|
|
@ -89,18 +129,16 @@ func newServiceWithMocks(root string, signed bool) (*Service, *gitmocks.Client)
|
|||
}, root)
|
||||
}
|
||||
|
||||
func newServiceWithOpt(cf clientFunc, root string) (*Service, *gitmocks.Client) {
|
||||
func newServiceWithOpt(t *testing.T, cf clientFunc, root string) (*Service, *gitmocks.Client, *repoCacheMocks) {
|
||||
helmClient := &helmmocks.Client{}
|
||||
gitClient := &gitmocks.Client{}
|
||||
paths := &iomocks.TempPaths{}
|
||||
cf(gitClient, helmClient, paths)
|
||||
service := NewService(metrics.NewMetricsServer(), cache.NewCache(
|
||||
cacheutil.NewCache(cacheutil.NewInMemoryCache(1*time.Minute)),
|
||||
1*time.Minute,
|
||||
1*time.Minute,
|
||||
), RepoServerInitConstants{ParallelismLimit: 1}, argo.NewResourceTracking(), &git.NoopCredsStore{}, root)
|
||||
cacheMocks := newCacheMocks()
|
||||
t.Cleanup(cacheMocks.mockCache.StopRedisCallback)
|
||||
service := NewService(metrics.NewMetricsServer(), cacheMocks.cache, RepoServerInitConstants{ParallelismLimit: 1}, argo.NewResourceTracking(), &git.NoopCredsStore{}, root)
|
||||
|
||||
service.newGitClient = func(rawRepoURL string, root string, creds git.Creds, insecure bool, enableLfs bool, prosy string, opts ...git.ClientOpts) (client git.Client, e error) {
|
||||
service.newGitClient = func(rawRepoURL string, root string, creds git.Creds, insecure bool, enableLfs bool, proxy string, opts ...git.ClientOpts) (client git.Client, e error) {
|
||||
return gitClient, nil
|
||||
}
|
||||
service.newHelmClient = func(repoURL string, creds helm.Creds, enableOci bool, proxy string, opts ...helm.ClientOpts) helm.Client {
|
||||
|
|
@ -110,20 +148,20 @@ func newServiceWithOpt(cf clientFunc, root string) (*Service, *gitmocks.Client)
|
|||
return io.NopCloser
|
||||
}
|
||||
service.gitRepoPaths = paths
|
||||
return service, gitClient
|
||||
return service, gitClient, cacheMocks
|
||||
}
|
||||
|
||||
func newService(root string) *Service {
|
||||
service, _ := newServiceWithMocks(root, false)
|
||||
func newService(t *testing.T, root string) *Service {
|
||||
service, _, _ := newServiceWithMocks(t, root, false)
|
||||
return service
|
||||
}
|
||||
|
||||
func newServiceWithSignature(root string) *Service {
|
||||
service, _ := newServiceWithMocks(root, true)
|
||||
func newServiceWithSignature(t *testing.T, root string) *Service {
|
||||
service, _, _ := newServiceWithMocks(t, root, true)
|
||||
return service
|
||||
}
|
||||
|
||||
func newServiceWithCommitSHA(root, revision string) *Service {
|
||||
func newServiceWithCommitSHA(t *testing.T, root, revision string) *Service {
|
||||
var revisionErr error
|
||||
|
||||
commitSHARegex := regexp.MustCompile("^[0-9A-Fa-f]{40}$")
|
||||
|
|
@ -131,7 +169,7 @@ func newServiceWithCommitSHA(root, revision string) *Service {
|
|||
revisionErr = errors.New("not a commit SHA")
|
||||
}
|
||||
|
||||
service, gitClient := newServiceWithOpt(func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
|
||||
service, gitClient, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
|
||||
gitClient.On("Init").Return(nil)
|
||||
gitClient.On("Fetch", mock.Anything).Return(nil)
|
||||
gitClient.On("Checkout", mock.Anything, mock.Anything).Return(nil)
|
||||
|
|
@ -150,7 +188,7 @@ func newServiceWithCommitSHA(root, revision string) *Service {
|
|||
}
|
||||
|
||||
func TestGenerateYamlManifestInDir(t *testing.T) {
|
||||
service := newService("../../manifests/base")
|
||||
service := newService(t, "../../manifests/base")
|
||||
|
||||
src := argoappv1.ApplicationSource{Path: "."}
|
||||
q := apiclient.ManifestRequest{
|
||||
|
|
@ -247,7 +285,7 @@ func TestGenerateManifests_MissingSymlinkDestination(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGenerateManifests_K8SAPIResetCache(t *testing.T) {
|
||||
service := newService("../../manifests/base")
|
||||
service := newService(t, "../../manifests/base")
|
||||
|
||||
src := argoappv1.ApplicationSource{Path: "."}
|
||||
q := apiclient.ManifestRequest{
|
||||
|
|
@ -275,7 +313,7 @@ func TestGenerateManifests_K8SAPIResetCache(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGenerateManifests_EmptyCache(t *testing.T) {
|
||||
service := newService("../../manifests/base")
|
||||
service, gitMocks, mockCache := newServiceWithMocks(t, "../../manifests/base", false)
|
||||
|
||||
src := argoappv1.ApplicationSource{Path: "."}
|
||||
q := apiclient.ManifestRequest{
|
||||
|
|
@ -291,11 +329,85 @@ func TestGenerateManifests_EmptyCache(t *testing.T) {
|
|||
res, err := service.GenerateManifest(context.Background(), &q)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, len(res.Manifests) > 0)
|
||||
mockCache.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{
|
||||
ExternalSets: 2,
|
||||
ExternalGets: 2,
|
||||
ExternalDeletes: 1})
|
||||
gitMocks.AssertCalled(t, "LsRemote", mock.Anything)
|
||||
gitMocks.AssertCalled(t, "Fetch", mock.Anything)
|
||||
}
|
||||
|
||||
// Test that calling manifest generation on source helm reference helm files that when the revision is cached it does not call ls-remote
|
||||
func TestGenerateManifestsHelmWithRefs_CachedNoLsRemote(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
repopath := fmt.Sprintf("%s/tmprepo", dir)
|
||||
cacheMocks := newCacheMocks()
|
||||
t.Cleanup(func() {
|
||||
cacheMocks.mockCache.StopRedisCallback()
|
||||
err := filepath.WalkDir(dir,
|
||||
func(path string, di fs.DirEntry, err error) error {
|
||||
if err == nil {
|
||||
return os.Chmod(path, 0777)
|
||||
}
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
service := NewService(metrics.NewMetricsServer(), cacheMocks.cache, RepoServerInitConstants{ParallelismLimit: 1}, argo.NewResourceTracking(), &git.NoopCredsStore{}, repopath)
|
||||
var gitClient git.Client
|
||||
var err error
|
||||
service.newGitClient = func(rawRepoURL string, root string, creds git.Creds, insecure bool, enableLfs bool, proxy string, opts ...git.ClientOpts) (client git.Client, e error) {
|
||||
opts = append(opts, git.WithEventHandlers(git.EventHandlers{
|
||||
// Primary check, we want to make sure ls-remote is not called when the item is in cache
|
||||
OnLsRemote: func(repo string) func() {
|
||||
return func() {
|
||||
assert.Fail(t, "LsRemote should not be called when the item is in cache")
|
||||
}
|
||||
},
|
||||
}))
|
||||
gitClient, err = git.NewClientExt(rawRepoURL, root, creds, insecure, enableLfs, proxy, opts...)
|
||||
return gitClient, err
|
||||
}
|
||||
repoRemote := fmt.Sprintf("file://%s", repopath)
|
||||
revision := initGitRepo(t, newGitRepoOptions{
|
||||
path: repopath,
|
||||
createPath: true,
|
||||
remote: repoRemote,
|
||||
helmChartOptions: newGitRepoHelmChartOptions{
|
||||
chartName: "my-chart",
|
||||
chartVersion: "v1.0.0",
|
||||
valuesFiles: map[string]map[string]string{"test.yaml": {"testval": "test"}}},
|
||||
})
|
||||
src := argoappv1.ApplicationSource{RepoURL: repoRemote, Path: ".", TargetRevision: "HEAD", Helm: &argoappv1.ApplicationSourceHelm{
|
||||
ValueFiles: []string{"$ref/test.yaml"},
|
||||
}}
|
||||
repo := &argoappv1.Repository{
|
||||
Repo: repoRemote,
|
||||
}
|
||||
q := apiclient.ManifestRequest{
|
||||
Repo: repo,
|
||||
Revision: "HEAD",
|
||||
HasMultipleSources: true,
|
||||
ApplicationSource: &src,
|
||||
ProjectName: "default",
|
||||
ProjectSourceRepos: []string{"*"},
|
||||
RefSources: map[string]*argoappv1.RefTarget{"$ref": {TargetRevision: "HEAD", Repo: *repo}},
|
||||
}
|
||||
err = cacheMocks.cacheutilCache.SetItem(fmt.Sprintf("git-refs|%s", repoRemote), [][2]string{{"HEAD", revision}}, 30*time.Second, false)
|
||||
assert.NoError(t, err)
|
||||
_, err = service.GenerateManifest(context.Background(), &q)
|
||||
assert.NoError(t, err)
|
||||
cacheMocks.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{
|
||||
ExternalSets: 2,
|
||||
ExternalGets: 5})
|
||||
}
|
||||
|
||||
// ensure we can use a semver constraint range (>= 1.0.0) and get back the correct chart (1.0.0)
|
||||
func TestHelmManifestFromChartRepo(t *testing.T) {
|
||||
service := newService(".")
|
||||
root := t.TempDir()
|
||||
service, gitMocks, mockCache := newServiceWithMocks(t, root, false)
|
||||
source := &argoappv1.ApplicationSource{Chart: "my-chart", TargetRevision: ">= 1.0.0"}
|
||||
request := &apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: source, NoCache: true, ProjectName: "something",
|
||||
ProjectSourceRepos: []string{"*"}}
|
||||
|
|
@ -309,10 +421,14 @@ func TestHelmManifestFromChartRepo(t *testing.T) {
|
|||
Revision: "1.1.0",
|
||||
SourceType: "Helm",
|
||||
}, response)
|
||||
mockCache.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{
|
||||
ExternalSets: 1,
|
||||
ExternalGets: 0})
|
||||
gitMocks.AssertNotCalled(t, "LsRemote", mock.Anything)
|
||||
}
|
||||
|
||||
func TestHelmChartReferencingExternalValues(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
spec := argoappv1.ApplicationSpec{
|
||||
Sources: []argoappv1.ApplicationSource{
|
||||
{RepoURL: "https://helm.example.com", Chart: "my-chart", TargetRevision: ">= 1.0.0", Helm: &argoappv1.ApplicationSourceHelm{
|
||||
|
|
@ -342,7 +458,7 @@ func TestHelmChartReferencingExternalValues(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestHelmChartReferencingExternalValues_OutOfBounds_Symlink(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
err := os.Mkdir("testdata/oob-symlink", 0755)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
|
|
@ -376,7 +492,7 @@ func TestHelmChartReferencingExternalValues_OutOfBounds_Symlink(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGenerateManifestsUseExactRevision(t *testing.T) {
|
||||
service, gitClient := newServiceWithMocks(".", false)
|
||||
service, gitClient, _ := newServiceWithMocks(t, ".", false)
|
||||
|
||||
src := argoappv1.ApplicationSource{Path: "./testdata/recurse", Directory: &argoappv1.ApplicationSourceDirectory{Recurse: true}}
|
||||
|
||||
|
|
@ -390,7 +506,7 @@ func TestGenerateManifestsUseExactRevision(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRecurseManifestsInDir(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
|
||||
src := argoappv1.ApplicationSource{Path: "./testdata/recurse", Directory: &argoappv1.ApplicationSourceDirectory{Recurse: true}}
|
||||
|
||||
|
|
@ -403,7 +519,7 @@ func TestRecurseManifestsInDir(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestInvalidManifestsInDir(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
|
||||
src := argoappv1.ApplicationSource{Path: "./testdata/invalid-manifests", Directory: &argoappv1.ApplicationSourceDirectory{Recurse: true}}
|
||||
|
||||
|
|
@ -414,7 +530,7 @@ func TestInvalidManifestsInDir(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestInvalidMetadata(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
|
||||
src := argoappv1.ApplicationSource{Path: "./testdata/invalid-metadata", Directory: &argoappv1.ApplicationSourceDirectory{Recurse: true}}
|
||||
q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, AppLabelKey: "test", AppName: "invalid-metadata", TrackingMethod: "annotation+label"}
|
||||
|
|
@ -424,7 +540,7 @@ func TestInvalidMetadata(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestNilMetadataAccessors(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
expected := "{\"apiVersion\":\"v1\",\"kind\":\"ConfigMap\",\"metadata\":{\"annotations\":{\"argocd.argoproj.io/tracking-id\":\"nil-metadata-accessors:/ConfigMap:/my-map\"},\"labels\":{\"test\":\"nil-metadata-accessors\"},\"name\":\"my-map\"},\"stringData\":{\"foo\":\"bar\"}}"
|
||||
|
||||
src := argoappv1.ApplicationSource{Path: "./testdata/nil-metadata-accessors", Directory: &argoappv1.ApplicationSourceDirectory{Recurse: true}}
|
||||
|
|
@ -436,7 +552,7 @@ func TestNilMetadataAccessors(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGenerateJsonnetManifestInDir(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
|
||||
q := apiclient.ManifestRequest{
|
||||
Repo: &argoappv1.Repository{},
|
||||
|
|
@ -459,7 +575,7 @@ func TestGenerateJsonnetManifestInDir(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGenerateJsonnetManifestInRootDir(t *testing.T) {
|
||||
service := newService("testdata/jsonnet-1")
|
||||
service := newService(t, "testdata/jsonnet-1")
|
||||
|
||||
q := apiclient.ManifestRequest{
|
||||
Repo: &argoappv1.Repository{},
|
||||
|
|
@ -482,7 +598,7 @@ func TestGenerateJsonnetManifestInRootDir(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGenerateJsonnetLibOutside(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
|
||||
q := apiclient.ManifestRequest{
|
||||
Repo: &argoappv1.Repository{},
|
||||
|
|
@ -553,7 +669,7 @@ func TestManifestGenErrorCacheByNumRequests(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
testName := fmt.Sprintf("gen-attempts-%d-pause-%d-total-%d", tt.PauseGenerationAfterFailedGenerationAttempts, tt.PauseGenerationOnFailureForRequests, tt.TotalCacheInvocations)
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
|
||||
service.initConstants = RepoServerInitConstants{
|
||||
ParallelismLimit: 1,
|
||||
|
|
@ -631,7 +747,7 @@ func TestManifestGenErrorCacheFileContentsChange(t *testing.T) {
|
|||
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
service := newService(tmpDir)
|
||||
service := newService(t, tmpDir)
|
||||
|
||||
service.initConstants = RepoServerInitConstants{
|
||||
ParallelismLimit: 1,
|
||||
|
|
@ -701,7 +817,7 @@ func TestManifestGenErrorCacheByMinutesElapsed(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
testName := fmt.Sprintf("pause-time-%d", tt.PauseGenerationOnFailureForMinutes)
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
|
||||
// Here we simulate the passage of time by overriding the now() function of Service
|
||||
currentTime := time.Now()
|
||||
|
|
@ -771,7 +887,7 @@ func TestManifestGenErrorCacheByMinutesElapsed(t *testing.T) {
|
|||
|
||||
func TestManifestGenErrorCacheRespectsNoCache(t *testing.T) {
|
||||
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
|
||||
service.initConstants = RepoServerInitConstants{
|
||||
ParallelismLimit: 1,
|
||||
|
|
@ -828,7 +944,7 @@ func TestManifestGenErrorCacheRespectsNoCache(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGenerateHelmWithValues(t *testing.T) {
|
||||
service := newService("../../util/helm/testdata/redis")
|
||||
service := newService(t, "../../util/helm/testdata/redis")
|
||||
|
||||
res, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
|
||||
Repo: &argoappv1.Repository{},
|
||||
|
|
@ -865,7 +981,7 @@ func TestGenerateHelmWithValues(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestHelmWithMissingValueFiles(t *testing.T) {
|
||||
service := newService("../../util/helm/testdata/redis")
|
||||
service := newService(t, "../../util/helm/testdata/redis")
|
||||
missingValuesFile := "values-prod-overrides.yaml"
|
||||
|
||||
req := &apiclient.ManifestRequest{
|
||||
|
|
@ -893,7 +1009,7 @@ func TestHelmWithMissingValueFiles(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGenerateHelmWithEnvVars(t *testing.T) {
|
||||
service := newService("../../util/helm/testdata/redis")
|
||||
service := newService(t, "../../util/helm/testdata/redis")
|
||||
|
||||
res, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
|
||||
Repo: &argoappv1.Repository{},
|
||||
|
|
@ -930,7 +1046,7 @@ func TestGenerateHelmWithEnvVars(t *testing.T) {
|
|||
// The requested value file (`../minio/values.yaml`) is outside the app path (`./util/helm/testdata/redis`), however
|
||||
// since the requested value is still under the repo directory (`~/go/src/github.com/argoproj/argo-cd`), it is allowed
|
||||
func TestGenerateHelmWithValuesDirectoryTraversal(t *testing.T) {
|
||||
service := newService("../../util/helm/testdata")
|
||||
service := newService(t, "../../util/helm/testdata")
|
||||
_, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
|
||||
Repo: &argoappv1.Repository{},
|
||||
AppName: "test",
|
||||
|
|
@ -947,7 +1063,7 @@ func TestGenerateHelmWithValuesDirectoryTraversal(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
|
||||
// Test the case where the path is "."
|
||||
service = newService("./testdata")
|
||||
service = newService(t, "./testdata")
|
||||
_, err = service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
|
||||
Repo: &argoappv1.Repository{},
|
||||
AppName: "test",
|
||||
|
|
@ -961,7 +1077,7 @@ func TestGenerateHelmWithValuesDirectoryTraversal(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestChartRepoWithOutOfBoundsSymlink(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
source := &argoappv1.ApplicationSource{Chart: "out-of-bounds-chart", TargetRevision: ">= 1.0.0"}
|
||||
request := &apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: source, NoCache: true}
|
||||
_, err := service.GenerateManifest(context.Background(), request)
|
||||
|
|
@ -971,7 +1087,7 @@ func TestChartRepoWithOutOfBoundsSymlink(t *testing.T) {
|
|||
// This is a Helm first-class app with a values file inside the repo directory
|
||||
// (`~/go/src/github.com/argoproj/argo-cd/reposerver/repository`), so it is allowed
|
||||
func TestHelmManifestFromChartRepoWithValueFile(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
source := &argoappv1.ApplicationSource{
|
||||
Chart: "my-chart",
|
||||
TargetRevision: ">= 1.0.0",
|
||||
|
|
@ -1000,7 +1116,7 @@ func TestHelmManifestFromChartRepoWithValueFile(t *testing.T) {
|
|||
// This is a Helm first-class app with a values file outside the repo directory
|
||||
// (`~/go/src/github.com/argoproj/argo-cd/reposerver/repository`), so it is not allowed
|
||||
func TestHelmManifestFromChartRepoWithValueFileOutsideRepo(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
source := &argoappv1.ApplicationSource{
|
||||
Chart: "my-chart",
|
||||
TargetRevision: ">= 1.0.0",
|
||||
|
|
@ -1015,7 +1131,7 @@ func TestHelmManifestFromChartRepoWithValueFileOutsideRepo(t *testing.T) {
|
|||
|
||||
func TestHelmManifestFromChartRepoWithValueFileLinks(t *testing.T) {
|
||||
t.Run("Valid symlink", func(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
source := &argoappv1.ApplicationSource{
|
||||
Chart: "my-chart",
|
||||
TargetRevision: ">= 1.0.0",
|
||||
|
|
@ -1031,7 +1147,7 @@ func TestHelmManifestFromChartRepoWithValueFileLinks(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGenerateHelmWithURL(t *testing.T) {
|
||||
service := newService("../../util/helm/testdata/redis")
|
||||
service := newService(t, "../../util/helm/testdata/redis")
|
||||
|
||||
_, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
|
||||
Repo: &argoappv1.Repository{},
|
||||
|
|
@ -1054,7 +1170,7 @@ func TestGenerateHelmWithURL(t *testing.T) {
|
|||
// (`~/go/src/github.com/argoproj/argo-cd/util/helm/testdata/redis`), so it is blocked
|
||||
func TestGenerateHelmWithValuesDirectoryTraversalOutsideRepo(t *testing.T) {
|
||||
t.Run("Values file with relative path pointing outside repo root", func(t *testing.T) {
|
||||
service := newService("../../util/helm/testdata/redis")
|
||||
service := newService(t, "../../util/helm/testdata/redis")
|
||||
_, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
|
||||
Repo: &argoappv1.Repository{},
|
||||
AppName: "test",
|
||||
|
|
@ -1073,7 +1189,7 @@ func TestGenerateHelmWithValuesDirectoryTraversalOutsideRepo(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("Values file with relative path pointing inside repo root", func(t *testing.T) {
|
||||
service := newService("./testdata")
|
||||
service := newService(t, "./testdata")
|
||||
_, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
|
||||
Repo: &argoappv1.Repository{},
|
||||
AppName: "test",
|
||||
|
|
@ -1091,7 +1207,7 @@ func TestGenerateHelmWithValuesDirectoryTraversalOutsideRepo(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("Values file with absolute path stays within repo root", func(t *testing.T) {
|
||||
service := newService("./testdata")
|
||||
service := newService(t, "./testdata")
|
||||
_, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
|
||||
Repo: &argoappv1.Repository{},
|
||||
AppName: "test",
|
||||
|
|
@ -1109,7 +1225,7 @@ func TestGenerateHelmWithValuesDirectoryTraversalOutsideRepo(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("Values file with absolute path using back-references outside repo root", func(t *testing.T) {
|
||||
service := newService("./testdata")
|
||||
service := newService(t, "./testdata")
|
||||
_, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
|
||||
Repo: &argoappv1.Repository{},
|
||||
AppName: "test",
|
||||
|
|
@ -1128,7 +1244,7 @@ func TestGenerateHelmWithValuesDirectoryTraversalOutsideRepo(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("Remote values file from forbidden protocol", func(t *testing.T) {
|
||||
service := newService("./testdata")
|
||||
service := newService(t, "./testdata")
|
||||
_, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
|
||||
Repo: &argoappv1.Repository{},
|
||||
AppName: "test",
|
||||
|
|
@ -1147,7 +1263,7 @@ func TestGenerateHelmWithValuesDirectoryTraversalOutsideRepo(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("Remote values file from custom allowed protocol", func(t *testing.T) {
|
||||
service := newService("./testdata")
|
||||
service := newService(t, "./testdata")
|
||||
_, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
|
||||
Repo: &argoappv1.Repository{},
|
||||
AppName: "test",
|
||||
|
|
@ -1168,7 +1284,7 @@ func TestGenerateHelmWithValuesDirectoryTraversalOutsideRepo(t *testing.T) {
|
|||
|
||||
// File parameter should not allow traversal outside of the repository root
|
||||
func TestGenerateHelmWithAbsoluteFileParameter(t *testing.T) {
|
||||
service := newService("../..")
|
||||
service := newService(t, "../..")
|
||||
|
||||
file, err := os.CreateTemp("", "external-secret.txt")
|
||||
assert.NoError(t, err)
|
||||
|
|
@ -1209,7 +1325,7 @@ func TestGenerateHelmWithAbsoluteFileParameter(t *testing.T) {
|
|||
// directory (`~/go/src/github.com/argoproj/argo-cd`), it is allowed. It is used as a means of
|
||||
// providing direct content to a helm chart via a specific key.
|
||||
func TestGenerateHelmWithFileParameter(t *testing.T) {
|
||||
service := newService("../../util/helm/testdata")
|
||||
service := newService(t, "../../util/helm/testdata")
|
||||
|
||||
res, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
|
||||
Repo: &argoappv1.Repository{},
|
||||
|
|
@ -1234,7 +1350,7 @@ func TestGenerateHelmWithFileParameter(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGenerateNullList(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
|
||||
t.Run("null list", func(t *testing.T) {
|
||||
res1, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
|
||||
|
|
@ -1302,7 +1418,7 @@ func TestGenerateFromUTF16(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestListApps(t *testing.T) {
|
||||
service := newService("./testdata")
|
||||
service := newService(t, "./testdata")
|
||||
|
||||
res, err := service.ListApps(context.Background(), &apiclient.ListAppsRequest{Repo: &argoappv1.Repository{}})
|
||||
assert.NoError(t, err)
|
||||
|
|
@ -1329,7 +1445,7 @@ func TestListApps(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGetAppDetailsHelm(t *testing.T) {
|
||||
service := newService("../../util/helm/testdata/dependency")
|
||||
service := newService(t, "../../util/helm/testdata/dependency")
|
||||
|
||||
res, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{
|
||||
Repo: &argoappv1.Repository{},
|
||||
|
|
@ -1344,8 +1460,26 @@ func TestGetAppDetailsHelm(t *testing.T) {
|
|||
assert.Equal(t, "Helm", res.Type)
|
||||
assert.EqualValues(t, []string{"values-production.yaml", "values.yaml"}, res.Helm.ValueFiles)
|
||||
}
|
||||
|
||||
func TestGetAppDetailsHelmUsesCache(t *testing.T) {
|
||||
service := newService(t, "../../util/helm/testdata/dependency")
|
||||
|
||||
res, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{
|
||||
Repo: &argoappv1.Repository{},
|
||||
Source: &argoappv1.ApplicationSource{
|
||||
Path: ".",
|
||||
},
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, res.Helm)
|
||||
|
||||
assert.Equal(t, "Helm", res.Type)
|
||||
assert.EqualValues(t, []string{"values-production.yaml", "values.yaml"}, res.Helm.ValueFiles)
|
||||
}
|
||||
|
||||
func TestGetAppDetailsHelm_WithNoValuesFile(t *testing.T) {
|
||||
service := newService("../../util/helm/testdata/api-versions")
|
||||
service := newService(t, "../../util/helm/testdata/api-versions")
|
||||
|
||||
res, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{
|
||||
Repo: &argoappv1.Repository{},
|
||||
|
|
@ -1363,7 +1497,7 @@ func TestGetAppDetailsHelm_WithNoValuesFile(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGetAppDetailsKustomize(t *testing.T) {
|
||||
service := newService("../../util/kustomize/testdata/kustomization_yaml")
|
||||
service := newService(t, "../../util/kustomize/testdata/kustomization_yaml")
|
||||
|
||||
res, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{
|
||||
Repo: &argoappv1.Repository{},
|
||||
|
|
@ -1380,7 +1514,7 @@ func TestGetAppDetailsKustomize(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGetHelmCharts(t *testing.T) {
|
||||
service := newService("../..")
|
||||
service := newService(t, "../..")
|
||||
res, err := service.GetHelmCharts(context.Background(), &apiclient.HelmChartsRequest{Repo: &argoappv1.Repository{}})
|
||||
|
||||
// fix flakiness
|
||||
|
|
@ -1401,7 +1535,7 @@ func TestGetHelmCharts(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGetRevisionMetadata(t *testing.T) {
|
||||
service, gitClient := newServiceWithMocks("../..", false)
|
||||
service, gitClient, _ := newServiceWithMocks(t, "../..", false)
|
||||
now := time.Now()
|
||||
|
||||
gitClient.On("RevisionMetadata", mock.Anything).Return(&git.RevisionMetadata{
|
||||
|
|
@ -1469,7 +1603,7 @@ func TestGetRevisionMetadata(t *testing.T) {
|
|||
func TestGetSignatureVerificationResult(t *testing.T) {
|
||||
// Commit with signature and verification requested
|
||||
{
|
||||
service := newServiceWithSignature("../../manifests/base")
|
||||
service := newServiceWithSignature(t, "../../manifests/base")
|
||||
|
||||
src := argoappv1.ApplicationSource{Path: "."}
|
||||
q := apiclient.ManifestRequest{
|
||||
|
|
@ -1486,7 +1620,7 @@ func TestGetSignatureVerificationResult(t *testing.T) {
|
|||
}
|
||||
// Commit with signature and verification not requested
|
||||
{
|
||||
service := newServiceWithSignature("../../manifests/base")
|
||||
service := newServiceWithSignature(t, "../../manifests/base")
|
||||
|
||||
src := argoappv1.ApplicationSource{Path: "."}
|
||||
q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, ProjectName: "something",
|
||||
|
|
@ -1498,7 +1632,7 @@ func TestGetSignatureVerificationResult(t *testing.T) {
|
|||
}
|
||||
// Commit without signature and verification requested
|
||||
{
|
||||
service := newService("../../manifests/base")
|
||||
service := newService(t, "../../manifests/base")
|
||||
|
||||
src := argoappv1.ApplicationSource{Path: "."}
|
||||
q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, VerifySignature: true, ProjectName: "something",
|
||||
|
|
@ -1510,7 +1644,7 @@ func TestGetSignatureVerificationResult(t *testing.T) {
|
|||
}
|
||||
// Commit without signature and verification not requested
|
||||
{
|
||||
service := newService("../../manifests/base")
|
||||
service := newService(t, "../../manifests/base")
|
||||
|
||||
src := argoappv1.ApplicationSource{Path: "."}
|
||||
q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, VerifySignature: true, ProjectName: "something",
|
||||
|
|
@ -1543,7 +1677,7 @@ func Test_newEnv(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestService_newHelmClientResolveRevision(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
|
||||
t.Run("EmptyRevision", func(t *testing.T) {
|
||||
_, _, err := service.newHelmClientResolveRevision(&argoappv1.Repository{}, "", "", true)
|
||||
|
|
@ -1557,7 +1691,7 @@ func TestService_newHelmClientResolveRevision(t *testing.T) {
|
|||
|
||||
func TestGetAppDetailsWithAppParameterFile(t *testing.T) {
|
||||
t.Run("No app name set and app specific file exists", func(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
runWithTempTestdata(t, "multi", func(t *testing.T, path string) {
|
||||
details, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{
|
||||
Repo: &argoappv1.Repository{},
|
||||
|
|
@ -1570,7 +1704,7 @@ func TestGetAppDetailsWithAppParameterFile(t *testing.T) {
|
|||
})
|
||||
})
|
||||
t.Run("No app specific override", func(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
runWithTempTestdata(t, "single-global", func(t *testing.T, path string) {
|
||||
details, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{
|
||||
Repo: &argoappv1.Repository{},
|
||||
|
|
@ -1584,7 +1718,7 @@ func TestGetAppDetailsWithAppParameterFile(t *testing.T) {
|
|||
})
|
||||
})
|
||||
t.Run("Only app specific override", func(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
runWithTempTestdata(t, "single-app-only", func(t *testing.T, path string) {
|
||||
details, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{
|
||||
Repo: &argoappv1.Repository{},
|
||||
|
|
@ -1598,7 +1732,7 @@ func TestGetAppDetailsWithAppParameterFile(t *testing.T) {
|
|||
})
|
||||
})
|
||||
t.Run("App specific override", func(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
runWithTempTestdata(t, "multi", func(t *testing.T, path string) {
|
||||
details, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{
|
||||
Repo: &argoappv1.Repository{},
|
||||
|
|
@ -1612,7 +1746,7 @@ func TestGetAppDetailsWithAppParameterFile(t *testing.T) {
|
|||
})
|
||||
})
|
||||
t.Run("App specific overrides containing non-mergeable field", func(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
runWithTempTestdata(t, "multi", func(t *testing.T, path string) {
|
||||
details, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{
|
||||
Repo: &argoappv1.Repository{},
|
||||
|
|
@ -1626,7 +1760,7 @@ func TestGetAppDetailsWithAppParameterFile(t *testing.T) {
|
|||
})
|
||||
})
|
||||
t.Run("Broken app-specific overrides", func(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
runWithTempTestdata(t, "multi", func(t *testing.T, path string) {
|
||||
_, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{
|
||||
Repo: &argoappv1.Repository{},
|
||||
|
|
@ -1668,7 +1802,7 @@ func runWithTempTestdata(t *testing.T, path string, runner func(t *testing.T, pa
|
|||
func TestGenerateManifestsWithAppParameterFile(t *testing.T) {
|
||||
t.Run("Single global override", func(t *testing.T) {
|
||||
runWithTempTestdata(t, "single-global", func(t *testing.T, path string) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
manifests, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
|
||||
Repo: &argoappv1.Repository{},
|
||||
ApplicationSource: &argoappv1.ApplicationSource{
|
||||
|
|
@ -1699,7 +1833,7 @@ func TestGenerateManifestsWithAppParameterFile(t *testing.T) {
|
|||
|
||||
t.Run("Single global override Helm", func(t *testing.T) {
|
||||
runWithTempTestdata(t, "single-global-helm", func(t *testing.T, path string) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
manifests, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
|
||||
Repo: &argoappv1.Repository{},
|
||||
ApplicationSource: &argoappv1.ApplicationSource{
|
||||
|
|
@ -1729,7 +1863,7 @@ func TestGenerateManifestsWithAppParameterFile(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("Application specific override", func(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
runWithTempTestdata(t, "single-app-only", func(t *testing.T, path string) {
|
||||
manifests, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
|
||||
Repo: &argoappv1.Repository{},
|
||||
|
|
@ -1760,8 +1894,29 @@ func TestGenerateManifestsWithAppParameterFile(t *testing.T) {
|
|||
})
|
||||
})
|
||||
|
||||
t.Run("Multi-source with source as ref only does not generate manifests", func(t *testing.T) {
|
||||
service := newService(t, ".")
|
||||
runWithTempTestdata(t, "single-app-only", func(t *testing.T, path string) {
|
||||
manifests, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
|
||||
Repo: &argoappv1.Repository{},
|
||||
ApplicationSource: &argoappv1.ApplicationSource{
|
||||
Path: "",
|
||||
Chart: "",
|
||||
Ref: "test",
|
||||
},
|
||||
AppName: "testapp-multi-ref-only",
|
||||
ProjectName: "something",
|
||||
ProjectSourceRepos: []string{"*"},
|
||||
HasMultipleSources: true,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, manifests.Manifests)
|
||||
assert.NotEmpty(t, manifests.Revision)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Application specific override for other app", func(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
runWithTempTestdata(t, "single-app-only", func(t *testing.T, path string) {
|
||||
manifests, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
|
||||
Repo: &argoappv1.Repository{},
|
||||
|
|
@ -1793,7 +1948,7 @@ func TestGenerateManifestsWithAppParameterFile(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("Override info does not appear in cache key", func(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
runWithTempTestdata(t, "single-global", func(t *testing.T, path string) {
|
||||
source := &argoappv1.ApplicationSource{
|
||||
Path: path,
|
||||
|
|
@ -1843,7 +1998,7 @@ func TestGenerateManifestWithAnnotatedAndRegularGitTagHashes(t *testing.T) {
|
|||
ProjectSourceRepos: []string{"*"},
|
||||
},
|
||||
wantError: false,
|
||||
service: newServiceWithCommitSHA(".", regularGitTagHash),
|
||||
service: newServiceWithCommitSHA(t, ".", regularGitTagHash),
|
||||
},
|
||||
|
||||
{
|
||||
|
|
@ -1859,7 +2014,7 @@ func TestGenerateManifestWithAnnotatedAndRegularGitTagHashes(t *testing.T) {
|
|||
ProjectSourceRepos: []string{"*"},
|
||||
},
|
||||
wantError: false,
|
||||
service: newServiceWithCommitSHA(".", annotatedGitTaghash),
|
||||
service: newServiceWithCommitSHA(t, ".", annotatedGitTaghash),
|
||||
},
|
||||
|
||||
{
|
||||
|
|
@ -1875,7 +2030,7 @@ func TestGenerateManifestWithAnnotatedAndRegularGitTagHashes(t *testing.T) {
|
|||
ProjectSourceRepos: []string{"*"},
|
||||
},
|
||||
wantError: true,
|
||||
service: newServiceWithCommitSHA(".", invalidGitTaghash),
|
||||
service: newServiceWithCommitSHA(t, ".", invalidGitTaghash),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
|
|
@ -1900,7 +2055,7 @@ func TestGenerateManifestWithAnnotatedAndRegularGitTagHashes(t *testing.T) {
|
|||
func TestGenerateManifestWithAnnotatedTagsAndMultiSourceApp(t *testing.T) {
|
||||
annotatedGitTaghash := "95249be61b028d566c29d47b19e65c5603388a41"
|
||||
|
||||
service := newServiceWithCommitSHA(".", annotatedGitTaghash)
|
||||
service := newServiceWithCommitSHA(t, ".", annotatedGitTaghash)
|
||||
|
||||
refSources := map[string]*argoappv1.RefTarget{}
|
||||
|
||||
|
|
@ -2485,7 +2640,7 @@ func Test_findManifests(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestTestRepoOCI(t *testing.T) {
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
_, err := service.TestRepository(context.Background(), &apiclient.TestRepositoryRequest{
|
||||
Repo: &argoappv1.Repository{
|
||||
Repo: "https://demo.goharbor.io",
|
||||
|
|
@ -2510,7 +2665,7 @@ func Test_getHelmDependencyRepos(t *testing.T) {
|
|||
|
||||
func TestResolveRevision(t *testing.T) {
|
||||
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
repo := &argoappv1.Repository{Repo: "https://github.com/argoproj/argo-cd"}
|
||||
app := &argoappv1.Application{Spec: argoappv1.ApplicationSpec{Source: &argoappv1.ApplicationSource{}}}
|
||||
resolveRevisionResponse, err := service.ResolveRevision(context.Background(), &apiclient.ResolveRevisionRequest{
|
||||
|
|
@ -2532,7 +2687,7 @@ func TestResolveRevision(t *testing.T) {
|
|||
|
||||
func TestResolveRevisionNegativeScenarios(t *testing.T) {
|
||||
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
repo := &argoappv1.Repository{Repo: "https://github.com/argoproj/argo-cd"}
|
||||
app := &argoappv1.Application{Spec: argoappv1.ApplicationSpec{Source: &argoappv1.ApplicationSource{}}}
|
||||
resolveRevisionResponse, err := service.ResolveRevision(context.Background(), &apiclient.ResolveRevisionRequest{
|
||||
|
|
@ -2579,19 +2734,57 @@ func TestDirectoryPermissionInitializer(t *testing.T) {
|
|||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func initGitRepo(repoPath string, remote string) error {
|
||||
if err := os.Mkdir(repoPath, 0755); err != nil {
|
||||
return err
|
||||
func addHelmToGitRepo(t *testing.T, options newGitRepoOptions) {
|
||||
err := os.WriteFile(filepath.Join(options.path, "Chart.yaml"), []byte("name: test\nversion: v1.0.0"), 0777)
|
||||
assert.NoError(t, err)
|
||||
for valuesFileName, values := range options.helmChartOptions.valuesFiles {
|
||||
valuesFileContents, err := yaml.Marshal(values)
|
||||
assert.NoError(t, err)
|
||||
err = os.WriteFile(filepath.Join(options.path, valuesFileName), valuesFileContents, 0777)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
cmd := exec.Command("git", "add", "-A")
|
||||
cmd.Dir = options.path
|
||||
assert.NoError(t, cmd.Run())
|
||||
cmd = exec.Command("git", "commit", "-m", "Initial commit")
|
||||
cmd.Dir = options.path
|
||||
assert.NoError(t, cmd.Run())
|
||||
}
|
||||
|
||||
func initGitRepo(t *testing.T, options newGitRepoOptions) (revision string) {
|
||||
if options.createPath {
|
||||
assert.NoError(t, os.Mkdir(options.path, 0755))
|
||||
}
|
||||
|
||||
cmd := exec.Command("git", "init", repoPath)
|
||||
cmd.Dir = repoPath
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
cmd := exec.Command("git", "init", options.path)
|
||||
cmd.Dir = options.path
|
||||
assert.NoError(t, cmd.Run())
|
||||
|
||||
if options.remote != "" {
|
||||
cmd = exec.Command("git", "remote", "add", "origin", options.path)
|
||||
cmd.Dir = options.path
|
||||
assert.NoError(t, cmd.Run())
|
||||
}
|
||||
cmd = exec.Command("git", "remote", "add", "origin", remote)
|
||||
cmd.Dir = repoPath
|
||||
return cmd.Run()
|
||||
|
||||
commitAdded := options.addEmptyCommit || options.helmChartOptions.chartName != ""
|
||||
if options.addEmptyCommit {
|
||||
cmd = exec.Command("git", "commit", "-m", "Initial commit", "--allow-empty")
|
||||
cmd.Dir = options.path
|
||||
assert.NoError(t, cmd.Run())
|
||||
} else if options.helmChartOptions.chartName != "" {
|
||||
addHelmToGitRepo(t, options)
|
||||
}
|
||||
|
||||
if commitAdded {
|
||||
var revB bytes.Buffer
|
||||
cmd = exec.Command("git", "rev-parse", "HEAD", options.path)
|
||||
cmd.Dir = options.path
|
||||
cmd.Stdout = &revB
|
||||
assert.NoError(t, cmd.Run())
|
||||
revision = strings.Split(revB.String(), "\n")[0]
|
||||
}
|
||||
return revision
|
||||
}
|
||||
|
||||
func TestInit(t *testing.T) {
|
||||
|
|
@ -2604,16 +2797,16 @@ func TestInit(t *testing.T) {
|
|||
})
|
||||
|
||||
repoPath := path.Join(dir, "repo1")
|
||||
require.NoError(t, initGitRepo(repoPath, "https://github.com/argo-cd/test-repo1"))
|
||||
initGitRepo(t, newGitRepoOptions{path: repoPath, remote: "https://github.com/argo-cd/test-repo1", createPath: true, addEmptyCommit: false})
|
||||
|
||||
service := newService(".")
|
||||
service := newService(t, ".")
|
||||
service.rootDir = dir
|
||||
|
||||
require.NoError(t, service.Init())
|
||||
|
||||
_, err := os.ReadDir(dir)
|
||||
require.Error(t, err)
|
||||
require.NoError(t, initGitRepo(path.Join(dir, "repo2"), "https://github.com/argo-cd/test-repo2"))
|
||||
initGitRepo(t, newGitRepoOptions{path: path.Join(dir, "repo2"), remote: "https://github.com/argo-cd/test-repo2", createPath: true, addEmptyCommit: false})
|
||||
}
|
||||
|
||||
// TestCheckoutRevisionCanGetNonstandardRefs shows that we can fetch a revision that points to a non-standard ref. In
|
||||
|
|
@ -2926,7 +3119,7 @@ func TestErrorGetGitDirectories(t *testing.T) {
|
|||
want *apiclient.GitDirectoriesResponse
|
||||
wantErr assert.ErrorAssertionFunc
|
||||
}{
|
||||
{name: "InvalidRepo", fields: fields{service: newService(".")}, args: args{
|
||||
{name: "InvalidRepo", fields: fields{service: newService(t, ".")}, args: args{
|
||||
ctx: context.TODO(),
|
||||
request: &apiclient.GitDirectoriesRequest{
|
||||
Repo: nil,
|
||||
|
|
@ -2935,7 +3128,7 @@ func TestErrorGetGitDirectories(t *testing.T) {
|
|||
},
|
||||
}, want: nil, wantErr: assert.Error},
|
||||
{name: "InvalidResolveRevision", fields: fields{service: func() *Service {
|
||||
s, _ := newServiceWithOpt(func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
|
||||
s, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
|
||||
gitClient.On("Checkout", mock.Anything, mock.Anything).Return(nil)
|
||||
gitClient.On("LsRemote", mock.Anything).Return("", fmt.Errorf("ah error"))
|
||||
paths.On("GetPath", mock.Anything).Return(".", nil)
|
||||
|
|
@ -2966,7 +3159,7 @@ func TestErrorGetGitDirectories(t *testing.T) {
|
|||
func TestGetGitDirectories(t *testing.T) {
|
||||
// test not using the cache
|
||||
root := "./testdata/git-files-dirs"
|
||||
s, _ := newServiceWithOpt(func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
|
||||
s, _, cacheMocks := newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
|
||||
gitClient.On("Init").Return(nil)
|
||||
gitClient.On("Fetch", mock.Anything).Return(nil)
|
||||
gitClient.On("Checkout", mock.Anything, mock.Anything).Once().Return(nil)
|
||||
|
|
@ -2989,6 +3182,10 @@ func TestGetGitDirectories(t *testing.T) {
|
|||
directories, err = s.GetGitDirectories(context.TODO(), dirRequest)
|
||||
assert.Nil(t, err)
|
||||
assert.ElementsMatch(t, []string{"app", "app/bar", "app/foo/bar", "somedir", "app/foo"}, directories.GetPaths())
|
||||
cacheMocks.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{
|
||||
ExternalSets: 1,
|
||||
ExternalGets: 2,
|
||||
})
|
||||
}
|
||||
|
||||
func TestErrorGetGitFiles(t *testing.T) {
|
||||
|
|
@ -3006,7 +3203,7 @@ func TestErrorGetGitFiles(t *testing.T) {
|
|||
want *apiclient.GitFilesResponse
|
||||
wantErr assert.ErrorAssertionFunc
|
||||
}{
|
||||
{name: "InvalidRepo", fields: fields{service: newService(".")}, args: args{
|
||||
{name: "InvalidRepo", fields: fields{service: newService(t, ".")}, args: args{
|
||||
ctx: context.TODO(),
|
||||
request: &apiclient.GitFilesRequest{
|
||||
Repo: nil,
|
||||
|
|
@ -3015,7 +3212,7 @@ func TestErrorGetGitFiles(t *testing.T) {
|
|||
},
|
||||
}, want: nil, wantErr: assert.Error},
|
||||
{name: "InvalidResolveRevision", fields: fields{service: func() *Service {
|
||||
s, _ := newServiceWithOpt(func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
|
||||
s, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
|
||||
gitClient.On("Checkout", mock.Anything, mock.Anything).Return(nil)
|
||||
gitClient.On("LsRemote", mock.Anything).Return("", fmt.Errorf("ah error"))
|
||||
paths.On("GetPath", mock.Anything).Return(".", nil)
|
||||
|
|
@ -3048,7 +3245,7 @@ func TestGetGitFiles(t *testing.T) {
|
|||
files := []string{"./testdata/git-files-dirs/somedir/config.yaml",
|
||||
"./testdata/git-files-dirs/config.yaml", "./testdata/git-files-dirs/config.yaml", "./testdata/git-files-dirs/app/foo/bar/config.yaml"}
|
||||
root := ""
|
||||
s, _ := newServiceWithOpt(func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
|
||||
s, _, cacheMocks := newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
|
||||
gitClient.On("Init").Return(nil)
|
||||
gitClient.On("Fetch", mock.Anything).Return(nil)
|
||||
gitClient.On("Checkout", mock.Anything, mock.Anything).Once().Return(nil)
|
||||
|
|
@ -3081,6 +3278,10 @@ func TestGetGitFiles(t *testing.T) {
|
|||
fileResponse, err = s.GetGitFiles(context.TODO(), filesRequest)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, fileResponse.GetMap())
|
||||
cacheMocks.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{
|
||||
ExternalSets: 1,
|
||||
ExternalGets: 2,
|
||||
})
|
||||
}
|
||||
|
||||
func Test_getRepoSanitizerRegex(t *testing.T) {
|
||||
|
|
@ -3090,3 +3291,45 @@ func Test_getRepoSanitizerRegex(t *testing.T) {
|
|||
msg = r.ReplaceAllString("error message containing /tmp/_argocd-repo/SENSITIVE/with/trailing/path and other stuff", "<path to cached source>")
|
||||
assert.Equal(t, "error message containing <path to cached source>/with/trailing/path and other stuff", msg)
|
||||
}
|
||||
|
||||
func TestGetRevisionChartDetails(t *testing.T) {
|
||||
t.Run("Test revision semvar", func(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
service := newService(t, root)
|
||||
_, err := service.GetRevisionChartDetails(context.Background(), &apiclient.RepoServerRevisionChartDetailsRequest{
|
||||
Repo: &v1alpha1.Repository{
|
||||
Repo: fmt.Sprintf("file://%s", root),
|
||||
Name: "test-repo-name",
|
||||
Type: "helm",
|
||||
},
|
||||
Name: "test-name",
|
||||
Revision: "test-revision",
|
||||
})
|
||||
assert.ErrorContains(t, err, "invalid revision")
|
||||
})
|
||||
|
||||
t.Run("Test GetRevisionChartDetails", func(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
service := newService(t, root)
|
||||
repoUrl := fmt.Sprintf("file://%s", root)
|
||||
err := service.cache.SetRevisionChartDetails(repoUrl, "my-chart", "1.1.0", &argoappv1.ChartDetails{
|
||||
Description: "test-description",
|
||||
Home: "test-home",
|
||||
Maintainers: []string{"test-maintainer"},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
chartDetails, err := service.GetRevisionChartDetails(context.Background(), &apiclient.RepoServerRevisionChartDetailsRequest{
|
||||
Repo: &v1alpha1.Repository{
|
||||
Repo: fmt.Sprintf("file://%s", root),
|
||||
Name: "test-repo-name",
|
||||
Type: "helm",
|
||||
},
|
||||
Name: "my-chart",
|
||||
Revision: "1.1.0",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "test-description", chartDetails.Description)
|
||||
assert.Equal(t, "test-home", chartDetails.Home)
|
||||
assert.Equal(t, []string{"test-maintainer"}, chartDetails.Maintainers)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ if obj.status ~= nil then
|
|||
|
||||
if obj.status.state == "error" then
|
||||
hs.status = "Degraded"
|
||||
hs.message = "Cluster is on error: " .. table.concat(obj.status.messages, ", ")
|
||||
hs.message = "Cluster is on error: " .. table.concat(obj.status.message, ", ")
|
||||
return hs
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ status:
|
|||
pmm: {}
|
||||
proxysql: {}
|
||||
pxc:
|
||||
image: ''
|
||||
image: ""
|
||||
ready: 1
|
||||
size: 2
|
||||
status: error
|
||||
|
|
@ -20,5 +20,5 @@ status:
|
|||
ready: 1
|
||||
size: 2
|
||||
state: error
|
||||
messages:
|
||||
- we lost node
|
||||
message:
|
||||
- we lost node
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ func TestNotificationServer(t *testing.T) {
|
|||
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)
|
||||
apiFactory := api.NewFactory(settings.GetFactorySettings(argocdService, "argocd-notifications-secret", "argocd-notifications-cm", false), testNamespace, secretInformer, configMapInformer)
|
||||
|
||||
t.Run("TestListServices", func(t *testing.T) {
|
||||
server := NewServer(apiFactory)
|
||||
|
|
|
|||
|
|
@ -288,7 +288,7 @@ func NewServer(ctx context.Context, opts ArgoCDServerOpts) *ArgoCDServer {
|
|||
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)
|
||||
apiFactory := api.NewFactory(settings_notif.GetFactorySettings(argocdService, "argocd-notifications-secret", "argocd-notifications-cm", false), opts.Namespace, secretInformer, configMapInformer)
|
||||
|
||||
dbInstance := db.NewDB(opts.Namespace, settingsMgr, opts.KubeClientset)
|
||||
logger := log.NewEntry(log.StandardLogger())
|
||||
|
|
|
|||
|
|
@ -599,6 +599,134 @@ func TestRenderHelmValuesObject(t *testing.T) {
|
|||
|
||||
}
|
||||
|
||||
func TestTemplatePatch(t *testing.T) {
|
||||
|
||||
expectedApp := argov1alpha1.Application{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: application.ApplicationKind,
|
||||
APIVersion: "argoproj.io/v1alpha1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "my-cluster-guestbook",
|
||||
Namespace: fixture.TestNamespace(),
|
||||
Finalizers: []string{"resources-finalizer.argocd.argoproj.io"},
|
||||
Annotations: map[string]string{
|
||||
"annotation-some-key": "annotation-some-value",
|
||||
},
|
||||
},
|
||||
Spec: argov1alpha1.ApplicationSpec{
|
||||
Project: "default",
|
||||
Source: &argov1alpha1.ApplicationSource{
|
||||
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
|
||||
TargetRevision: "HEAD",
|
||||
Path: "guestbook",
|
||||
},
|
||||
Destination: argov1alpha1.ApplicationDestination{
|
||||
Server: "https://kubernetes.default.svc",
|
||||
Namespace: "guestbook",
|
||||
},
|
||||
SyncPolicy: &argov1alpha1.SyncPolicy{
|
||||
SyncOptions: argov1alpha1.SyncOptions{"CreateNamespace=true"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
templatePatch := `{
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
{{- range $k, $v := .annotations }}
|
||||
"{{ $k }}": "{{ $v }}"
|
||||
{{- end }}
|
||||
}
|
||||
},
|
||||
{{- if .createNamespace }}
|
||||
"spec": {
|
||||
"syncPolicy": {
|
||||
"syncOptions": [
|
||||
"CreateNamespace=true"
|
||||
]
|
||||
}
|
||||
}
|
||||
{{- end }}
|
||||
}
|
||||
`
|
||||
|
||||
var expectedAppNewNamespace *argov1alpha1.Application
|
||||
var expectedAppNewMetadata *argov1alpha1.Application
|
||||
|
||||
Given(t).
|
||||
// Create a ListGenerator-based ApplicationSet
|
||||
When().Create(v1alpha1.ApplicationSet{ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "patch-template",
|
||||
},
|
||||
Spec: v1alpha1.ApplicationSetSpec{
|
||||
GoTemplate: true,
|
||||
Template: v1alpha1.ApplicationSetTemplate{
|
||||
ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{.cluster}}-guestbook"},
|
||||
Spec: argov1alpha1.ApplicationSpec{
|
||||
Project: "default",
|
||||
Source: &argov1alpha1.ApplicationSource{
|
||||
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
|
||||
TargetRevision: "HEAD",
|
||||
Path: "guestbook",
|
||||
},
|
||||
Destination: argov1alpha1.ApplicationDestination{
|
||||
Server: "{{.url}}",
|
||||
Namespace: "guestbook",
|
||||
},
|
||||
},
|
||||
},
|
||||
TemplatePatch: &templatePatch,
|
||||
Generators: []v1alpha1.ApplicationSetGenerator{
|
||||
{
|
||||
List: &v1alpha1.ListGenerator{
|
||||
Elements: []apiextensionsv1.JSON{{
|
||||
Raw: []byte(`{
|
||||
"cluster": "my-cluster",
|
||||
"url": "https://kubernetes.default.svc",
|
||||
"createNamespace": true,
|
||||
"annotations": {
|
||||
"annotation-some-key": "annotation-some-value"
|
||||
}
|
||||
}`),
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}).Then().Expect(ApplicationsExist([]argov1alpha1.Application{expectedApp})).
|
||||
|
||||
// Update the ApplicationSet template namespace, and verify it updates the Applications
|
||||
When().
|
||||
And(func() {
|
||||
expectedAppNewNamespace = expectedApp.DeepCopy()
|
||||
expectedAppNewNamespace.Spec.Destination.Namespace = "guestbook2"
|
||||
}).
|
||||
Update(func(appset *v1alpha1.ApplicationSet) {
|
||||
appset.Spec.Template.Spec.Destination.Namespace = "guestbook2"
|
||||
}).Then().Expect(ApplicationsExist([]argov1alpha1.Application{*expectedAppNewNamespace})).
|
||||
|
||||
// Update the metadata fields in the appset template, and make sure it propagates to the apps
|
||||
When().
|
||||
And(func() {
|
||||
expectedAppNewMetadata = expectedAppNewNamespace.DeepCopy()
|
||||
expectedAppNewMetadata.ObjectMeta.Labels = map[string]string{
|
||||
"label-key": "label-value",
|
||||
}
|
||||
}).
|
||||
Update(func(appset *v1alpha1.ApplicationSet) {
|
||||
appset.Spec.Template.Labels = map[string]string{"label-key": "label-value"}
|
||||
}).Then().Expect(ApplicationsExist([]argov1alpha1.Application{*expectedAppNewMetadata})).
|
||||
|
||||
// verify the ApplicationSet status conditions were set correctly
|
||||
Expect(ApplicationSetHasConditions("patch-template", ExpectedConditions)).
|
||||
|
||||
// Delete the ApplicationSet, and verify it deletes the Applications
|
||||
When().
|
||||
Delete().Then().Expect(ApplicationsDoNotExist([]argov1alpha1.Application{*expectedAppNewMetadata}))
|
||||
|
||||
}
|
||||
|
||||
func TestSyncPolicyCreateUpdate(t *testing.T) {
|
||||
|
||||
expectedApp := argov1alpha1.Application{
|
||||
|
|
|
|||
|
|
@ -31,35 +31,45 @@ export const ApplicationResourceList = ({
|
|||
return null;
|
||||
}
|
||||
const parentNode = ((resources || []).length > 0 && (getResNode(tree.nodes, nodeKey(resources[0])) as ResourceNode)?.parentRefs?.[0]) || ({} as ResourceRef);
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const view = searchParams.get('view');
|
||||
|
||||
const ParentRefDetails = () => {
|
||||
return Object.keys(parentNode).length > 0 ? (
|
||||
<div className='resource-parent-node-info-title'>
|
||||
<div>Parent Node Info</div>
|
||||
<div className='resource-parent-node-info-title__label'>
|
||||
<div>Name:</div>
|
||||
<div>{parentNode?.name}</div>
|
||||
</div>
|
||||
<div className='resource-parent-node-info-title__label'>
|
||||
<div>Kind:</div>
|
||||
<div>{parentNode?.kind}</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div />
|
||||
);
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<div className='resource-details__header' style={{paddingTop: '20px'}}>
|
||||
{Object.keys(parentNode).length > 0 && (
|
||||
<div className='resource-parent-node-info-title'>
|
||||
<div> Parent Node Info</div>
|
||||
<div className='resource-parent-node-info-title__label'>
|
||||
<div>Name:</div>
|
||||
<div>{parentNode?.name}</div>
|
||||
</div>
|
||||
<div className='resource-parent-node-info-title__label'>
|
||||
<div>Kind:</div>
|
||||
<div> {parentNode?.kind}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Display only when the view is set to or network */}
|
||||
{(view === 'tree' || view === 'network') && (
|
||||
<div className='resource-details__header' style={{paddingTop: '20px'}}>
|
||||
<ParentRefDetails />
|
||||
</div>
|
||||
)}
|
||||
<div className='argo-table-list argo-table-list--clickable'>
|
||||
<div className='argo-table-list__head'>
|
||||
<div className='row'>
|
||||
<div className='columns small-1 xxxlarge-1' />
|
||||
<div className='columns small-2 xxxlarge-2'>NAME</div>
|
||||
<div className='columns small-2 xxxlarge-1'>NAME</div>
|
||||
<div className='columns small-1 xxxlarge-1'>GROUP/KIND</div>
|
||||
<div className='columns small-1 xxxlarge-1'>SYNC ORDER</div>
|
||||
<div className='columns small-2 xxxlarge-2'>NAMESPACE</div>
|
||||
<div className='columns small-2 xxxlarge-1'>NAMESPACE</div>
|
||||
{(parentNode.kind === 'Rollout' || parentNode.kind === 'Deployment') && <div className='columns small-1 xxxlarge-1'>REVISION</div>}
|
||||
<div className='columns small-2 xxxlarge-2'>CREATED AT</div>
|
||||
<div className='columns small-2 xxxlarge-2'>STATUS</div>
|
||||
<div className='columns small-2 xxxlarge-1'>CREATED AT</div>
|
||||
<div className='columns small-2 xxxlarge-1'>STATUS</div>
|
||||
</div>
|
||||
</div>
|
||||
{resources
|
||||
|
|
@ -79,7 +89,7 @@ export const ApplicationResourceList = ({
|
|||
<div>{ResourceLabel({kind: res.kind})}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='columns small-2 xxxlarge-2'>
|
||||
<div className='columns small-2 xxxlarge-1'>
|
||||
{res.name}
|
||||
{res.kind === 'Application' && (
|
||||
<Consumer>
|
||||
|
|
@ -98,7 +108,7 @@ export const ApplicationResourceList = ({
|
|||
</div>
|
||||
<div className='columns small-1 xxxlarge-1'>{[res.group, res.kind].filter(item => !!item).join('/')}</div>
|
||||
<div className='columns small-1 xxxlarge-1'>{res.syncWave || '-'}</div>
|
||||
<div className='columns small-2 xxxlarge-2'>{res.namespace}</div>
|
||||
<div className='columns small-2 xxxlarge-1'>{res.namespace}</div>
|
||||
{res.kind === 'ReplicaSet' &&
|
||||
((getResNode(tree.nodes, nodeKey(res)) as ResourceNode).info || [])
|
||||
.filter(tag => !tag.name.includes('Node'))
|
||||
|
|
@ -111,7 +121,7 @@ export const ApplicationResourceList = ({
|
|||
);
|
||||
})}
|
||||
|
||||
<div className='columns small-2 xxxlarge-2'>
|
||||
<div className='columns small-2 xxxlarge-1'>
|
||||
{res.createdAt && (
|
||||
<span>
|
||||
<Moment fromNow={true} ago={true}>
|
||||
|
|
@ -121,7 +131,7 @@ export const ApplicationResourceList = ({
|
|||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className='columns small-2 xxxlarge-2'>
|
||||
<div className='columns small-2 xxxlarge-1'>
|
||||
{res.health && (
|
||||
<React.Fragment>
|
||||
<HealthStatusIcon state={res.health} /> {res.health.status}
|
||||
|
|
|
|||
|
|
@ -564,11 +564,13 @@ function renderPodGroup(props: ApplicationResourceTreeProps, id: string, node: R
|
|||
</div>
|
||||
<div className='application-resource-tree__node--lower-section'>
|
||||
{[podGroupHealthy, podGroupDegraded, podGroupInProgress].map((pods, index) => {
|
||||
return (
|
||||
<div key={index} className={`application-resource-tree__node--lower-section__pod-group`}>
|
||||
{renderPodGroupByStatus(props, node, pods, showPodGroupByStatus)}
|
||||
</div>
|
||||
);
|
||||
if (pods.length > 0) {
|
||||
return (
|
||||
<div key={index} className={`application-resource-tree__node--lower-section__pod-group`}>
|
||||
{renderPodGroupByStatus(props, node, pods, showPodGroupByStatus)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -280,7 +280,7 @@ export const ResourceDetails = (props: ResourceDetailsProps) => {
|
|||
const settings = await services.authService.settings();
|
||||
const execEnabled = settings.execEnabled;
|
||||
const logsAllowed = await services.accounts.canI('logs', 'get', application.spec.project + '/' + application.metadata.name);
|
||||
const execAllowed = await services.accounts.canI('exec', 'create', application.spec.project + '/' + application.metadata.name);
|
||||
const execAllowed = execEnabled && (await services.accounts.canI('exec', 'create', application.spec.project + '/' + application.metadata.name));
|
||||
const links = await services.applications.getResourceLinks(application.metadata.name, application.metadata.namespace, selectedNode).catch(() => null);
|
||||
return {controlledState, liveState, events, podState, execEnabled, execAllowed, logsAllowed, links};
|
||||
}}>
|
||||
|
|
|
|||
|
|
@ -473,8 +473,8 @@ function getActionItems(
|
|||
const execAction = services.authService
|
||||
.settings()
|
||||
.then(async settings => {
|
||||
const execAllowed = await services.accounts.canI('exec', 'create', application.spec.project + '/' + application.metadata.name);
|
||||
if (resource.kind === 'Pod' && settings.execEnabled && execAllowed) {
|
||||
const execAllowed = settings.execEnabled && (await services.accounts.canI('exec', 'create', application.spec.project + '/' + application.metadata.name));
|
||||
if (resource.kind === 'Pod' && execAllowed) {
|
||||
return [
|
||||
{
|
||||
title: 'Exec',
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {FormApi, Text} from 'react-form';
|
|||
import {RouteComponentProps} from 'react-router';
|
||||
|
||||
import {BadgePanel, CheckboxField, DataLoader, EditablePanel, ErrorNotification, MapInputField, Page, Query} from '../../../shared/components';
|
||||
import {AppContext, Consumer} from '../../../shared/context';
|
||||
import {AppContext, Consumer, AuthSettingsCtx} from '../../../shared/context';
|
||||
import {GroupKind, Groups, Project, DetailedProjectsResponse, ProjectSpec, ResourceKinds} from '../../../shared/models';
|
||||
import {CreateJWTTokenParams, DeleteJWTTokenParams, ProjectRoleParams, services} from '../../../shared/services';
|
||||
|
||||
|
|
@ -52,6 +52,7 @@ function reduceGlobal(projs: Project[]): ProjectSpec & {count: number} {
|
|||
merged.namespaceResourceWhitelist = merged.namespaceResourceWhitelist.concat(proj.spec.namespaceResourceWhitelist || []);
|
||||
merged.sourceRepos = merged.sourceRepos.concat(proj.spec.sourceRepos || []);
|
||||
merged.destinations = merged.destinations.concat(proj.spec.destinations || []);
|
||||
merged.sourceNamespaces = merged.sourceNamespaces.concat(proj.spec.sourceNamespaces || []);
|
||||
|
||||
merged.sourceRepos = merged.sourceRepos.filter((item, index) => {
|
||||
return (
|
||||
|
|
@ -106,6 +107,15 @@ function reduceGlobal(projs: Project[]): ProjectSpec & {count: number} {
|
|||
})
|
||||
);
|
||||
});
|
||||
|
||||
merged.sourceNamespaces = merged.sourceNamespaces.filter((item, index) => {
|
||||
return (
|
||||
index ===
|
||||
merged.sourceNamespaces.findIndex(obj => {
|
||||
return obj === item;
|
||||
})
|
||||
);
|
||||
});
|
||||
merged.count += 1;
|
||||
|
||||
return merged;
|
||||
|
|
@ -116,6 +126,7 @@ function reduceGlobal(projs: Project[]): ProjectSpec & {count: number} {
|
|||
namespaceResourceWhitelist: new Array<GroupKind>(),
|
||||
clusterResourceWhitelist: new Array<GroupKind>(),
|
||||
sourceRepos: [],
|
||||
sourceNamespaces: [],
|
||||
signatureKeys: [],
|
||||
destinations: [],
|
||||
description: '',
|
||||
|
|
@ -648,7 +659,51 @@ export class ProjectDetails extends React.Component<RouteComponentProps<{name: s
|
|||
}
|
||||
items={[]}
|
||||
/>
|
||||
|
||||
<AuthSettingsCtx.Consumer>
|
||||
{authCtx =>
|
||||
authCtx.appsInAnyNamespaceEnabled && (
|
||||
<EditablePanel
|
||||
save={item => this.saveProject(item)}
|
||||
values={proj}
|
||||
title={
|
||||
<React.Fragment>SOURCE NAMESPACES {helpTip('Kubernetes namespaces where application resources are allowed to be created in')}</React.Fragment>
|
||||
}
|
||||
view={
|
||||
<React.Fragment>
|
||||
{proj.spec.sourceNamespaces
|
||||
? proj.spec.sourceNamespaces.map((namespace, i) => (
|
||||
<div className='row white-box__details-row' key={i}>
|
||||
<div className='columns small-12'>{namespace}</div>
|
||||
</div>
|
||||
))
|
||||
: emptyMessage('source namespaces')}
|
||||
</React.Fragment>
|
||||
}
|
||||
edit={formApi => (
|
||||
<React.Fragment>
|
||||
{(formApi.values.spec.sourceNamespaces || []).map((_: Project, i: number) => (
|
||||
<div className='row white-box__details-row' key={i}>
|
||||
<div className='columns small-12'>
|
||||
<FormField formApi={formApi} field={`spec.sourceNamespaces[${i}]`} component={AutocompleteField} />
|
||||
<i
|
||||
className='fa fa-times'
|
||||
onClick={() => formApi.setValue('spec.sourceNamespaces', removeEl(formApi.values.spec.sourceNamespaces, i))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
className='argo-button argo-button--short'
|
||||
onClick={() => formApi.setValue('spec.sourceNamespaces', (formApi.values.spec.sourceNamespaces || []).concat('*'))}>
|
||||
ADD SOURCE
|
||||
</button>
|
||||
</React.Fragment>
|
||||
)}
|
||||
items={[]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</AuthSettingsCtx.Consumer>
|
||||
<EditablePanel
|
||||
save={item => this.saveProject(item)}
|
||||
values={proj}
|
||||
|
|
|
|||
|
|
@ -99,6 +99,36 @@ function viewSourceReposInfoList(type: field, proj: Project) {
|
|||
);
|
||||
}
|
||||
|
||||
const sourceNamespacesInfoByField: {[type: string]: {title: string; helpText: string}} = {
|
||||
sourceNamespaces: {
|
||||
title: 'source namespaces',
|
||||
helpText: 'Kubernetes namespaces where application resources are allowed to be created in'
|
||||
}
|
||||
};
|
||||
|
||||
function viewSourceNamespacesInfoList(type: field, proj: Project) {
|
||||
const info = sourceNamespacesInfoByField[type];
|
||||
const list = proj.spec[type] as Array<string>;
|
||||
return (
|
||||
<React.Fragment>
|
||||
<p className='project-details__list-title'>
|
||||
{info.title} {helpTip(info.helpText)}
|
||||
</p>
|
||||
{(list || []).length > 0 ? (
|
||||
<React.Fragment>
|
||||
{list.map((namespace, i) => (
|
||||
<div className='row white-box__details-row' key={i}>
|
||||
<div className='columns small-12'>{namespace}</div>
|
||||
</div>
|
||||
))}
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<p>The {info.title} is empty</p>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
const destinationsInfoByField: {[type: string]: {title: string; helpText: string}} = {
|
||||
destinations: {
|
||||
title: 'destinations',
|
||||
|
|
@ -180,6 +210,8 @@ export const ResourceListsPanel = ({proj, saveProject, title}: {proj: Project; t
|
|||
<React.Fragment key={key}>{viewList(key as field, proj)}</React.Fragment>
|
||||
))}
|
||||
{!proj.metadata && Object.keys(sourceReposInfoByField).map(key => <React.Fragment key={key}>{viewSourceReposInfoList(key as field, proj)}</React.Fragment>)}
|
||||
{!proj.metadata &&
|
||||
Object.keys(sourceNamespacesInfoByField).map(key => <React.Fragment key={key}>{viewSourceNamespacesInfoList(key as field, proj)}</React.Fragment>)}
|
||||
{!proj.metadata && Object.keys(destinationsInfoByField).map(key => <React.Fragment key={key}>{viewDestinationsInfoList(key as field, proj)}</React.Fragment>)}
|
||||
</React.Fragment>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -714,6 +714,7 @@ export interface ProjectSignatureKey {
|
|||
|
||||
export interface ProjectSpec {
|
||||
sourceRepos: string[];
|
||||
sourceNamespaces: string[];
|
||||
destinations: ApplicationDestination[];
|
||||
description: string;
|
||||
roles: ProjectRole[];
|
||||
|
|
|
|||
|
|
@ -58,6 +58,13 @@ $deselected-text: #818d94;
|
|||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__tooltip {
|
||||
max-width: 300px;
|
||||
@media screen and (max-width: 590px) {
|
||||
max-width: 250px;
|
||||
}
|
||||
}
|
||||
|
||||
&__nav-item {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ export const Sidebar = (props: SidebarProps) => {
|
|||
</div>
|
||||
|
||||
{(props.navItems || []).map(item => (
|
||||
<Tooltip key={item.path} content={item?.tooltip || item.title} {...tooltipProps}>
|
||||
<Tooltip key={item.path} content={<div className='sidebar__tooltip'>{item?.tooltip || item.title}</div>} {...tooltipProps}>
|
||||
<div
|
||||
key={item.title}
|
||||
className={`sidebar__nav-item ${locationPath === item.path || locationPath.startsWith(`${item.path}/`) ? 'sidebar__nav-item--active' : ''}`}
|
||||
|
|
|
|||
65
util/cache/mocks/cacheclient.go
vendored
Normal file
65
util/cache/mocks/cacheclient.go
vendored
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
cache "github.com/argoproj/argo-cd/v2/util/cache"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
type MockCacheClient struct {
|
||||
mock.Mock
|
||||
BaseCache cache.CacheClient
|
||||
ReadDelay time.Duration
|
||||
WriteDelay time.Duration
|
||||
}
|
||||
|
||||
func (c *MockCacheClient) Set(item *cache.Item) error {
|
||||
args := c.Called(item)
|
||||
if len(args) > 0 && args.Get(0) != nil {
|
||||
return args.Get(0).(error)
|
||||
}
|
||||
if c.WriteDelay > 0 {
|
||||
time.Sleep(c.WriteDelay)
|
||||
}
|
||||
return c.BaseCache.Set(item)
|
||||
}
|
||||
|
||||
func (c *MockCacheClient) Get(key string, obj interface{}) error {
|
||||
args := c.Called(key, obj)
|
||||
if len(args) > 0 && args.Get(0) != nil {
|
||||
return args.Get(0).(error)
|
||||
}
|
||||
if c.ReadDelay > 0 {
|
||||
time.Sleep(c.ReadDelay)
|
||||
}
|
||||
return c.BaseCache.Get(key, obj)
|
||||
}
|
||||
|
||||
func (c *MockCacheClient) Delete(key string) error {
|
||||
args := c.Called(key)
|
||||
if len(args) > 0 && args.Get(0) != nil {
|
||||
return args.Get(0).(error)
|
||||
}
|
||||
if c.WriteDelay > 0 {
|
||||
time.Sleep(c.WriteDelay)
|
||||
}
|
||||
return c.BaseCache.Delete(key)
|
||||
}
|
||||
|
||||
func (c *MockCacheClient) OnUpdated(ctx context.Context, key string, callback func() error) error {
|
||||
args := c.Called(ctx, key, callback)
|
||||
if len(args) > 0 && args.Get(0) != nil {
|
||||
return args.Get(0).(error)
|
||||
}
|
||||
return c.BaseCache.OnUpdated(ctx, key, callback)
|
||||
}
|
||||
|
||||
func (c *MockCacheClient) NotifyUpdated(key string) error {
|
||||
args := c.Called(key)
|
||||
if len(args) > 0 && args.Get(0) != nil {
|
||||
return args.Get(0).(error)
|
||||
}
|
||||
return c.BaseCache.NotifyUpdated(key)
|
||||
}
|
||||
|
|
@ -12,17 +12,20 @@ import (
|
|||
service "github.com/argoproj/argo-cd/v2/util/notification/argocd"
|
||||
)
|
||||
|
||||
func GetFactorySettings(argocdService service.Service, secretName, configMapName string) api.Settings {
|
||||
func GetFactorySettings(argocdService service.Service, secretName, configMapName string, selfServiceNotificationEnabled bool) api.Settings {
|
||||
return api.Settings{
|
||||
SecretName: secretName,
|
||||
ConfigMapName: configMapName,
|
||||
InitGetVars: func(cfg *api.Config, configMap *v1.ConfigMap, secret *v1.Secret) (api.GetVars, error) {
|
||||
if selfServiceNotificationEnabled {
|
||||
return initGetVarsWithoutSecret(argocdService, cfg, configMap, secret)
|
||||
}
|
||||
return initGetVars(argocdService, cfg, configMap, secret)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func initGetVars(argocdService service.Service, cfg *api.Config, configMap *v1.ConfigMap, secret *v1.Secret) (api.GetVars, error) {
|
||||
func getContext(cfg *api.Config, configMap *v1.ConfigMap, secret *v1.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 {
|
||||
|
|
@ -32,6 +35,28 @@ func initGetVars(argocdService service.Service, cfg *api.Config, configMap *v1.C
|
|||
if err := ApplyLegacyConfig(cfg, context, configMap, secret); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return context, nil
|
||||
}
|
||||
|
||||
func initGetVarsWithoutSecret(argocdService service.Service, cfg *api.Config, configMap *v1.ConfigMap, secret *v1.Secret) (api.GetVars, error) {
|
||||
context, err := getContext(cfg, configMap, secret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return func(obj map[string]interface{}, dest services.Destination) map[string]interface{} {
|
||||
return expression.Spawn(&unstructured.Unstructured{Object: obj}, argocdService, map[string]interface{}{
|
||||
"app": obj,
|
||||
"context": injectLegacyVar(context, dest.Service),
|
||||
})
|
||||
}, nil
|
||||
}
|
||||
|
||||
func initGetVars(argocdService service.Service, cfg *api.Config, configMap *v1.ConfigMap, secret *v1.Secret) (api.GetVars, error) {
|
||||
context, err := getContext(cfg, configMap, secret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return func(obj map[string]interface{}, dest services.Destination) map[string]interface{} {
|
||||
return expression.Spawn(&unstructured.Unstructured{Object: obj}, argocdService, map[string]interface{}{
|
||||
|
|
|
|||
Loading…
Reference in a new issue