Merge branch 'master' into master

This commit is contained in:
yevhenvolchenko 2023-12-01 20:41:22 +00:00 committed by GitHub
commit 901095bf81
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
59 changed files with 2310 additions and 885 deletions

View file

@ -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

View file

@ -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'}"

View file

@ -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()})

View file

@ -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)

View 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
}

View 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)
}

View file

@ -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 {

View file

@ -6143,6 +6143,9 @@
},
"template": {
"$ref": "#/definitions/v1alpha1ApplicationSetTemplate"
},
"templatePatch": {
"type": "string"
}
}
},

View file

@ -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
}

View file

@ -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()

View file

@ -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)

View file

@ -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()

View file

@ -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

View file

@ -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.

View file

@ -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"

View file

@ -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.

View file

@ -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
```

View file

@ -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
```

View file

@ -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
```

View file

@ -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
```

View file

@ -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 * * *" \

View file

@ -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
```

View file

@ -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
```

View file

@ -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
```

View file

@ -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

View file

@ -16,4 +16,13 @@ rules:
- list
- watch
- update
- patch
- patch
- apiGroups:
- ""
resources:
- secrets
- configmaps
verbs:
- get
- list
- watch

View file

@ -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:

View file

@ -20123,6 +20123,8 @@ spec:
- metadata
- spec
type: object
templatePatch:
type: string
required:
- generators
- template

View file

@ -15183,6 +15183,8 @@ spec:
- metadata
- spec
type: object
templatePatch:
type: string
required:
- generators
- template

View file

@ -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:

View file

@ -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:

View file

@ -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:

View file

@ -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:

View file

@ -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 := &notificationController{
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) {

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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"},
},

View file

@ -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
}

View 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
}

View file

@ -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)

View file

@ -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)
})
}

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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())

View file

@ -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{

View file

@ -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} &nbsp;

View file

@ -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>

View file

@ -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};
}}>

View file

@ -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',

View file

@ -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}

View file

@ -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>
}

View file

@ -714,6 +714,7 @@ export interface ProjectSignatureKey {
export interface ProjectSpec {
sourceRepos: string[];
sourceNamespaces: string[];
destinations: ApplicationDestination[];
description: string;
roles: ProjectRole[];

View file

@ -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;

View file

@ -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
View 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)
}

View file

@ -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{}{