github.com/argoproj/argo-cd/v3@v3.2.1/cmd/argocd/commands/admin/settings_test.go (about)

     1  package admin
     2  
     3  import (
     4  	"context"
     5  	"io"
     6  	"os"
     7  	"testing"
     8  
     9  	"github.com/argoproj/argo-cd/v3/common"
    10  	utilio "github.com/argoproj/argo-cd/v3/util/io"
    11  	"github.com/argoproj/argo-cd/v3/util/settings"
    12  
    13  	"github.com/stretchr/testify/assert"
    14  	"github.com/stretchr/testify/require"
    15  	corev1 "k8s.io/api/core/v1"
    16  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    17  	"k8s.io/client-go/kubernetes/fake"
    18  )
    19  
    20  func captureStdout(callback func()) (string, error) {
    21  	oldStdout := os.Stdout
    22  	oldStderr := os.Stderr
    23  	r, w, err := os.Pipe()
    24  	if err != nil {
    25  		return "", err
    26  	}
    27  	os.Stdout = w
    28  	defer func() {
    29  		os.Stdout = oldStdout
    30  		os.Stderr = oldStderr
    31  	}()
    32  
    33  	callback()
    34  	utilio.Close(w)
    35  
    36  	data, err := io.ReadAll(r)
    37  	if err != nil {
    38  		return "", err
    39  	}
    40  	return string(data), err
    41  }
    42  
    43  func newSettingsManager(data map[string]string) *settings.SettingsManager {
    44  	ctx := context.Background()
    45  
    46  	clientset := fake.NewClientset(&corev1.ConfigMap{
    47  		ObjectMeta: metav1.ObjectMeta{
    48  			Namespace: "default",
    49  			Name:      common.ArgoCDConfigMapName,
    50  			Labels: map[string]string{
    51  				"app.kubernetes.io/part-of": "argocd",
    52  			},
    53  		},
    54  		Data: data,
    55  	}, &corev1.Secret{
    56  		ObjectMeta: metav1.ObjectMeta{
    57  			Namespace: "default",
    58  			Name:      common.ArgoCDSecretName,
    59  		},
    60  		Data: map[string][]byte{
    61  			"admin.password":   []byte("test"),
    62  			"server.secretkey": []byte("test"),
    63  		},
    64  	})
    65  	return settings.NewSettingsManager(ctx, clientset, "default")
    66  }
    67  
    68  type fakeCmdContext struct {
    69  	mgr *settings.SettingsManager
    70  }
    71  
    72  func newCmdContext(data map[string]string) *fakeCmdContext {
    73  	return &fakeCmdContext{mgr: newSettingsManager(data)}
    74  }
    75  
    76  func (ctx *fakeCmdContext) createSettingsManager(context.Context) (*settings.SettingsManager, error) {
    77  	return ctx.mgr, nil
    78  }
    79  
    80  type validatorTestCase struct {
    81  	validator       string
    82  	data            map[string]string
    83  	containsSummary string
    84  	containsError   string
    85  }
    86  
    87  func TestCreateSettingsManager(t *testing.T) {
    88  	ctx := t.Context()
    89  
    90  	f, closer, err := tempFile(`apiVersion: v1
    91  kind: ConfigMap
    92  metadata:
    93    name: argocd-cm
    94  data:
    95    url: https://myargocd.com`)
    96  	require.NoError(t, err)
    97  	defer utilio.Close(closer)
    98  
    99  	opts := settingsOpts{argocdCMPath: f}
   100  	settingsManager, err := opts.createSettingsManager(ctx)
   101  
   102  	require.NoError(t, err)
   103  
   104  	argoCDSettings, err := settingsManager.GetSettings()
   105  	require.NoError(t, err)
   106  
   107  	assert.Equal(t, "https://myargocd.com", argoCDSettings.URL)
   108  }
   109  
   110  func TestValidator(t *testing.T) {
   111  	testCases := map[string]validatorTestCase{
   112  		"General_SSOIsNotConfigured": {
   113  			validator: "general", containsSummary: "SSO is not configured",
   114  		},
   115  		"General_DexInvalidConfig": {
   116  			validator:     "general",
   117  			data:          map[string]string{"dex.config": "abcdefg"},
   118  			containsError: "invalid dex.config",
   119  		},
   120  		"General_OIDCConfigured": {
   121  			validator: "general",
   122  			data: map[string]string{
   123  				"url": "https://myargocd.com",
   124  				"oidc.config": `
   125  name: Okta
   126  issuer: https://dev-123456.oktapreview.com
   127  clientID: aaaabbbbccccddddeee
   128  clientSecret: aaaabbbbccccddddeee`,
   129  			},
   130  			containsSummary: "OIDC is configured",
   131  		},
   132  		"General_DexConfiguredMissingURL": {
   133  			validator: "general",
   134  			data: map[string]string{
   135  				"dex.config": `connectors:
   136  - type: github
   137    name: GitHub
   138    config:
   139      clientID: aabbccddeeff00112233
   140      clientSecret: aabbccddeeff00112233`,
   141  			},
   142  			containsSummary: "Dex is configured ('url' field is missing)",
   143  		},
   144  		"Kustomize_ModifiedOptions": {
   145  			validator:       "kustomize",
   146  			containsSummary: "default options",
   147  		},
   148  		"Kustomize_DefaultOptions": {
   149  			validator: "kustomize",
   150  			data: map[string]string{
   151  				"kustomize.buildOptions":  "updated-options (2 versions)",
   152  				"kustomize.versions.v123": "binary-123",
   153  				"kustomize.versions.v321": "binary-321",
   154  			},
   155  			containsSummary: "updated-options",
   156  		},
   157  		"Accounts": {
   158  			validator: "accounts",
   159  			data: map[string]string{
   160  				"accounts.user1": "apiKey, login",
   161  				"accounts.user2": "login",
   162  				"accounts.user3": "apiKey",
   163  			},
   164  			containsSummary: "4 accounts",
   165  		},
   166  		"ResourceOverrides": {
   167  			validator: "resource-overrides",
   168  			data: map[string]string{
   169  				"resource.customizations": `
   170  admissionregistration.k8s.io/MutatingWebhookConfiguration:
   171    ignoreDifferences: |
   172    jsonPointers:
   173    - /webhooks/0/clientConfig/caBundle`,
   174  			},
   175  			containsSummary: "2 resource overrides",
   176  		},
   177  	}
   178  	for name := range testCases {
   179  		tc := testCases[name]
   180  		t.Run(name, func(t *testing.T) {
   181  			validator, ok := validatorsByGroup[tc.validator]
   182  			if !assert.True(t, ok) {
   183  				return
   184  			}
   185  			summary, err := validator(newSettingsManager(tc.data))
   186  			if tc.containsSummary != "" {
   187  				require.NoError(t, err)
   188  				assert.Contains(t, summary, tc.containsSummary)
   189  			} else if tc.containsError != "" {
   190  				assert.ErrorContains(t, err, tc.containsError)
   191  			}
   192  		})
   193  	}
   194  }
   195  
   196  const (
   197  	testDeploymentYAML = `apiVersion: v1
   198  apiVersion: apps/v1
   199  kind: Deployment
   200  metadata:
   201    name: nginx-deployment
   202    labels:
   203      app: nginx
   204  spec:
   205    replicas: 0`
   206  )
   207  
   208  const (
   209  	testCustomResourceYAML = `apiVersion: v1
   210  apiVersion: example.com/v1alpha1
   211  kind: ExampleResource
   212  metadata:
   213    name: example-resource
   214    labels:
   215      app: example
   216  spec:
   217    replicas: 0`
   218  )
   219  
   220  const (
   221  	testCronJobYAML = `apiVersion: batch/v1
   222  kind: CronJob
   223  metadata:
   224    name: hello
   225    namespace: test-ns
   226    uid: "123"
   227  spec:
   228    schedule: "* * * * *"`
   229  )
   230  
   231  func tempFile(content string) (string, io.Closer, error) {
   232  	f, err := os.CreateTemp("", "*.yaml")
   233  	if err != nil {
   234  		return "", nil, err
   235  	}
   236  	_, err = f.WriteString(content)
   237  	if err != nil {
   238  		_ = os.Remove(f.Name())
   239  		return "", nil, err
   240  	}
   241  	defer func() {
   242  		if err = f.Close(); err != nil {
   243  			panic(err)
   244  		}
   245  	}()
   246  	return f.Name(), utilio.NewCloser(func() error {
   247  		return os.Remove(f.Name())
   248  	}), nil
   249  }
   250  
   251  func TestValidateSettingsCommand_NoErrors(t *testing.T) {
   252  	cmd := NewValidateSettingsCommand(newCmdContext(map[string]string{}))
   253  	out, err := captureStdout(func() {
   254  		err := cmd.Execute()
   255  		require.NoError(t, err)
   256  	})
   257  
   258  	require.NoError(t, err)
   259  	for k := range validatorsByGroup {
   260  		assert.Contains(t, out, "✅ "+k)
   261  	}
   262  }
   263  
   264  func TestResourceOverrideIgnoreDifferences(t *testing.T) {
   265  	f, closer, err := tempFile(testDeploymentYAML)
   266  	require.NoError(t, err)
   267  	defer utilio.Close(closer)
   268  
   269  	t.Run("NoOverridesConfigured", func(t *testing.T) {
   270  		cmd := NewResourceOverridesCommand(newCmdContext(map[string]string{}))
   271  		out, err := captureStdout(func() {
   272  			cmd.SetArgs([]string{"ignore-differences", f})
   273  			err := cmd.Execute()
   274  			require.NoError(t, err)
   275  		})
   276  		require.NoError(t, err)
   277  		assert.Contains(t, out, "Ignore differences are not configured for 'apps/Deployment'\n")
   278  	})
   279  
   280  	t.Run("DataIgnored", func(t *testing.T) {
   281  		cmd := NewResourceOverridesCommand(newCmdContext(map[string]string{
   282  			"resource.customizations": `apps/Deployment:
   283    ignoreDifferences: |
   284      jsonPointers:
   285      - /spec`,
   286  		}))
   287  		out, err := captureStdout(func() {
   288  			cmd.SetArgs([]string{"ignore-differences", f})
   289  			err := cmd.Execute()
   290  			require.NoError(t, err)
   291  		})
   292  		require.NoError(t, err)
   293  		assert.Contains(t, out, "< spec:")
   294  	})
   295  }
   296  
   297  func TestResourceOverrideHealth(t *testing.T) {
   298  	f, closer, err := tempFile(testCustomResourceYAML)
   299  	require.NoError(t, err)
   300  	defer utilio.Close(closer)
   301  
   302  	t.Run("NoHealthAssessment", func(t *testing.T) {
   303  		cmd := NewResourceOverridesCommand(newCmdContext(map[string]string{
   304  			"resource.customizations": `example.com/ExampleResource: {}`,
   305  		}))
   306  		out, err := captureStdout(func() {
   307  			cmd.SetArgs([]string{"health", f})
   308  			err := cmd.Execute()
   309  			require.NoError(t, err)
   310  		})
   311  		require.NoError(t, err)
   312  		assert.Contains(t, out, "Health script is not configured for 'example.com/ExampleResource'\n")
   313  	})
   314  
   315  	t.Run("HealthAssessmentConfigured", func(t *testing.T) {
   316  		cmd := NewResourceOverridesCommand(newCmdContext(map[string]string{
   317  			"resource.customizations": `example.com/ExampleResource:
   318    health.lua: |
   319      return { status = "Progressing" }
   320  `,
   321  		}))
   322  		out, err := captureStdout(func() {
   323  			cmd.SetArgs([]string{"health", f})
   324  			err := cmd.Execute()
   325  			require.NoError(t, err)
   326  		})
   327  		require.NoError(t, err)
   328  		assert.Contains(t, out, "Progressing")
   329  	})
   330  
   331  	t.Run("HealthAssessmentConfiguredWildcard", func(t *testing.T) {
   332  		cmd := NewResourceOverridesCommand(newCmdContext(map[string]string{
   333  			"resource.customizations": `example.com/*:
   334    health.lua: |
   335      return { status = "Progressing" }
   336  `,
   337  		}))
   338  		out, err := captureStdout(func() {
   339  			cmd.SetArgs([]string{"health", f})
   340  			err := cmd.Execute()
   341  			require.NoError(t, err)
   342  		})
   343  		require.NoError(t, err)
   344  		assert.Contains(t, out, "Progressing")
   345  	})
   346  }
   347  
   348  func TestResourceOverrideAction(t *testing.T) {
   349  	f, closer, err := tempFile(testDeploymentYAML)
   350  	require.NoError(t, err)
   351  	defer utilio.Close(closer)
   352  
   353  	cronJobFile, closer, err := tempFile(testCronJobYAML)
   354  	require.NoError(t, err)
   355  	defer utilio.Close(closer)
   356  
   357  	t.Run("NoActions", func(t *testing.T) {
   358  		cmd := NewResourceOverridesCommand(newCmdContext(map[string]string{
   359  			"resource.customizations": `apps/Deployment: {}`,
   360  		}))
   361  		out, err := captureStdout(func() {
   362  			cmd.SetArgs([]string{"run-action", f, "test"})
   363  			err := cmd.Execute()
   364  			require.NoError(t, err)
   365  		})
   366  		require.NoError(t, err)
   367  		assert.Contains(t, out, "Actions are not configured")
   368  	})
   369  
   370  	t.Run("OldStyleActionConfigured", func(t *testing.T) {
   371  		cmd := NewResourceOverridesCommand(newCmdContext(map[string]string{
   372  			"resource.customizations": `apps/Deployment:
   373    actions: |
   374      discovery.lua: |
   375        actions = {}
   376        actions["resume"] = {["disabled"] = false}
   377        actions["restart"] = {["disabled"] = false}
   378        return actions
   379      definitions:
   380      - name: test
   381        action.lua: |
   382          obj.metadata.labels["test"] = 'updated'
   383          return obj
   384  `,
   385  		}))
   386  		out, err := captureStdout(func() {
   387  			cmd.SetArgs([]string{"run-action", f, "test"})
   388  			err := cmd.Execute()
   389  			require.NoError(t, err)
   390  		})
   391  		require.NoError(t, err)
   392  		assert.Contains(t, out, "test: updated")
   393  
   394  		out, err = captureStdout(func() {
   395  			cmd.SetArgs([]string{"list-actions", f})
   396  			err := cmd.Execute()
   397  			require.NoError(t, err)
   398  		})
   399  		require.NoError(t, err)
   400  		assert.Contains(t, out, `NAME     DISABLED
   401  restart  false
   402  resume   false
   403  `)
   404  	})
   405  
   406  	t.Run("NewStyleActionConfigured", func(t *testing.T) {
   407  		cmd := NewResourceOverridesCommand(newCmdContext(map[string]string{
   408  			"resource.customizations": `batch/CronJob:
   409    actions: |
   410      discovery.lua: |
   411        actions = {}
   412        actions["create-a-job"] = {["disabled"] = false}
   413        return actions
   414      definitions:
   415      - name: test
   416        action.lua: |
   417          job1 = {}
   418          job1.apiVersion = "batch/v1"
   419          job1.kind = "Job"
   420          job1.metadata = {}
   421          job1.metadata.name = "hello-1"
   422          job1.metadata.namespace = "obj.metadata.namespace"
   423          impactedResource1 = {}
   424          impactedResource1.operation = "create"
   425          impactedResource1.resource = job1
   426          result = {}
   427          result[1] = impactedResource1
   428          return result
   429  `,
   430  		}))
   431  		out, err := captureStdout(func() {
   432  			cmd.SetArgs([]string{"run-action", cronJobFile, "test"})
   433  			err := cmd.Execute()
   434  			require.NoError(t, err)
   435  		})
   436  		require.NoError(t, err)
   437  		assert.Contains(t, out, "resource was created:")
   438  		assert.Contains(t, out, "hello-1")
   439  
   440  		out, err = captureStdout(func() {
   441  			cmd.SetArgs([]string{"list-actions", cronJobFile})
   442  			err := cmd.Execute()
   443  			require.NoError(t, err)
   444  		})
   445  
   446  		require.NoError(t, err)
   447  		assert.Contains(t, out, "NAME")
   448  		assert.Contains(t, out, "DISABLED")
   449  		assert.Contains(t, out, "create-a-job")
   450  		assert.Contains(t, out, "false")
   451  	})
   452  }