Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow individual extension configs (#20491) #20525

Merged
merged 1 commit into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion docs/developer-guide/extensions/proxy-extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,38 @@ data:
server: https://some-cluster
```

Note: There is no need to restart Argo CD Server after modifiying the
Proxy extensions can also be provided individually using dedicated
Argo CD configmap keys for better GitOps operations. The example below
demonstrates how to configure the same hypothetical httpbin config
above using a dedicated key:

```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-cm
namespace: argocd
data:
extension.config.httpbin: |
connectionTimeout: 2s
keepAlive: 15s
idleConnectionTimeout: 60s
maxIdleConnections: 30
services:
- url: http://httpbin.org
headers:
- name: some-header
value: '$some.argocd.secret.key'
cluster:
name: some-cluster
server: https://some-cluster
```

Attention: Extension names must be unique in the Argo CD configmap. If
duplicated keys are found, the Argo CD API server will log an error
message and no proxy extension will be registered.

Note: There is no need to restart Argo CD Server after modifying the
`extension.config` entry in Argo CD configmap. Changes will be
automatically applied. A new proxy registry will be built making
all new incoming extensions requests (`<argocd-host>/extensions/*`) to
Expand Down
54 changes: 36 additions & 18 deletions server/extension/extension.go
Original file line number Diff line number Diff line change
Expand Up @@ -410,28 +410,46 @@ func proxyKey(extName, cName, cServer string) ProxyKey {
}

func parseAndValidateConfig(s *settings.ArgoCDSettings) (*ExtensionConfigs, error) {
if s.ExtensionConfig == "" {
if len(s.ExtensionConfig) == 0 {
return nil, fmt.Errorf("no extensions configurations found")
}

extConfigMap := map[string]interface{}{}
err := yaml.Unmarshal([]byte(s.ExtensionConfig), &extConfigMap)
if err != nil {
return nil, fmt.Errorf("invalid extension config: %w", err)
}

parsedExtConfig := settings.ReplaceMapSecrets(extConfigMap, s.Secrets)
parsedExtConfigBytes, err := yaml.Marshal(parsedExtConfig)
if err != nil {
return nil, fmt.Errorf("error marshaling parsed extension config: %w", err)
}

configs := ExtensionConfigs{}
err = yaml.Unmarshal(parsedExtConfigBytes, &configs)
if err != nil {
return nil, fmt.Errorf("invalid parsed extension config: %w", err)
for extName, extConfig := range s.ExtensionConfig {
extConfigMap := map[string]interface{}{}
err := yaml.Unmarshal([]byte(extConfig), &extConfigMap)
if err != nil {
return nil, fmt.Errorf("invalid extension config: %w", err)
}

parsedExtConfig := settings.ReplaceMapSecrets(extConfigMap, s.Secrets)
parsedExtConfigBytes, err := yaml.Marshal(parsedExtConfig)
if err != nil {
return nil, fmt.Errorf("error marshaling parsed extension config: %w", err)
}
// empty extName means that this is the main configuration defined by
// the 'extension.config' configmap key
if extName == "" {
mainConfig := ExtensionConfigs{}
err = yaml.Unmarshal(parsedExtConfigBytes, &mainConfig)
if err != nil {
return nil, fmt.Errorf("invalid parsed extension config: %w", err)
}
configs.Extensions = append(configs.Extensions, mainConfig.Extensions...)
} else {
backendConfig := BackendConfig{}
err = yaml.Unmarshal(parsedExtConfigBytes, &backendConfig)
if err != nil {
return nil, fmt.Errorf("invalid parsed backend extension config for extension %s: %w", extName, err)
}
ext := ExtensionConfig{
Name: extName,
Backend: backendConfig,
}
configs.Extensions = append(configs.Extensions, ext)
}
}
err = validateConfigs(&configs)
err := validateConfigs(&configs)
if err != nil {
return nil, fmt.Errorf("validation error: %w", err)
}
Expand Down Expand Up @@ -546,7 +564,7 @@ func (m *Manager) RegisterExtensions() error {
if err != nil {
return fmt.Errorf("error getting settings: %w", err)
}
if settings.ExtensionConfig == "" {
if len(settings.ExtensionConfig) == 0 {
m.log.Infof("No extensions configured.")
return nil
}
Expand Down
27 changes: 23 additions & 4 deletions server/extension/extension_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,12 +162,16 @@ func TestRegisterExtensions(t *testing.T) {
t.Parallel()
f := setup()
settings := &settings.ArgoCDSettings{
ExtensionConfig: getExtensionConfigString(),
ExtensionConfig: map[string]string{
"": getExtensionConfigString(),
"another-ext": getSingleExtensionConfigString(),
},
}
f.settingsGetterMock.On("Get", mock.Anything).Return(settings, nil)
expectedProxyRegistries := []string{
"external-backend",
"some-backend",
"another-ext",
}

// when
Expand Down Expand Up @@ -223,7 +227,9 @@ func TestRegisterExtensions(t *testing.T) {
t.Parallel()
f := setup()
settings := &settings.ArgoCDSettings{
ExtensionConfig: tc.configYaml,
ExtensionConfig: map[string]string{
"": tc.configYaml,
},
}
f.settingsGetterMock.On("Get", mock.Anything).Return(settings, nil)

Expand Down Expand Up @@ -362,8 +368,10 @@ func TestCallExtension(t *testing.T) {
secrets["extension.auth.header2"] = "Bearer another-bearer-token"

settings := &settings.ArgoCDSettings{
ExtensionConfig: configYaml,
Secrets: secrets,
ExtensionConfig: map[string]string{
"": configYaml,
},
Secrets: secrets,
}
f.settingsGetterMock.On("Get", mock.Anything).Return(settings, nil)
}
Expand Down Expand Up @@ -796,6 +804,17 @@ extensions:
`
}

func getSingleExtensionConfigString() string {
return `
connectionTimeout: 10s
keepAlive: 11s
idleConnectionTimeout: 12s
maxIdleConnections: 30
services:
- url: http://localhost:7777
`
}

func getExtensionConfigNoService() string {
return `
extensions:
Expand Down
2 changes: 1 addition & 1 deletion server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -705,7 +705,7 @@ func (a *ArgoCDServer) watchSettings() {
log.Infof("gogs secret modified. restarting")
break
}
if prevExtConfig != a.settings.ExtensionConfig {
if !reflect.DeepEqual(prevExtConfig, a.settings.ExtensionConfig) {
prevExtConfig = a.settings.ExtensionConfig
log.Infof("extensions configs modified. Updating proxy registry...")
err := a.extensionManager.UpdateExtensionRegistry(a.settings)
Expand Down
19 changes: 15 additions & 4 deletions util/settings/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,9 @@ type ArgoCDSettings struct {
OIDCTLSInsecureSkipVerify bool `json:"oidcTLSInsecureSkipVerify"`
// AppsInAnyNamespaceEnabled indicates whether applications are allowed to be created in any namespace
AppsInAnyNamespaceEnabled bool `json:"appsInAnyNamespaceEnabled"`
// ExtensionConfig configurations related to ArgoCD proxy extensions. The value
// is a yaml string defined in extension.ExtensionConfigs struct.
ExtensionConfig string `json:"extensionConfig,omitempty"`
// ExtensionConfig configurations related to ArgoCD proxy extensions. The keys are the extension name.
// The value is a yaml string defined in extension.ExtensionConfigs struct.
ExtensionConfig map[string]string `json:"extensionConfig,omitempty"`
// ImpersonationEnabled indicates whether Application sync privileges can be decoupled from control plane
// privileges using impersonation
ImpersonationEnabled bool `json:"impersonationEnabled"`
Expand Down Expand Up @@ -1537,10 +1537,21 @@ func updateSettingsFromConfigMap(settings *ArgoCDSettings, argoCDCM *apiv1.Confi
}
settings.TrackingMethod = argoCDCM.Data[settingsResourceTrackingMethodKey]
settings.OIDCTLSInsecureSkipVerify = argoCDCM.Data[oidcTLSInsecureSkipVerifyKey] == "true"
settings.ExtensionConfig = argoCDCM.Data[extensionConfig]
settings.ExtensionConfig = getExtensionConfigs(argoCDCM.Data)
settings.ImpersonationEnabled = argoCDCM.Data[impersonationEnabledKey] == "true"
}

func getExtensionConfigs(cmData map[string]string) map[string]string {
result := make(map[string]string)
for k, v := range cmData {
if strings.HasPrefix(k, extensionConfig) {
extName := strings.TrimPrefix(strings.TrimPrefix(k, extensionConfig), ".")
result[extName] = v
}
}
return result
}

// validateExternalURL ensures the external URL that is set on the configmap is valid
func validateExternalURL(u string) error {
if u == "" {
Expand Down
46 changes: 46 additions & 0 deletions util/settings/settings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,52 @@ func TestGetRepositories(t *testing.T) {
assert.Equal(t, []Repository{{URL: "http://foo"}}, filter)
}

func TestGetExtensionConfigs(t *testing.T) {
type cases struct {
name string
input map[string]string
expected map[string]string
expectedLen int
}

testCases := []cases{
{
name: "will return main config successfully",
expectedLen: 1,
input: map[string]string{
extensionConfig: "test",
},
expected: map[string]string{
"": "test",
},
},
{
name: "will return main and additional config successfully",
expectedLen: 2,
input: map[string]string{
extensionConfig: "main config",
fmt.Sprintf("%s.anotherExtension", extensionConfig): "another config",
},
expected: map[string]string{
"": "main config",
"anotherExtension": "another config",
},
},
}

for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
// When
output := getExtensionConfigs(tc.input)

// Then
assert.Len(t, output, tc.expectedLen)
assert.Equal(t, tc.expected, output)
})
}
}

func TestSaveRepositories(t *testing.T) {
kubeClient, settingsManager := fixtures(nil)
err := settingsManager.SaveRepositories([]Repository{{URL: "http://foo"}})
Expand Down
Loading