diff --git a/src/frontend/packages/cloud-foundry/src/features/applications/deploy-application/deploy-application-deployer.ts b/src/frontend/packages/cloud-foundry/src/features/applications/deploy-application/deploy-application-deployer.ts index df2a5062c3..6de6b8a927 100644 --- a/src/frontend/packages/cloud-foundry/src/features/applications/deploy-application/deploy-application-deployer.ts +++ b/src/frontend/packages/cloud-foundry/src/features/applications/deploy-application/deploy-application-deployer.ts @@ -45,6 +45,7 @@ interface GitSCMSourceInfo extends DeploySource { commit: string; scm: string; endpointGuid: string; + accessToken?: string; } // Structure used to provide metadata about the Git Url source @@ -253,7 +254,8 @@ export class DeployApplicationDeployer { commit: appSource.gitDetails.commit, url: appSource.gitDetails.url, scm: appSource.type.id, - endpointGuid: appSource.gitDetails.endpointGuid + endpointGuid: appSource.gitDetails.endpointGuid, + accessToken: appSource.gitDetails.accessToken }; const msg = { diff --git a/src/frontend/packages/cloud-foundry/src/features/applications/deploy-application/deploy-application-step2/deploy-application-step2.component.html b/src/frontend/packages/cloud-foundry/src/features/applications/deploy-application/deploy-application-step2/deploy-application-step2.component.html index 9879d583f3..de3eb1fbbd 100644 --- a/src/frontend/packages/cloud-foundry/src/features/applications/deploy-application/deploy-application-step2/deploy-application-step2.component.html +++ b/src/frontend/packages/cloud-foundry/src/features/applications/deploy-application/deploy-application-step2/deploy-application-step2.component.html @@ -11,10 +11,22 @@
+ + + + + GitHub Enterprise deployment url is not valid + + + + + [appGithubProjectExists]="sourceType.id + ',' + sourceType.endpointGuid + ',' + (accessToken || '')" required> diff --git a/src/frontend/packages/cloud-foundry/src/features/applications/deploy-application/deploy-application-step2/deploy-application-step2.component.ts b/src/frontend/packages/cloud-foundry/src/features/applications/deploy-application/deploy-application-step2/deploy-application-step2.component.ts index 6a3f9d958c..7c98222741 100644 --- a/src/frontend/packages/cloud-foundry/src/features/applications/deploy-application/deploy-application-step2/deploy-application-step2.component.ts +++ b/src/frontend/packages/cloud-foundry/src/features/applications/deploy-application/deploy-application-step2/deploy-application-step2.component.ts @@ -50,6 +50,8 @@ import { getCommitGuid } from '../../../../../../git/src/store/git-entity-factor import { DeployApplicationState, SourceType } from '../../../../store/types/deploy-application.types'; import { ApplicationDeploySourceTypes, DEPLOY_TYPES_IDS } from '../deploy-application-steps.types'; import { GitSuggestedRepo } from './../../../../../../git/src/store/git.public-types'; +import { GitHubSCM } from 'frontend/packages/git/src/shared/scm/github-scm'; +import { BaseSCM } from 'frontend/packages/git/src/shared/scm/scm-base'; @@ -98,6 +100,11 @@ export class DeployApplicationStep2Component // We don't have any repositories to suggest initially - need user to start typing suggestedRepos$: Observable; + // GitHub Enterprise/private repos + isInvalidGithubEnterpriseUrl: boolean; + accessToken: string; + // -------------- + // Git URL gitUrl: string; gitUrlBranchName: string; @@ -141,6 +148,7 @@ export class DeployApplicationStep2Component projectName: this.repository, branch: this.repositoryBranch, url: repo.entity.clone_url, + accessToken: this.accessToken, commit: this.isRedeploy ? this.commitInfo.sha : undefined, endpointGuid: this.sourceType.endpointGuid, }, null)); @@ -345,6 +353,21 @@ export class DeployApplicationStep2Component this.subscriptions.push(setProjectName.subscribe()); this.suggestedRepos$ = this.sourceSelectionForm.valueChanges.pipe( + tap(form => { + const isValidUrl = (input: string) => { try { var url = new URL(input); return Boolean(url) } catch (e) { return false } } + + this.isInvalidGithubEnterpriseUrl = form.githubEnterpriseUrl && !isValidUrl(form.githubEnterpriseUrl) + + if (form.githubEnterpriseUrl && !this.isInvalidGithubEnterpriseUrl) { + (this.scm as unknown as BaseSCM).setPublicApi(form.githubEnterpriseUrl) + } + + if (form.githubAccessToken) { + (this.scm as GitHubSCM).setAccessToken(form.githubAccessToken) + } else { + (this.scm as GitHubSCM).clearAccessToken() + } + }), map(form => form.projectName), startWith(''), pairwise(), diff --git a/src/frontend/packages/cloud-foundry/src/features/applications/deploy-application/deploy-application-steps.types.ts b/src/frontend/packages/cloud-foundry/src/features/applications/deploy-application/deploy-application-steps.types.ts index b92c91eb93..d00d4474ff 100644 --- a/src/frontend/packages/cloud-foundry/src/features/applications/deploy-application/deploy-application-steps.types.ts +++ b/src/frontend/packages/cloud-foundry/src/features/applications/deploy-application/deploy-application-steps.types.ts @@ -34,7 +34,7 @@ export class ApplicationDeploySourceTypes { name: 'GitHub', id: DEPLOY_TYPES_IDS.GITHUB, group: 'gitscm', - helpText: 'Please select the GitHub project and branch you would like to deploy from.', + helpText: 'Please select the GitHub project and branch you would like to deploy from. If the GitHub repository is private or located on a GitHub Enterprise deployment, include an access token (and the GitHub Enterprise deployment url).', graphic: { // TODO: Move cf assets to CF package (#3769) location: '/core/assets/endpoint-icons/github-logo.png', diff --git a/src/frontend/packages/cloud-foundry/src/features/applications/deploy-application/github-project-exists.directive.ts b/src/frontend/packages/cloud-foundry/src/features/applications/deploy-application/github-project-exists.directive.ts index 805731dc45..9d1333674a 100644 --- a/src/frontend/packages/cloud-foundry/src/features/applications/deploy-application/github-project-exists.directive.ts +++ b/src/frontend/packages/cloud-foundry/src/features/applications/deploy-application/github-project-exists.directive.ts @@ -41,12 +41,12 @@ export class GithubProjectExistsDirective implements Validator { return this.lastValue.length && this.lastValue.indexOf(name) === 0; } - private getTypeAndEndpoint(): [GitSCMType, string] { + private getTypeAndEndpointWithAuth(): [GitSCMType, string, string] { const res = this.appGithubProjectExists.split(','); - if (res.length === 2) { - return [res[0] as GitSCMType, res[1]]; + if (res.length === 3) { + return [res[0] as GitSCMType, res[1], res[2]]; } - console.warn('appGithubProjectExists value should be `,'); + console.warn('appGithubProjectExists value should be `,,`'); return null; } @@ -64,7 +64,7 @@ export class GithubProjectExistsDirective implements Validator { debounceTime(250), tap(createAppState => { if (createAppState.projectExists && createAppState.projectExists.name !== c.value) { - this.store.dispatch(new CheckProjectExists(this.scmService.getSCM(...this.getTypeAndEndpoint()), c.value)); + this.store.dispatch(new CheckProjectExists(this.scmService.getSCM(...this.getTypeAndEndpointWithAuth()), c.value)); } }), filter(createAppState => diff --git a/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/github-commits/github-commits-list-config-deploy.service.ts b/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/github-commits/github-commits-list-config-deploy.service.ts index 11b0179daf..e9ff310ecd 100644 --- a/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/github-commits/github-commits-list-config-deploy.service.ts +++ b/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/github-commits/github-commits-list-config-deploy.service.ts @@ -33,6 +33,7 @@ export class GithubCommitsListConfigServiceDeploy extends GithubCommitsListConfi map((appSource: DeployApplicationSource) => { return (appSource.type.id === 'github' || appSource.type.id === 'gitlab') ? { scm: appSource.type.id as GitSCMType, + accessToken: appSource.gitDetails.accessToken, projectName: appSource.gitDetails.projectName, sha: appSource.gitDetails.branch.name, endpointGuid: appSource.gitDetails.endpointGuid @@ -41,7 +42,7 @@ export class GithubCommitsListConfigServiceDeploy extends GithubCommitsListConfi filter(fetchDetails => !!fetchDetails && !!fetchDetails.projectName && !!fetchDetails.sha), first() ).subscribe(fetchDetails => { - const scm = scmService.getSCM(fetchDetails.scm, fetchDetails.endpointGuid); + const scm = scmService.getSCM(fetchDetails.scm, fetchDetails.endpointGuid, fetchDetails.accessToken); this.dataSource = new GithubCommitsDataSource(this.store, this, scm, fetchDetails.projectName, fetchDetails.sha); this.initialised.next(true); diff --git a/src/frontend/packages/cloud-foundry/src/store/types/deploy-application.types.ts b/src/frontend/packages/cloud-foundry/src/store/types/deploy-application.types.ts index 6b673f0bf0..d3cefce3ee 100644 --- a/src/frontend/packages/cloud-foundry/src/store/types/deploy-application.types.ts +++ b/src/frontend/packages/cloud-foundry/src/store/types/deploy-application.types.ts @@ -71,6 +71,7 @@ export interface GitAppDetails { projectName: string; branch: GitBranch; endpointGuid: string; + accessToken?: string; commit?: string; branchName?: string; url?: string; diff --git a/src/frontend/packages/git/src/shared/scm/github-scm.ts b/src/frontend/packages/git/src/shared/scm/github-scm.ts index babe37ae63..d3f28588e0 100644 --- a/src/frontend/packages/git/src/shared/scm/github-scm.ts +++ b/src/frontend/packages/git/src/shared/scm/github-scm.ts @@ -1,7 +1,7 @@ import { HttpClient } from '@angular/common/http'; import { flattenPagination } from '@stratosui/store'; import { Observable } from 'rxjs'; -import { map, switchMap } from 'rxjs/operators'; +import { map, tap, switchMap } from 'rxjs/operators'; import { GitBranch, GitCommit, GitRepo } from '../../store/git.public-types'; import { GitSuggestedRepo } from './../../store/git.public-types'; @@ -14,12 +14,29 @@ import { import { GitSCM, SCMIcon } from './scm'; import { BaseSCM, GitApiRequest } from './scm-base'; import { GitSCMType } from './scm.service'; +import { HttpOptions } from 'frontend/packages/core/src/core/core.types'; export class GitHubSCM extends BaseSCM implements GitSCM { - constructor(gitHubURL: string, endpointGuid: string) { + private options: HttpOptions; + + constructor(gitHubURL: string, endpointGuid: string, accessToken?: string) { super(gitHubURL); this.endpointGuid = endpointGuid; + if (accessToken && accessToken.trim() != "") { + this.setAccessToken(accessToken); + } + } + + setAccessToken(input: string) { + this.options = new HttpOptions(); + this.options.headers = {"Authorization": `Bearer ${input}`}; + } + + clearAccessToken() { + if (this.options) { + this.options.headers = {}; + } } getType(): GitSCMType { @@ -38,19 +55,20 @@ export class GitHubSCM extends BaseSCM implements GitSCM { } getRepository(httpClient: HttpClient, projectName: string): Observable { - return this.getAPI().pipe( - switchMap(api => httpClient.get(`${api.url}/repos/${projectName}`, api.requestArgs)) + return this.getAPI(this.options).pipe( + switchMap(api => { + return httpClient.get(`${api.url}/repos/${projectName}`, api.requestArgs) + }) ); } getBranch(httpClient: HttpClient, projectName: string, branchName: string): Observable { - return this.getAPI().pipe( + return this.getAPI(this.options).pipe( switchMap(api => httpClient.get(`${api.url}/repos/${projectName}/branches/${branchName}`, api.requestArgs)) ); } - getBranches(httpClient: HttpClient, projectName: string): Observable { - return this.getAPI().pipe( + return this.getAPI(this.options).pipe( switchMap(api => { const url = `${api.url}/repos/${projectName}/branches`; const config = new GithubFlattenerForArrayPaginationConfig(httpClient, url, api.requestArgs); @@ -71,7 +89,7 @@ export class GitHubSCM extends BaseSCM implements GitSCM { } getCommitApi(projectName: string, commitSha: string): Observable { - return this.getAPI().pipe( + return this.getAPI(this.options).pipe( map(api => ({ ...api, url: `${api.url}/repos/${projectName}/commits/${commitSha}`, @@ -80,7 +98,7 @@ export class GitHubSCM extends BaseSCM implements GitSCM { } getCommits(httpClient: HttpClient, projectName: string, ref: string): Observable { - return this.getAPI().pipe( + return this.getAPI(this.options).pipe( switchMap(api => httpClient.get( `${api.url}/repos/${projectName}/commits?sha=${ref}`, { ...api.requestArgs, @@ -98,7 +116,7 @@ export class GitHubSCM extends BaseSCM implements GitSCM { } getMatchingRepositories(httpClient: HttpClient, projectName: string): Observable { - return this.getAPI().pipe( + return this.getAPI(this.options).pipe( switchMap(api => { const prjParts = projectName.split('/'); let url = `${api.url}/search/repositories?q=${projectName}+in:name+fork:true`; diff --git a/src/frontend/packages/git/src/shared/scm/scm-base.ts b/src/frontend/packages/git/src/shared/scm/scm-base.ts index f15b77d3d1..21d93481f7 100644 --- a/src/frontend/packages/git/src/shared/scm/scm-base.ts +++ b/src/frontend/packages/git/src/shared/scm/scm-base.ts @@ -21,25 +21,29 @@ export abstract class BaseSCM { constructor(public publicApiUrl: string) { } + public setPublicApi(url: string) { + this.publicApiUrl = url + } + public getPublicApi(): string { return this.publicApiUrl; } - public getAPI(): Observable { + public getAPI(options: HttpOptions = new HttpOptions()): Observable { return this.getEndpoint(this.endpointGuid).pipe( map(endpoint => { if (!endpoint) { // No endpoint, use the default or overwritten public api associated with this type return { url: this.getPublicApi(), - requestArgs: {} + requestArgs: options }; } // We have an endpoint so always proxy via backend return { url: `${commonPrefix}/${endpoint.guid}`, requestArgs: { - ... new HttpOptions(), + ... options, headers: { 'x-cap-no-token': `${!endpoint.user}` } diff --git a/src/frontend/packages/git/src/shared/scm/scm.service.ts b/src/frontend/packages/git/src/shared/scm/scm.service.ts index 9018084ff4..c8a916b89d 100644 --- a/src/frontend/packages/git/src/shared/scm/scm.service.ts +++ b/src/frontend/packages/git/src/shared/scm/scm.service.ts @@ -17,10 +17,10 @@ export class GitSCMService { ) { } - public getSCM(type: GitSCMType, endpointGuid: string): GitSCM { + public getSCM(type: GitSCMType, endpointGuid: string, accessToken?: string): GitSCM { switch (type) { case 'github': - return new GitHubSCM(this.gitHubURL, endpointGuid); + return new GitHubSCM(this.gitHubURL, endpointGuid, accessToken); case 'gitlab': return new GitLabSCM(endpointGuid); } diff --git a/src/jetstream/plugins/cfapppush/deploy.go b/src/jetstream/plugins/cfapppush/deploy.go index d5081279b1..4382311771 100644 --- a/src/jetstream/plugins/cfapppush/deploy.go +++ b/src/jetstream/plugins/cfapppush/deploy.go @@ -454,11 +454,12 @@ func (cfAppPush *CFAppPush) getGitSCMSource(clientWebSocket *websocket.Conn, tem log.Debugf("GitSCM SCM: %s, Source: %s, branch %s, url: %s", info.SCM, info.Project, info.Branch, loggerURL) cloneDetails := CloneDetails{ - Url: cloneURL, - LoggerUrl: loggerURL, - Branch: info.Branch, - Commit: info.CommitHash, - SkipSSL: skipSSL, + Url: cloneURL, + LoggerUrl: loggerURL, + Branch: info.Branch, + Commit: info.CommitHash, + SkipSSL: skipSSL, + AccessToken: info.AcccessToken, } info.CommitHash, err = cloneRepository(cloneDetails, clientWebSocket, tempDir) if err != nil { @@ -625,6 +626,10 @@ func cloneRepository(cloneDetails CloneDetails, clientWebSocket *websocket.Conn, vcsGit := GetVCS() + if len(cloneDetails.AccessToken) > 0 { + vcsGit = GetVCS(withAccessToken(cloneDetails.AccessToken)) + } + err := vcsGit.Create(cloneDetails.SkipSSL, tempDir, cloneDetails.Url, cloneDetails.Branch) if err != nil { log.Infof("Failed to clone repo %s due to %+v", cloneDetails.LoggerUrl, err) diff --git a/src/jetstream/plugins/cfapppush/types.go b/src/jetstream/plugins/cfapppush/types.go index 95969d4765..d555c93e9d 100644 --- a/src/jetstream/plugins/cfapppush/types.go +++ b/src/jetstream/plugins/cfapppush/types.go @@ -47,6 +47,7 @@ type GitSCMSourceInfo struct { SCM string `json:"scm"` EndpointGUID string `json:"endpointGuid"` // credentials of which to use, e.g. of a private GitHub instance Username string `json:"username"` // GitLab username has to be supplied by the frontend + AcccessToken string `json:"accessToken"` // GitHub private repos/enterprise repos can supply a token through the frontend } // Structure used to provide metadata about the Git Url source @@ -117,9 +118,10 @@ type Applications struct { } type CloneDetails struct { - Url string - LoggerUrl string - Branch string - Commit string - SkipSSL bool + Url string + LoggerUrl string + Branch string + Commit string + SkipSSL bool + AccessToken string } diff --git a/src/jetstream/plugins/cfapppush/vcs.go b/src/jetstream/plugins/cfapppush/vcs.go index 5e85384688..2c8258b070 100644 --- a/src/jetstream/plugins/cfapppush/vcs.go +++ b/src/jetstream/plugins/cfapppush/vcs.go @@ -4,6 +4,8 @@ package cfapppush import ( "bytes" + "fmt" + "net/url" "os" "os/exec" "strconv" @@ -15,20 +17,36 @@ import ( var vcsGit = &vcsCmd{ name: "Git", cmd: "git", + accessToken: "", createCmd: []string{"clone -c http.sslVerify={sslVerify} -b {branch} {repo} {dir} "}, resetToCommitCmd: []string{"reset --hard {commit}"}, checkoutCmd: []string{"checkout refs/remotes/origin/{branch}"}, headCmd: []string{"rev-parse HEAD"}, } +type vcsOptions func(*vcsCmd) + // Currently only git is supported -func GetVCS() *vcsCmd { +func GetVCS(opts ...vcsOptions) *vcsCmd { + vcsGit := &(*vcsGit) + + for _, opt := range opts { + opt(vcsGit) + } + return vcsGit } +func withAccessToken(accessToken string) vcsOptions { + return func(vc *vcsCmd) { + vc.accessToken = accessToken + } +} + type vcsCmd struct { - name string - cmd string // name of binary to invoke command + name string + cmd string // name of binary to invoke command + accessToken string // optional, if empty do not use it createCmd []string // commands to download a fresh copy of a repository checkoutCmd []string // commands to checkout a branch @@ -37,8 +55,18 @@ type vcsCmd struct { } func (vcs *vcsCmd) Create(skipSSL bool, dir string, repo string, branch string) error { + repoUrl, err := url.Parse(repo) + + if err != nil { + return fmt.Errorf("could not execute vcs create: %w", err) + } + + if len(vcs.accessToken) > 0 { + repoUrl.User = url.UserPassword("x-access-token", vcs.accessToken) + } + for _, cmd := range vcs.createCmd { - if err := vcs.run(".", cmd, "sslVerify", strconv.FormatBool(!skipSSL), "dir", dir, "repo", repo, "branch", branch); err != nil { + if err := vcs.run(".", cmd, "sslVerify", strconv.FormatBool(!skipSSL), "dir", dir, "repo", repoUrl.String(), "branch", branch); err != nil { return err } }