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: Configurable ArgoCD binary download links on Help page. Fixes #7698 #7755

Merged
merged 12 commits into from
Nov 30, 2021
7 changes: 7 additions & 0 deletions assets/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -3570,6 +3570,13 @@
"type": "object",
"title": "Help settings",
"properties": {
"binaryUrls": {
"type": "object",
"title": "the URLs for downloading argocd binaries",
"additionalProperties": {
"type": "string"
}
},
"chatText": {
"type": "string",
"title": "the text for getting chat help, defaults to \"Chat now!\""
Expand Down
5 changes: 5 additions & 0 deletions docs/operator-manual/argocd-cm.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ data:
help.chatUrl: "https://mycorp.slack.com/argo-cd"
# the text for getting chat help, defaults to "Chat now!"
help.chatText: "Chat now!"
# The URLs to download additional ArgoCD binaries (besides the Linux amd64 binary included by default)
# for different OS architectures. If provided, additional download buttons will be displayed on the help page.
help.download.darwin-amd64: test-path1
help.download.darwin-arm64: test-path2
help.download.windows-amd64: test-path3

# A dex connector configuration (optional). See SSO configuration documentation:
# https://github.com/argoproj/argo-cd/blob/master/docs/operator-manual/sso
Expand Down
308 changes: 237 additions & 71 deletions pkg/apiclient/settings/settings.pb.go

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions server/settings/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,9 @@ func (s *Server) Get(ctx context.Context, q *settingspkg.SettingsQuery) (*settin
AnonymizeUsers: gaSettings.AnonymizeUsers,
},
Help: &settingspkg.Help{
ChatUrl: help.ChatURL,
ChatText: help.ChatText,
ChatUrl: help.ChatURL,
ChatText: help.ChatText,
BinaryUrls: help.BinaryURLs,
},
Plugins: plugins,
UserLoginsDisabled: userLoginsDisabled,
Expand Down
2 changes: 2 additions & 0 deletions server/settings/settings.proto
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ message Help {
string chatUrl = 1;
// the text for getting chat help, defaults to "Chat now!"
string chatText = 2;
// the URLs for downloading argocd binaries
map<string, string> binaryUrls = 3;
}

// Plugin settings
Expand Down
114 changes: 79 additions & 35 deletions ui/src/app/help/components/help.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,84 @@
import * as React from 'react';
import {Page} from '../../shared/components';
import {DataLoader, Page} from '../../shared/components';
import {Consumer} from '../../shared/context';
import {combineLatest} from 'rxjs';
import {services} from '../../shared/services';
import {map} from 'rxjs/operators';

require('./help.scss');

export const Help = () => (
<Consumer>
{ctx => (
<Page title='Help'>
<div className='row'>
<div className='columns large-4 small-6'>
<div className='help-box'>
<p>New to Argo CD?</p>
<a className='user-info-panel-buttons argo-button argo-button--base' href='https://argo-cd.readthedocs.io'>
Read the docs
</a>
</div>
</div>
<div className='columns large-4 small-6'>
<div className='help-box'>
<p>Want to download the CLI tool?</p>
<a href={`download/argocd-linux-${process.env.HOST_ARCH}`} className='user-info-panel-buttons argo-button argo-button--base'>
<i className='fab fa-linux ' /> Linux
</a>
</div>
</div>
<div className='columns large-4 small-6'>
<div className='help-box'>
<p>You want to develop against Argo CD's API?</p>
<a className='user-info-panel-buttons argo-button argo-button--base' href='/swagger-ui'>
Open the API docs
</a>
</div>
</div>
</div>
</Page>
)}
</Consumer>
);
export const Help = () => {
return (
<DataLoader
load={() =>
combineLatest([services.authService.settings()]).pipe(
map(items => {
return {
binaryUrls: items[0].help.binaryUrls || {}
};
})
)
}>
{({binaryUrls}: {binaryUrls: Record<string, string>}) => {
return (
<Consumer>
{() => (
<Page title='Help'>
<div className='row'>
<div className='columns large-4 small-6'>
<div className='help-box'>
<p>New to Argo CD?</p>
<a className='user-info-panel-buttons argo-button argo-button--base' href='https://argo-cd.readthedocs.io'>
Read the docs
</a>
</div>
</div>
<div className='columns large-4 small-6'>
<div className='help-box'>
<p>Want to download the CLI tool?</p>
<a href={`download/argocd-linux-${process.env.HOST_ARCH}`} className='user-info-panel-buttons argo-button argo-button--base'>
<i className='fab fa-linux' /> Linux (amd64)
</a>
&nbsp;
{binaryUrls.hasOwnProperty('linux-arm64') && (
<a href={`${binaryUrls['linux-arm64']}`} className='user-info-panel-buttons argo-button argo-button--base'>
<i className='fab fa-linux' /> Linux (arm64)
</a>
)}
&nbsp;
{binaryUrls.hasOwnProperty('darwin-amd64') && (
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Page crashes if binaryUrls is not defined/null. Instead of writing separate if for every permutation you can use following snipped :

import classNames from 'classnames';
...
{Object.keys(binaryUrls || {}).map(binaryName => {
    const url = binaryUrls[binaryName];
    const match = binaryName.match(/.*(darwin|windows|linux)-(amd64|arm64)/);
    const [platform, arch] = match ? match.slice(1) : ['', ''];
    return (
        <>
            &nbsp;
            <a key={binaryName} href={url} className='user-info-panel-buttons argo-button argo-button--base'>
                <i
                    className={classNames('fab', {
                        'fa-windows': platform === 'windows',
                        'fa-apple': platform === 'darwin',
                        'fa-linux': platform === 'linux'
                    })}
                />{' '}
                MacOS {arch && `( ${arch} )`}
            </a>
        </>
    );
})}

Copy link
Member Author

@terrytangyuan terrytangyuan Nov 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried refactoring this in 5e93d00 but got stuck with some additional issues.

Though I was able to fix the page crushing issue very easily. Would you mind leaving the refactoring of typescript code later as I have zero experience and don't have much bandwidth to debug further currently?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for long response. Thanks for fixing the crash. I think it is ok to keep it as is - refactoring is optional.

<a href={`${binaryUrls['darwin-amd64']}`} className='user-info-panel-buttons argo-button argo-button--base'>
terrytangyuan marked this conversation as resolved.
Show resolved Hide resolved
<i className='fab fa-apple' /> MacOS (amd64)
</a>
)}
&nbsp;
{binaryUrls.hasOwnProperty('darwin-arm64') && (
<a href={`${binaryUrls['darwin-arm64']}`} className='user-info-panel-buttons argo-button argo-button--base'>
<i className='fab fa-apple' /> MacOS (arm64)
</a>
)}
&nbsp;
{binaryUrls.hasOwnProperty('windows-amd64') && (
<a href={`${binaryUrls['windows-amd64']}`} className='user-info-panel-buttons argo-button argo-button--base'>
<i className='fab fa-windows' /> Windows
</a>
)}
</div>
</div>
<div className='columns large-4 small-6'>
<div className='help-box'>
<p>You want to develop against Argo CD's API?</p>
<a className='user-info-panel-buttons argo-button argo-button--base' href='/swagger-ui'>
Open the API docs
</a>
</div>
</div>
</div>
</Page>
)}
</Consumer>
);
}}
</DataLoader>
);
};
1 change: 1 addition & 0 deletions ui/src/app/shared/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,7 @@ export interface AuthSettings {
help: {
chatUrl: string;
chatText: string;
binaryUrls: Record<string, string>;
};
plugins: Plugin[];
userLoginsDisabled: boolean;
Expand Down
22 changes: 20 additions & 2 deletions util/settings/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ type ArgoCDSettings struct {
UiBannerPosition string `json:"uiBannerPosition,omitempty"`
// PasswordPattern for password regular expression
PasswordPattern string `json:"passwordPattern,omitempty"`
// BinaryUrls contains the URLs for downloading argocd binaries
BinaryUrls map[string]string `json:"binaryUrls,omitempty"`
}

type GoogleAnalytics struct {
Expand All @@ -106,6 +108,8 @@ type Help struct {
ChatURL string `json:"chatUrl,omitempty"`
// the text for getting chat help, defaults to "Chat now!"
ChatText string `json:"chatText,omitempty"`
// the URLs for downloading argocd binaries
BinaryURLs map[string]string `json:"binaryUrl,omitempty"`
}

type OIDCConfig struct {
Expand Down Expand Up @@ -309,6 +313,8 @@ const (
settingUiBannerPermanentKey = "ui.bannerpermanent"
// settingUiBannerPositionKey designates the key for the position of the banner
settingUiBannerPositionKey = "ui.bannerposition"
// settingsBinaryUrlsKey designates the key for the argocd binary URLs
settingsBinaryUrlsKey = "help.download"
// globalProjectsKey designates the key for global project settings
globalProjectsKey = "globalProjects"
// initialPasswordSecretName is the name of the secret that will hold the initial admin password
Expand Down Expand Up @@ -925,8 +931,9 @@ func (mgr *SettingsManager) GetHelp() (*Help, error) {
chatText = "Chat now!"
}
return &Help{
ChatURL: argoCDCM.Data[helpChatURL],
ChatText: chatText,
ChatURL: argoCDCM.Data[helpChatURL],
ChatText: chatText,
BinaryURLs: getDownloadBinaryUrlsFromConfigMap(argoCDCM),
}, nil
}

Expand Down Expand Up @@ -1061,6 +1068,16 @@ func (mgr *SettingsManager) ensureSynced(forceResync bool) error {
return mgr.initialize(ctx)
}

func getDownloadBinaryUrlsFromConfigMap(argoCDCM *apiv1.ConfigMap) map[string]string {
binaryUrls := map[string]string{}
for _, archType := range []string{"darwin-amd64", "darwin-arm64", "windows-amd64", "linux-arm64", "linux-amd64"} {
if val, ok := argoCDCM.Data[settingsBinaryUrlsKey+"."+archType]; ok {
binaryUrls[archType] = val
}
}
return binaryUrls
}

// updateSettingsFromConfigMap transfers settings from a Kubernetes configmap into an ArgoCDSettings struct.
func updateSettingsFromConfigMap(settings *ArgoCDSettings, argoCDCM *apiv1.ConfigMap) {
settings.DexConfig = argoCDCM.Data[settingDexConfigKey]
Expand All @@ -1072,6 +1089,7 @@ func updateSettingsFromConfigMap(settings *ArgoCDSettings, argoCDCM *apiv1.Confi
settings.UiBannerContent = argoCDCM.Data[settingUiBannerContentKey]
settings.UiBannerPermanent = argoCDCM.Data[settingUiBannerPermanentKey] == "true"
settings.UiBannerPosition = argoCDCM.Data[settingUiBannerPositionKey]
settings.BinaryUrls = getDownloadBinaryUrlsFromConfigMap(argoCDCM)
if err := validateExternalURL(argoCDCM.Data[settingURLKey]); err != nil {
log.Warnf("Failed to validate URL in configmap: %v", err)
}
Expand Down
25 changes: 25 additions & 0 deletions util/settings/settings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,15 @@ func TestSettingsManager_GetHelp(t *testing.T) {
assert.Equal(t, "foo", h.ChatURL)
assert.Equal(t, "bar", h.ChatText)
})
t.Run("GetBinaryUrls", func(t *testing.T) {
_, settingsManager := fixtures(map[string]string{
"help.download.darwin-amd64": "amd64-path",
"help.download.unsupported": "nowhere",
})
h, err := settingsManager.GetHelp()
assert.NoError(t, err)
assert.Equal(t, map[string]string{"darwin-amd64": "amd64-path"}, h.BinaryURLs)
})
}

func TestGetOIDCConfig(t *testing.T) {
Expand Down Expand Up @@ -895,6 +904,22 @@ func Test_GetTLSConfiguration(t *testing.T) {
})
}

func TestDownloadArgoCDBinaryUrls(t *testing.T) {
_, settingsManager := fixtures(map[string]string{
"help.download.darwin-amd64": "some-url",
})
argoCDCM, err := settingsManager.getConfigMap()
assert.NoError(t, err)
assert.Equal(t, "some-url", argoCDCM.Data["help.download.darwin-amd64"])

_, settingsManager = fixtures(map[string]string{
"help.download.unsupported": "some-url",
})
argoCDCM, err = settingsManager.getConfigMap()
assert.NoError(t, err)
assert.Equal(t, "some-url", argoCDCM.Data["help.download.unsupported"])
}

func TestSecretKeyRef(t *testing.T) {
data := map[string]string{
"oidc.config": `name: Okta
Expand Down