From f524bbdea2f6fbfa47cd01a594459081132572bf Mon Sep 17 00:00:00 2001 From: danztran Date: Sun, 28 Jun 2020 22:24:47 +0700 Subject: [PATCH 01/27] plugin/auth-backend: add gitlab oauth --- packages/app/src/apis.ts | 11 ++ .../core-api/src/apis/definitions/auth.ts | 23 ++- .../auth/gitlab/GitlabAuth.test.ts | 46 +++++ .../implementations/auth/gitlab/GitlabAuth.ts | 135 ++++++++++++++ .../apis/implementations/auth/gitlab/index.ts | 18 ++ .../apis/implementations/auth/gitlab/types.ts | 27 +++ .../src/apis/implementations/auth/index.ts | 1 + .../core/src/layout/Sidebar/UserSettings.tsx | 6 + packages/dev-utils/src/devApp/apiFactories.ts | 13 ++ packages/storybook/.storybook/apis.js | 10 ++ plugins/auth-backend/package.json | 1 + .../src/lib/PassportStrategyHelper.ts | 4 +- .../auth-backend/src/providers/factories.ts | 2 + .../src/providers/gitlab/index.ts | 17 ++ .../src/providers/gitlab/provider.ts | 166 ++++++++++++++++++ plugins/auth-backend/src/providers/types.ts | 4 + plugins/auth-backend/src/service/router.ts | 13 +- yarn.lock | 9 +- 18 files changed, 497 insertions(+), 9 deletions(-) create mode 100644 packages/core-api/src/apis/implementations/auth/gitlab/GitlabAuth.test.ts create mode 100644 packages/core-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts create mode 100644 packages/core-api/src/apis/implementations/auth/gitlab/index.ts create mode 100644 packages/core-api/src/apis/implementations/auth/gitlab/types.ts create mode 100644 plugins/auth-backend/src/providers/gitlab/index.ts create mode 100644 plugins/auth-backend/src/providers/gitlab/provider.ts diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index d2b2706464..40a8c9f275 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -27,11 +27,13 @@ import { GoogleAuth, GithubAuth, OktaAuth, + GitlabAuth, oauthRequestApiRef, OAuthRequestManager, googleAuthApiRef, githubAuthApiRef, oktaAuthApiRef, + gitlabAuthApiRef, storageApiRef, WebStorage, } from '@backstage/core'; @@ -102,6 +104,15 @@ export const apis = (config: ConfigApi) => { }), ); + builder.add( + gitlabAuthApiRef, + GitlabAuth.create({ + apiOrigin: 'http://localhost:7000', + basePath: '/auth/', + oauthRequestApi, + }), + ); + builder.add( techRadarApiRef, new TechRadar({ diff --git a/packages/core-api/src/apis/definitions/auth.ts b/packages/core-api/src/apis/definitions/auth.ts index a944e85a7e..c9a17f0153 100644 --- a/packages/core-api/src/apis/definitions/auth.ts +++ b/packages/core-api/src/apis/definitions/auth.ts @@ -242,11 +242,24 @@ export const githubAuthApiRef = createApiRef< */ export const oktaAuthApiRef = createApiRef< OAuthApi & - OpenIdConnectApi & - ProfileInfoApi & - BackstageIdentityApi & - SessionStateApi + OpenIdConnectApi & + ProfileInfoApi & + BackstageIdentityApi & + SessionStateApi >({ id: 'core.auth.okta', description: 'Provides authentication towards Okta APIs', -}); +}); + +/** + * Provides authentication towards Gitlab APIs. + * + * See https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#limiting-scopes-of-a-personal-access-token + * for a full list of supported scopes. + */ +export const gitlabAuthApiRef = createApiRef< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionStateApi +>({ + id: 'core.auth.gitlab', + description: 'Provides authentication towards Gitlab APIs', +}); diff --git a/packages/core-api/src/apis/implementations/auth/gitlab/GitlabAuth.test.ts b/packages/core-api/src/apis/implementations/auth/gitlab/GitlabAuth.test.ts new file mode 100644 index 0000000000..18346b3208 --- /dev/null +++ b/packages/core-api/src/apis/implementations/auth/gitlab/GitlabAuth.test.ts @@ -0,0 +1,46 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import GitlabAuth from './GitlabAuth'; + +describe('GitlabAuth', () => { + it('should get access token', async () => { + const getSession = jest + .fn() + .mockResolvedValue({ providerInfo: { accessToken: 'access-token' } }); + const gitlabAuth = new GitlabAuth({ getSession } as any); + + expect(await gitlabAuth.getAccessToken()).toBe('access-token'); + expect(getSession).toBeCalledTimes(1); + }); + + it('should normalize scope', () => { + const tests = [ + { + arguments: ['read_user api write_repository'], + expect: new Set(['read_user', 'api', 'write_repository']), + }, + { + arguments: ['read_repository sudo'], + expect: new Set(['read_repository', 'sudo']), + }, + ]; + + for (const test of tests) { + expect(GitlabAuth.normalizeScope(...test.arguments)).toEqual(test.expect); + } + }); +}); diff --git a/packages/core-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts b/packages/core-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts new file mode 100644 index 0000000000..20e13f8a03 --- /dev/null +++ b/packages/core-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts @@ -0,0 +1,135 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import GitlabIcon from '@material-ui/icons/AcUnit'; +import { DefaultAuthConnector } from '../../../../lib/AuthConnector'; +import { GitlabSession } from './types'; +import { + OAuthApi, + SessionStateApi, + SessionState, + ProfileInfo, + BackstageIdentity, + AuthRequestOptions, +} from '../../../definitions/auth'; +import { OAuthRequestApi, AuthProvider } from '../../../definitions'; +import { SessionManager } from '../../../../lib/AuthSessionManager/types'; +import { StaticAuthSessionManager } from '../../../../lib/AuthSessionManager'; +import { Observable } from '../../../../types'; + +type CreateOptions = { + apiOrigin: string; + basePath: string; + + oauthRequestApi: OAuthRequestApi; + + environment?: string; + provider?: AuthProvider & { id: string }; +}; + +export type GitlabAuthResponse = { + providerInfo: { + accessToken: string; + scope: string; + expiresInSeconds: number; + }; + profile: ProfileInfo; + backstageIdentity: BackstageIdentity; +}; + +const DEFAULT_PROVIDER = { + id: 'gitlab', + title: 'Gitlab', + icon: GitlabIcon, +}; + +class GitlabAuth implements OAuthApi, SessionStateApi { + static create({ + apiOrigin, + basePath, + environment = 'development', + provider = DEFAULT_PROVIDER, + oauthRequestApi, + }: CreateOptions) { + const connector = new DefaultAuthConnector({ + apiOrigin, + basePath, + environment, + provider, + oauthRequestApi, + sessionTransform(res: GitlabAuthResponse): GitlabSession { + return { + ...res, + providerInfo: { + accessToken: res.providerInfo.accessToken, + scopes: GitlabAuth.normalizeScope(res.providerInfo.scope), + expiresAt: new Date( + Date.now() + res.providerInfo.expiresInSeconds * 1000, + ), + }, + }; + }, + }); + + const sessionManager = new StaticAuthSessionManager({ + connector, + defaultScopes: new Set(['read_user']), + sessionScopes: (session: GitlabSession) => session.providerInfo.scopes, + }); + + return new GitlabAuth(sessionManager); + } + + sessionState$(): Observable { + return this.sessionManager.sessionState$(); + } + + constructor(private readonly sessionManager: SessionManager) {} + + async getAccessToken(scope?: string, options?: AuthRequestOptions) { + const session = await this.sessionManager.getSession({ + ...options, + scopes: GitlabAuth.normalizeScope(scope), + }); + return session?.providerInfo.accessToken ?? ''; + } + + async getBackstageIdentity( + options: AuthRequestOptions = {}, + ): Promise { + const session = await this.sessionManager.getSession(options); + return session?.backstageIdentity; + } + + async getProfile(options: AuthRequestOptions = {}) { + const session = await this.sessionManager.getSession(options); + return session?.profile; + } + + async logout() { + await this.sessionManager.removeSession(); + } + + static normalizeScope(scope?: string): Set { + if (!scope) { + return new Set(); + } + + const scopeList = Array.isArray(scope) ? scope : scope.split(' '); + return new Set(scopeList); + } +} +export default GitlabAuth; diff --git a/packages/core-api/src/apis/implementations/auth/gitlab/index.ts b/packages/core-api/src/apis/implementations/auth/gitlab/index.ts new file mode 100644 index 0000000000..ee17b7de06 --- /dev/null +++ b/packages/core-api/src/apis/implementations/auth/gitlab/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './types'; +export { default as GitlabAuth } from './GitlabAuth'; diff --git a/packages/core-api/src/apis/implementations/auth/gitlab/types.ts b/packages/core-api/src/apis/implementations/auth/gitlab/types.ts new file mode 100644 index 0000000000..b03dfec084 --- /dev/null +++ b/packages/core-api/src/apis/implementations/auth/gitlab/types.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ProfileInfo, BackstageIdentity } from '../../../definitions'; + +export type GitlabSession = { + providerInfo: { + accessToken: string; + scopes: Set; + expiresAt: Date; + }; + profile: ProfileInfo; + backstageIdentity: BackstageIdentity; +}; diff --git a/packages/core-api/src/apis/implementations/auth/index.ts b/packages/core-api/src/apis/implementations/auth/index.ts index 4fa992dfb5..9d89ba5b04 100644 --- a/packages/core-api/src/apis/implementations/auth/index.ts +++ b/packages/core-api/src/apis/implementations/auth/index.ts @@ -16,4 +16,5 @@ export * from './google'; export * from './github'; +export * from './gitlab'; export * from './okta'; diff --git a/packages/core/src/layout/Sidebar/UserSettings.tsx b/packages/core/src/layout/Sidebar/UserSettings.tsx index 668cb1354a..937169c8bf 100644 --- a/packages/core/src/layout/Sidebar/UserSettings.tsx +++ b/packages/core/src/layout/Sidebar/UserSettings.tsx @@ -22,6 +22,7 @@ import { SidebarContext } from './config'; import { googleAuthApiRef, githubAuthApiRef, + gitlabAuthApiRef, identityApiRef, oktaAuthApiRef, useApi, @@ -57,6 +58,11 @@ export function SidebarUserSettings() { apiRef={githubAuthApiRef} icon={Star} /> + + GitlabAuth.create({ + apiOrigin: 'http://localhost:7000', + basePath: '/auth/', + oauthRequestApi, + }), +}); diff --git a/packages/storybook/.storybook/apis.js b/packages/storybook/.storybook/apis.js index 0d0cf74e8f..4851b872a8 100644 --- a/packages/storybook/.storybook/apis.js +++ b/packages/storybook/.storybook/apis.js @@ -6,6 +6,7 @@ import { OAuthRequestManager, googleAuthApiRef, githubAuthApiRef, + gitlabAuthApiRef, oktaAuthApiRef, AlertApiForwarder, ErrorApiForwarder, @@ -52,6 +53,15 @@ builder.add( }), ); +builder.add( + gitlabAuthApiRef, + GitlabAuth.create({ + apiOrigin: 'http://localhost:7000', + basePath: '/auth/', + oauthRequestApi, + }), +); + builder.add( oktaAuthApiRef, OktaAuth.create({ diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index a336441dc2..d756704a62 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -39,6 +39,7 @@ "morgan": "^1.10.0", "passport": "^0.4.1", "passport-github2": "^0.1.12", + "passport-gitlab2": "^5.0.0", "passport-google-oauth20": "^2.0.0", "passport-oauth2": "^1.5.0", "passport-okta-oauth": "^0.0.1", diff --git a/plugins/auth-backend/src/lib/PassportStrategyHelper.ts b/plugins/auth-backend/src/lib/PassportStrategyHelper.ts index 1167946560..1936e2ff05 100644 --- a/plugins/auth-backend/src/lib/PassportStrategyHelper.ts +++ b/plugins/auth-backend/src/lib/PassportStrategyHelper.ts @@ -137,7 +137,9 @@ export const executeRefreshTokenStrategy = async ( params: any, ) => { if (err) { - reject(new Error(`Failed to refresh access token ${err}`)); + reject( + new Error(`Failed to refresh access token: ${JSON.stringify(err)}`), + ); } if (!accessToken) { reject( diff --git a/plugins/auth-backend/src/providers/factories.ts b/plugins/auth-backend/src/providers/factories.ts index 9feefdac9f..7fa375bb6b 100644 --- a/plugins/auth-backend/src/providers/factories.ts +++ b/plugins/auth-backend/src/providers/factories.ts @@ -17,6 +17,7 @@ import Router from 'express-promise-router'; import { createGithubProvider } from './github'; import { createGoogleProvider } from './google'; +import { createGitlabProvider } from './gitlab'; import { createSamlProvider } from './saml'; import { createOktaProvider } from './okta'; import { AuthProviderFactory, AuthProviderConfig } from './types'; @@ -26,6 +27,7 @@ import { TokenIssuer } from '../identity'; const factories: { [providerId: string]: AuthProviderFactory } = { google: createGoogleProvider, github: createGithubProvider, + gitlab: createGitlabProvider, saml: createSamlProvider, okta: createOktaProvider, }; diff --git a/plugins/auth-backend/src/providers/gitlab/index.ts b/plugins/auth-backend/src/providers/gitlab/index.ts new file mode 100644 index 0000000000..7fba2dd95c --- /dev/null +++ b/plugins/auth-backend/src/providers/gitlab/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { createGitlabProvider } from './provider'; diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts new file mode 100644 index 0000000000..ffc24751d3 --- /dev/null +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -0,0 +1,166 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import express from 'express'; +import { Strategy as GitlabStrategy } from 'passport-gitlab2'; +import { + executeFrameHandlerStrategy, + executeRedirectStrategy, + makeProfileInfo, +} from '../../lib/PassportStrategyHelper'; +import { + OAuthProviderHandlers, + OAuthPrivateInfo, + AuthProviderConfig, + RedirectInfo, + EnvironmentProviderConfig, + OAuthProviderOptions, + OAuthProviderConfig, + OAuthResponse, + PassportDoneCallback, +} from '../types'; +import { OAuthProvider } from '../../lib/OAuthProvider'; +import { + EnvironmentHandlers, + EnvironmentHandler, +} from '../../lib/EnvironmentHandler'; +import { Logger } from 'winston'; +import { TokenIssuer } from '../../identity'; +import passport from 'passport'; + +export class GitlabAuthProvider implements OAuthProviderHandlers { + private readonly _strategy: GitlabStrategy; + private static _defaultExpiresInSeconds = 2 * 3600; + + static transformPassportProfile(rawProfile: any): passport.Profile { + const profile: passport.Profile = { + id: rawProfile.id, + username: rawProfile.username, + provider: rawProfile.provider, + displayName: rawProfile.displayName, + }; + if (rawProfile.emails && rawProfile.emails.length > 0) { + profile.emails = rawProfile.emails; + } + if (rawProfile.avatarUrl) { + profile.photos = [{ value: rawProfile.avatarUrl }]; + } + return profile; + } + + static transformOAuthResponse( + accessToken: string, + rawProfile: any, + params: any = {}, + ): OAuthResponse { + const passportProfile = GitlabAuthProvider.transformPassportProfile( + rawProfile, + ); + const profile = makeProfileInfo(passportProfile, params.id_token); + return { + providerInfo: { + accessToken, + scope: params.scope, + expiresInSeconds: + params.expires_in || GitlabAuthProvider._defaultExpiresInSeconds, + }, + profile, + }; + } + + constructor(options: OAuthProviderOptions) { + this._strategy = new GitlabStrategy( + { ...options }, + ( + accessToken: any, + _: any, + params: any, + rawProfile: any, + done: PassportDoneCallback, + ) => { + const oauthResponse = GitlabAuthProvider.transformOAuthResponse( + accessToken, + rawProfile, + params, + ); + done(undefined, oauthResponse); + }, + ); + } + + async start( + req: express.Request, + options: Record, + ): Promise { + return await executeRedirectStrategy(req, this._strategy, options); + } + + async handler( + req: express.Request, + ): Promise<{ response: OAuthResponse; refreshToken: string }> { + const { response } = await executeFrameHandlerStrategy< + OAuthResponse, + OAuthPrivateInfo + >(req, this._strategy); + + return { response }; + } +} + +export function createGitlabProvider( + { baseUrl }: AuthProviderConfig, + providerConfig: EnvironmentProviderConfig, + logger: Logger, + tokenIssuer: TokenIssuer, +) { + const envProviders: EnvironmentHandlers = {}; + + for (const [env, envConfig] of Object.entries(providerConfig)) { + const config = (envConfig as unknown) as OAuthProviderConfig; + const { secure, appOrigin, baseUrl: oauthBaseUrl } = config; + const callbackURLParam = `?env=${env}`; + + const opts = { + clientID: config.clientId, + clientSecret: config.clientSecret, + callbackURL: `${baseUrl}/gitlab/handler/frame${callbackURLParam}`, + baseURL: oauthBaseUrl, + }; + + if (!opts.clientID || !opts.clientSecret) { + if (process.env.NODE_ENV !== 'development') { + throw new Error( + 'Failed to initialize Gitlab auth provider, set AUTH_GITLAB_CLIENT_ID and AUTH_GITLAB_CLIENT_SECRET env vars', + ); + } + + logger.warn( + 'Gitlab auth provider disabled, set AUTH_GITLAB_CLIENT_ID and AUTH_GITLAB_CLIENT_SECRET env vars to enable', + ); + continue; + } + + envProviders[env] = new OAuthProvider(new GitlabAuthProvider(opts), { + disableRefresh: true, + providerId: 'gitlab', + secure, + baseUrl, + appOrigin, + tokenIssuer, + }); + } + return new EnvironmentHandler(envProviders); +} diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index aec4f9dfd0..8dff5bb60d 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -47,6 +47,10 @@ export type OAuthProviderConfig = { * to the window that initiates an auth request. */ appOrigin: string; + /** + * Base URL of the auth provider. + */ + baseUrl: string; /** * Client ID of the auth provider. */ diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index b9c0ff4496..1ba821247f 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -76,6 +76,15 @@ export async function createRouter( clientSecret: process.env.AUTH_GITHUB_CLIENT_SECRET!, }, }, + gitlab: { + development: { + appOrigin: 'http://localhost:3000', + secure: false, + baseUrl: process.env.GITLAB_BASE_URL || 'https://gitlab.com', + clientId: process.env.AUTH_GITLAB_CLIENT_ID!, + clientSecret: process.env.AUTH_GITLAB_CLIENT_SECRET!, + }, + }, saml: { development: { entryPoint: 'http://localhost:7001/', @@ -89,8 +98,8 @@ export async function createRouter( clientId: process.env.AUTH_OKTA_CLIENT_ID!, clientSecret: process.env.AUTH_OKTA_CLIENT_SECRET!, audience: process.env.AUTH_OKTA_AUDIENCE, - } - } + }, + }, }, }, }; diff --git a/yarn.lock b/yarn.lock index f4fa34b993..d442d3ce80 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14019,6 +14019,13 @@ passport-github2@^0.1.12: dependencies: passport-oauth2 "1.x.x" +passport-gitlab2@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/passport-gitlab2/-/passport-gitlab2-5.0.0.tgz#ea37e5285321c026a02671e87469cac28cce9b69" + integrity sha512-cXQMgM6JQx9wHVh7JLH30D8fplfwjsDwRz+zS0pqC8JS+4bNmc1J04NGp5g2M4yfwylH9kQRrMN98GxMw7q7cg== + dependencies: + passport-oauth2 "^1.4.0" + passport-google-oauth20@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz#0d241b2d21ebd3dc7f2b60669ec4d587e3a674ef" @@ -14035,7 +14042,7 @@ passport-oauth1@1.x.x: passport-strategy "1.x.x" utils-merge "1.x.x" -passport-oauth2@1.x.x, passport-oauth2@^1.5.0: +passport-oauth2@1.x.x, passport-oauth2@^1.4.0, passport-oauth2@^1.5.0: version "1.5.0" resolved "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.5.0.tgz#64babbb54ac46a4dcab35e7f266ed5294e3c4108" integrity sha512-kqBt6vR/5VlCK8iCx1/KpY42kQ+NEHZwsSyt4Y6STiNjU+wWICG1i8ucc1FapXDGO15C5O5VZz7+7vRzrDPXXQ== From 9fca100dc7b92065d78f7e258e154fcd75d93d83 Mon Sep 17 00:00:00 2001 From: danztran Date: Sun, 28 Jun 2020 23:21:37 +0700 Subject: [PATCH 02/27] plugin/auth-backend: update docs --- plugins/auth-backend/README.md | 47 +++++++++++++++---- .../src/providers/gitlab/provider.ts | 15 ++++-- plugins/auth-backend/src/providers/types.ts | 4 -- plugins/auth-backend/src/service/router.ts | 2 +- 4 files changed, 49 insertions(+), 19 deletions(-) diff --git a/plugins/auth-backend/README.md b/plugins/auth-backend/README.md index 147853f06f..ff0b1c7a3d 100644 --- a/plugins/auth-backend/README.md +++ b/plugins/auth-backend/README.md @@ -7,17 +7,46 @@ This is the backend part of the auth plugin. It responds to auth requests from the frontend, and fulfills them by delegating to the appropriate provider in the backend. -## Requirements - -Needs AUTH_GOOGLE_CLIENT_ID and AUTH_GOOGLE_CLIENT_SECRET set in the environment for the backend to startup - ## Local development -export AUTH_GOOGLE_CLIENT_ID= -read -r AUTH_GOOGLE_CLIENT_SECRET - -export AUTH_GOOGLE_CLIENT_SECRET -run `yarn start` in packages/backend folder +Choose your OAuth Providers, replace `x` with actual value and then start backend: +Example for Google Oauth Provider at root directory: + +```bash +export AUTH_GOOGLE_CLIENT_ID=x +export AUTH_GOOGLE_CLIENT_SECRET=x +yarn --cwd packages/backend start +``` + +### Google + +```bash +export AUTH_GOOGLE_CLIENT_ID=x +export AUTH_GOOGLE_CLIENT_SECRET=x +``` + +### Github + +```bash +export AUTH_GITHUB_CLIENT_ID=x +export AUTH_GITHUB_CLIENT_SECRET=x +``` + +### Gitlab + +```bash +export GITLAB_BASE_URL=x # default is https://gitlab.com +export AUTH_GITLAB_CLIENT_ID=x +export AUTH_GITLAB_CLIENT_SECRET=x +``` + +### Okta + +```bash +export AUTH_OKTA_AUDIENCE=x +export AUTH_OKTA_CLIENT_ID=x +export AUTH_OKTA_CLIENT_SECRET=x +``` ### SAML diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index ffc24751d3..6e823a83f9 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -129,15 +129,20 @@ export function createGitlabProvider( const envProviders: EnvironmentHandlers = {}; for (const [env, envConfig] of Object.entries(providerConfig)) { - const config = (envConfig as unknown) as OAuthProviderConfig; - const { secure, appOrigin, baseUrl: oauthBaseUrl } = config; + const { + secure, + appOrigin, + clientId, + clientSecret, + audience, + } = (envConfig as unknown) as OAuthProviderConfig; const callbackURLParam = `?env=${env}`; const opts = { - clientID: config.clientId, - clientSecret: config.clientSecret, + clientID: clientId, + clientSecret: clientSecret, callbackURL: `${baseUrl}/gitlab/handler/frame${callbackURLParam}`, - baseURL: oauthBaseUrl, + baseURL: audience, }; if (!opts.clientID || !opts.clientSecret) { diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 8dff5bb60d..aec4f9dfd0 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -47,10 +47,6 @@ export type OAuthProviderConfig = { * to the window that initiates an auth request. */ appOrigin: string; - /** - * Base URL of the auth provider. - */ - baseUrl: string; /** * Client ID of the auth provider. */ diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 1ba821247f..fb533a4bdd 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -80,9 +80,9 @@ export async function createRouter( development: { appOrigin: 'http://localhost:3000', secure: false, - baseUrl: process.env.GITLAB_BASE_URL || 'https://gitlab.com', clientId: process.env.AUTH_GITLAB_CLIENT_ID!, clientSecret: process.env.AUTH_GITLAB_CLIENT_SECRET!, + audience: process.env.GITLAB_BASE_URL! || 'https://gitlab.com', }, }, saml: { From eeaf25fce8ef176625c43e02c6404abb3e036f71 Mon Sep 17 00:00:00 2001 From: danztran Date: Mon, 29 Jun 2020 10:55:54 +0700 Subject: [PATCH 03/27] plugin/auth-backend: add gitlab types --- .../src/providers/gitlab/provider.ts | 17 +++++-------- .../src/providers/gitlab/types.d.ts | 25 +++++++++++++++++++ 2 files changed, 31 insertions(+), 11 deletions(-) create mode 100644 plugins/auth-backend/src/providers/gitlab/types.d.ts diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index 6e823a83f9..c2b1802708 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -23,7 +23,6 @@ import { } from '../../lib/PassportStrategyHelper'; import { OAuthProviderHandlers, - OAuthPrivateInfo, AuthProviderConfig, RedirectInfo, EnvironmentProviderConfig, @@ -89,7 +88,7 @@ export class GitlabAuthProvider implements OAuthProviderHandlers { _: any, params: any, rawProfile: any, - done: PassportDoneCallback, + done: PassportDoneCallback, ) => { const oauthResponse = GitlabAuthProvider.transformOAuthResponse( accessToken, @@ -108,15 +107,11 @@ export class GitlabAuthProvider implements OAuthProviderHandlers { return await executeRedirectStrategy(req, this._strategy, options); } - async handler( - req: express.Request, - ): Promise<{ response: OAuthResponse; refreshToken: string }> { - const { response } = await executeFrameHandlerStrategy< - OAuthResponse, - OAuthPrivateInfo - >(req, this._strategy); - - return { response }; + async handler(req: express.Request): Promise<{ response: OAuthResponse }> { + return await executeFrameHandlerStrategy( + req, + this._strategy, + ); } } diff --git a/plugins/auth-backend/src/providers/gitlab/types.d.ts b/plugins/auth-backend/src/providers/gitlab/types.d.ts new file mode 100644 index 0000000000..8cd7373c52 --- /dev/null +++ b/plugins/auth-backend/src/providers/gitlab/types.d.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +declare module 'passport-gitlab2' { + import { Request } from 'express'; + import { StrategyCreated } from 'passport'; + + export class Strategy { + constructor(options: any, verify: any); + authenticate(this: StrategyCreated, req: Request, options?: any): any; + } +} From c1806a0e2d479615def038ffccb546a140a71f3f Mon Sep 17 00:00:00 2001 From: danztran Date: Mon, 29 Jun 2020 11:43:17 +0700 Subject: [PATCH 04/27] plugin/auth-backend: add test for gitlab provider --- .../src/providers/gitlab/provider.test.ts | 100 ++++++++++++++++++ .../src/providers/gitlab/provider.ts | 24 +++-- 2 files changed, 117 insertions(+), 7 deletions(-) create mode 100644 plugins/auth-backend/src/providers/gitlab/provider.test.ts diff --git a/plugins/auth-backend/src/providers/gitlab/provider.test.ts b/plugins/auth-backend/src/providers/gitlab/provider.test.ts new file mode 100644 index 0000000000..5eec6c47f9 --- /dev/null +++ b/plugins/auth-backend/src/providers/gitlab/provider.test.ts @@ -0,0 +1,100 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { GitlabAuthProvider } from './provider'; + +describe('GitlabAuthProvider', () => { + it('should transform to type OAuthResponse', () => { + const tests = [ + { + arguments: { + accessToken: '19xasczxcm9n7gacn9jdgm19me', + rawProfile: { + id: 'uid-123', + username: 'jimmymarkum', + provider: 'gitlab', + displayName: 'Jimmy Markum', + emails: [ + { + value: 'jimmymarkum@gmail.com', + }, + ], + avatarUrl: + 'https://a1cf74336522e87f135f-2f21ace9a6cf0052456644b80fa06d4f.ssl.cf2.rackcdn.com/images/characters_opt/p-mystic-river-sean-penn.jpg', + }, + params: { + scope: 'user_read write_repository', + expires_in: 100, + }, + }, + expect: { + providerInfo: { + accessToken: '19xasczxcm9n7gacn9jdgm19me', + expiresInSeconds: 100, + scope: 'user_read write_repository', + }, + profile: { + email: 'jimmymarkum@gmail.com', + displayName: 'Jimmy Markum', + picture: + 'https://a1cf74336522e87f135f-2f21ace9a6cf0052456644b80fa06d4f.ssl.cf2.rackcdn.com/images/characters_opt/p-mystic-river-sean-penn.jpg', + }, + }, + }, + { + arguments: { + accessToken: + 'ajakljsdoiahoawxbrouawucmbawe.awkxjemaneasdxwe.sodijxqeqwexeqwxe', + rawProfile: { + id: 'ipd12039', + username: 'daveboyle', + provider: 'gitlab', + displayName: 'Dave Boyle', + emails: [ + { + value: 'daveboyle@gitlab.org', + }, + ], + }, + params: { + scope: 'read_repository', + }, + }, + expect: { + providerInfo: { + accessToken: + 'ajakljsdoiahoawxbrouawucmbawe.awkxjemaneasdxwe.sodijxqeqwexeqwxe', + scope: 'read_repository', + }, + profile: { + displayName: 'Dave Boyle', + email: 'daveboyle@gitlab.org', + }, + }, + }, + ]; + + for (const test of tests) { + expect( + GitlabAuthProvider.transformOAuthResponse( + test.arguments.accessToken, + test.arguments.rawProfile, + test.arguments.params, + ), + ).toEqual(test.expect); + } + }); +}); diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index c2b1802708..890fbb3531 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -42,7 +42,6 @@ import passport from 'passport'; export class GitlabAuthProvider implements OAuthProviderHandlers { private readonly _strategy: GitlabStrategy; - private static _defaultExpiresInSeconds = 2 * 3600; static transformPassportProfile(rawProfile: any): passport.Profile { const profile: passport.Profile = { @@ -51,12 +50,14 @@ export class GitlabAuthProvider implements OAuthProviderHandlers { provider: rawProfile.provider, displayName: rawProfile.displayName, }; + if (rawProfile.emails && rawProfile.emails.length > 0) { profile.emails = rawProfile.emails; } if (rawProfile.avatarUrl) { profile.photos = [{ value: rawProfile.avatarUrl }]; } + return profile; } @@ -68,14 +69,23 @@ export class GitlabAuthProvider implements OAuthProviderHandlers { const passportProfile = GitlabAuthProvider.transformPassportProfile( rawProfile, ); + const profile = makeProfileInfo(passportProfile, params.id_token); + const providerInfo = { + accessToken, + scope: params.scope, + expiresInSeconds: params.expires_in, + idToken: params.id_token, + }; + + if (params.expires_in) { + providerInfo.expiresInSeconds = params.expires_in; + } + if (params.id_token) { + providerInfo.idToken = params.id_token; + } return { - providerInfo: { - accessToken, - scope: params.scope, - expiresInSeconds: - params.expires_in || GitlabAuthProvider._defaultExpiresInSeconds, - }, + providerInfo, profile, }; } From fba2c8c3b28d9cc9c9db8227715c85be64a4c527 Mon Sep 17 00:00:00 2001 From: danztran Date: Mon, 29 Jun 2020 12:03:51 +0700 Subject: [PATCH 05/27] plugin/auth-backend: fix test on passport strategy helper --- plugins/auth-backend/src/lib/PassportStrategyHelper.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/plugins/auth-backend/src/lib/PassportStrategyHelper.ts b/plugins/auth-backend/src/lib/PassportStrategyHelper.ts index 1936e2ff05..4cc0de4444 100644 --- a/plugins/auth-backend/src/lib/PassportStrategyHelper.ts +++ b/plugins/auth-backend/src/lib/PassportStrategyHelper.ts @@ -137,9 +137,7 @@ export const executeRefreshTokenStrategy = async ( params: any, ) => { if (err) { - reject( - new Error(`Failed to refresh access token: ${JSON.stringify(err)}`), - ); + reject(new Error(`Failed to refresh access token ${err.toString()}`)); } if (!accessToken) { reject( From 11307d46b989729c8da28bec8a23b01a2959ceb3 Mon Sep 17 00:00:00 2001 From: danztran Date: Mon, 29 Jun 2020 12:13:30 +0700 Subject: [PATCH 06/27] storybook: fix gitlab auth --- packages/storybook/.storybook/apis.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/storybook/.storybook/apis.js b/packages/storybook/.storybook/apis.js index 4851b872a8..5b00a3c3e0 100644 --- a/packages/storybook/.storybook/apis.js +++ b/packages/storybook/.storybook/apis.js @@ -13,6 +13,7 @@ import { ErrorAlerter, GoogleAuth, GithubAuth, + GitlabAuth, OktaAuth, identityApiRef, } from '@backstage/core'; From 563a8a5f4e9ac8301b98512c1fb9fbc121915187 Mon Sep 17 00:00:00 2001 From: ellinors Date: Mon, 29 Jun 2020 13:37:50 +0200 Subject: [PATCH 07/27] Enabled 'dense' mode for Table cells. Hardcoded cell styling for Table was removed to enable 'dense' as a cell padding choice. Padding customisation for inherited smallSize was added. Dense mode was chosen for sentry issues table rows. --- packages/core/src/components/Table/Table.tsx | 13 ------------- packages/theme/src/baseTheme.ts | 5 ++++- .../SentryIssuesTable/SentryIssuesTable.tsx | 2 +- 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/packages/core/src/components/Table/Table.tsx b/packages/core/src/components/Table/Table.tsx index 047a6b1990..89aa2dce1f 100644 --- a/packages/core/src/components/Table/Table.tsx +++ b/packages/core/src/components/Table/Table.tsx @@ -35,7 +35,6 @@ import ViewColumn from '@material-ui/icons/ViewColumn'; import MTable, { Column, MaterialTableProps, - MTableCell, MTableHeader, MTableToolbar, Options, @@ -96,14 +95,6 @@ const tableIcons = { )), }; -const useCellStyles = makeStyles(theme => ({ - root: { - color: theme.palette.grey[500], - padding: theme.spacing(0, 2, 0, 2.5), - height: '56px', - }, -})); - const useHeaderStyles = makeStyles(theme => ({ header: { padding: theme.spacing(1, 2, 1, 2.5), @@ -169,7 +160,6 @@ export function Table({ subtitle, ...props }: TableProps) { - const cellClasses = useCellStyles(); const headerClasses = useHeaderStyles(); const toolbarClasses = useToolbarStyles(); const theme = useTheme(); @@ -185,9 +175,6 @@ export function Table({ return ( components={{ - Cell: cellProps => ( - - ), Header: headerProps => ( ), diff --git a/packages/theme/src/baseTheme.ts b/packages/theme/src/baseTheme.ts index 955b182ae9..dc68db11d6 100644 --- a/packages/theme/src/baseTheme.ts +++ b/packages/theme/src/baseTheme.ts @@ -118,9 +118,12 @@ export function createThemeOverrides(theme: BackstageTheme): Overrides { verticalAlign: 'middle', lineHeight: '1', margin: 0, - padding: '8px', + padding: theme.spacing(3, 2, 3, 2.5), borderBottom: 0, }, + sizeSmall: { + padding: theme.spacing(1, 2, 1, 2.5), + }, head: { wordBreak: 'break-word', overflow: 'hidden', diff --git a/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.tsx b/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.tsx index e861c6bbe5..cebd5563f4 100644 --- a/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.tsx +++ b/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.tsx @@ -64,7 +64,7 @@ const SentryIssuesTable: FC = ({ sentryIssues }) => { return ( From 74d949be98dfbad4bb4aadd353b6a77ffae39d81 Mon Sep 17 00:00:00 2001 From: Sebastian Qvarfordt Date: Mon, 29 Jun 2020 16:34:06 +0200 Subject: [PATCH 08/27] Changes to material theme css --- .../techdocs/src/reader/components/Reader.tsx | 72 +++++++++++-------- .../techdocs/src/reader/transformers/index.ts | 2 + .../transformers/modifyCssTransformer.ts | 45 ++++++++++++ .../reader/transformers/removeMkdocsHeader.ts | 28 ++++++++ 4 files changed, 116 insertions(+), 31 deletions(-) create mode 100644 plugins/techdocs/src/reader/transformers/modifyCssTransformer.ts create mode 100644 plugins/techdocs/src/reader/transformers/removeMkdocsHeader.ts diff --git a/plugins/techdocs/src/reader/components/Reader.tsx b/plugins/techdocs/src/reader/components/Reader.tsx index 474457ac5c..e9092f1394 100644 --- a/plugins/techdocs/src/reader/components/Reader.tsx +++ b/plugins/techdocs/src/reader/components/Reader.tsx @@ -21,6 +21,8 @@ import transformer, { addBaseUrl, rewriteDocLinks, addEventListener, + removeMkdocsHeader, + modifyCssTransformer, } from '../transformers'; import { docStorageURL } from '../../config'; import { Grid } from '@material-ui/core'; @@ -74,6 +76,16 @@ export const Reader = () => { rewriteDocLinks({ componentId, }), + modifyCssTransformer({ + cssTransforms: { + '.md-main__inner': [{ 'margin-top': '0' }], + '.md-sidebar': [{ top: '0' }, { width: '20rem' }], + '.md-typeset': [{ 'font-size': '1rem' }], + '.md-nav': [{ 'font-size': '1rem' }], + '.md-grid': [{ 'max-width': '80vw' }], + }, + }), + removeMkdocsHeader({}), ]); divElement.shadowRoot.innerHTML = ''; @@ -90,39 +102,37 @@ export const Reader = () => { return ( <> - {componentId ? ( -
- ) : ( - <> -
+
- - - - navigate('/docs/mkdocs')} - tags={['Developer Tool']} - title="MkDocs" - label="Read Docs" - description="MkDocs is a fast, simple and downright gorgeous static site generator that's geared towards building project documentation. " - /> - - - navigate('/docs/backstage-microsite')} - tags={['Service']} - title="Backstage" - label="Read Docs" - description="Getting started guides, API Overview, documentation around how to Create a Plugin and more. " - /> - + + {componentId ? ( +
+ ) : ( + + + navigate('/docs/mkdocs')} + tags={['Developer Tool']} + title="MkDocs" + label="Read Docs" + description="MkDocs is a fast, simple and downright gorgeous static site generator that's geared towards building project documentation. " + /> - - - )} + + navigate('/docs/backstage-microsite')} + tags={['Service']} + title="Backstage" + label="Read Docs" + description="Getting started guides, API Overview, documentation around how to Create a Plugin and more. " + /> + + + )} + ); }; diff --git a/plugins/techdocs/src/reader/transformers/index.ts b/plugins/techdocs/src/reader/transformers/index.ts index 64c5527fe7..e6d178b6bd 100644 --- a/plugins/techdocs/src/reader/transformers/index.ts +++ b/plugins/techdocs/src/reader/transformers/index.ts @@ -17,6 +17,8 @@ export * from './addBaseUrl'; export * from './rewriteDocLinks'; export * from './addEventListener'; +export * from './removeMkdocsHeader'; +export * from './modifyCssTransformer'; export type Transformer = (dom: Element) => Element; diff --git a/plugins/techdocs/src/reader/transformers/modifyCssTransformer.ts b/plugins/techdocs/src/reader/transformers/modifyCssTransformer.ts new file mode 100644 index 0000000000..2ee06c54da --- /dev/null +++ b/plugins/techdocs/src/reader/transformers/modifyCssTransformer.ts @@ -0,0 +1,45 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Transformer } from './index'; + +type ModifyCssTransformerOptions = { + // Example: { '.md-container': { 'marginTop': '10px' }} + cssTransforms: { [key: string]: { [key: string]: string }[] }; +}; + +export const modifyCssTransformer = ({ + cssTransforms, +}: ModifyCssTransformerOptions): Transformer => { + return dom => { + Object.entries(cssTransforms).forEach(([cssSelector, cssChanges]) => { + const elementsToChange = Array.from( + dom.querySelectorAll(cssSelector), + ); + if (elementsToChange.length < 1) return; + + cssChanges.forEach(changes => { + elementsToChange.forEach((element: HTMLElement) => { + Object.entries(changes).forEach(([cssProperty, cssValue]) => { + element.style.setProperty(cssProperty, cssValue); + }); + }); + }); + }); + + return dom; + }; +}; diff --git a/plugins/techdocs/src/reader/transformers/removeMkdocsHeader.ts b/plugins/techdocs/src/reader/transformers/removeMkdocsHeader.ts new file mode 100644 index 0000000000..f26947f8dc --- /dev/null +++ b/plugins/techdocs/src/reader/transformers/removeMkdocsHeader.ts @@ -0,0 +1,28 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Transformer } from './index'; + +type AddBaseUrlOptions = {}; + +export const removeMkdocsHeader = ({}: AddBaseUrlOptions): Transformer => { + return dom => { + // Remove the header + dom.querySelector('.md-header')?.remove(); + + return dom; + }; +}; From 8c503d57f1d60509087ab3044aead22f7f4846a3 Mon Sep 17 00:00:00 2001 From: ellinors Date: Tue, 30 Jun 2020 07:49:15 +0200 Subject: [PATCH 09/27] Added DenseTable to Storybook. --- .../src/components/Table/Table.stories.tsx | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/packages/core/src/components/Table/Table.stories.tsx b/packages/core/src/components/Table/Table.stories.tsx index 3709c301fd..11fa0c44dc 100644 --- a/packages/core/src/components/Table/Table.stories.tsx +++ b/packages/core/src/components/Table/Table.stories.tsx @@ -185,3 +185,38 @@ export const SubvalueTable = () => {
); }; + +export const DenseTable = () => { + const columns: TableColumn[] = [ + { + title: 'Column 1', + field: 'col1', + highlight: true, + }, + { + title: 'Column 2', + field: 'col2', + }, + { + title: 'Numeric value', + field: 'number', + type: 'numeric', + }, + { + title: 'A Date', + field: 'date', + type: 'date', + }, + ]; + + return ( +
+
+ + ); +}; From a306bf2c270a806895ddfd084e42aa8ec404a23b Mon Sep 17 00:00:00 2001 From: Raghunandan Date: Wed, 24 Jun 2020 12:18:37 +0200 Subject: [PATCH 10/27] Use new GithubAuth flow in GitOps plugin --- plugins/gitops-profiles/package.json | 2 +- plugins/gitops-profiles/src/api.ts | 22 +++++ .../components/ClusterList/ClusterList.tsx | 31 +++---- .../components/ClusterPage/ClusterPage.tsx | 29 ++++--- .../ProfileCatalog/ProfileCatalog.tsx | 51 ++++++++---- plugins/gitops-profiles/src/plugin.ts | 11 ++- plugins/gitops-profiles/src/routes.ts | 37 +++++++++ yarn.lock | 81 ++----------------- 8 files changed, 139 insertions(+), 125 deletions(-) create mode 100644 plugins/gitops-profiles/src/routes.ts diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index bfb0328909..67a76ef872 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -28,7 +28,7 @@ "@material-ui/lab": "4.0.0-alpha.45", "react": "^16.13.1", "react-dom": "^16.13.1", - "react-router-dom": "^5.2.0", + "react-router-dom": "6.0.0-alpha.5", "react-use": "^14.2.0" }, "devDependencies": { diff --git a/plugins/gitops-profiles/src/api.ts b/plugins/gitops-profiles/src/api.ts index 20e4dc4307..3a14dea837 100644 --- a/plugins/gitops-profiles/src/api.ts +++ b/plugins/gitops-profiles/src/api.ts @@ -79,6 +79,14 @@ export interface ListClusterRequest { gitHubToken: string; } +export interface GithubUserInfoRequest { + accessToken: string; +} + +export interface GithubUserInfoResponse { + login: string; +} + export class FetchError extends Error { get name(): string { return this.constructor.name; @@ -100,6 +108,7 @@ export type GitOpsApi = { cloneClusterFromTemplate(req: CloneFromTemplateRequest): Promise; applyProfiles(req: ApplyProfileRequest): Promise; listClusters(req: ListClusterRequest): Promise; + fetchUserInfo(req: GithubUserInfoRequest): Promise; }; export const gitOpsApiRef = createApiRef({ @@ -116,6 +125,19 @@ export class GitOpsRestApi implements GitOpsApi { return await resp.json(); } + async fetchUserInfo( + req: GithubUserInfoRequest, + ): Promise { + const resp = await fetch(`https://api.github.com/user`, { + method: 'get', + headers: new Headers({ + Authorization: `token ${req.accessToken}`, + }), + }); + if (!resp.ok) throw await FetchError.forResponse(resp); + return await resp.json(); + } + async fetchLog(req: PollLogRequest): Promise { return await this.fetch(`/api/cluster/run-status`, { method: 'post', diff --git a/plugins/gitops-profiles/src/components/ClusterList/ClusterList.tsx b/plugins/gitops-profiles/src/components/ClusterList/ClusterList.tsx index 8e5aa15aa9..c4b94e2958 100644 --- a/plugins/gitops-profiles/src/components/ClusterList/ClusterList.tsx +++ b/plugins/gitops-profiles/src/components/ClusterList/ClusterList.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { FC } from 'react'; +import React, { FC, useState } from 'react'; import { Content, ContentHeader, @@ -25,32 +25,28 @@ import { Progress, HeaderLabel, useApi, + githubAuthApiRef, } from '@backstage/core'; import ClusterTable from '../ClusterTable/ClusterTable'; import { Button } from '@material-ui/core'; -import { useAsync, useLocalStorage } from 'react-use'; +import { useAsync } from 'react-use'; import { gitOpsApiRef, ListClusterStatusesResponse } from '../../api'; import { Alert } from '@material-ui/lab'; const ClusterList: FC<{}> = () => { - const [loginInfo] = useLocalStorage<{ - token: string; - username: string; - name: string; - }>('githubLoginDetails', { - token: '', - username: '', - name: 'Guest', - }); - const api = useApi(gitOpsApiRef); + const githubAuth = useApi(githubAuthApiRef); + const [githubUsername, setGithubUsername] = useState(String); const { loading, error, value } = useAsync( - () => { + async () => { + const accessToken = await githubAuth.getAccessToken(); + const userInfo = await api.fetchUserInfo({ accessToken }); + setGithubUsername(userInfo.login); return api.listClusters({ - gitHubToken: loginInfo.token, - gitHubUser: loginInfo.username, + gitHubToken: accessToken, + gitHubUser: githubUsername, }); }, ); @@ -73,9 +69,6 @@ const ClusterList: FC<{}> = () => { Please make sure that you start GitOps-API backend on localhost port 3008 before using this plugin. - - If you're Guest, please login via GitHub first. - ); @@ -100,7 +93,7 @@ const ClusterList: FC<{}> = () => { return (
- +
{content}
diff --git a/plugins/gitops-profiles/src/components/ClusterPage/ClusterPage.tsx b/plugins/gitops-profiles/src/components/ClusterPage/ClusterPage.tsx index d454278255..c66b0613b5 100644 --- a/plugins/gitops-profiles/src/components/ClusterPage/ClusterPage.tsx +++ b/plugins/gitops-profiles/src/components/ClusterPage/ClusterPage.tsx @@ -24,21 +24,16 @@ import { Progress, HeaderLabel, useApi, + githubAuthApiRef, } from '@backstage/core'; import { Link } from '@material-ui/core'; import { useParams } from 'react-router-dom'; -import { useLocalStorage } from 'react-use'; import { gitOpsApiRef, Status } from '../../api'; import { transformRunStatus } from '../ProfileCatalog'; const ClusterPage: FC<{}> = () => { const params = useParams() as { owner: string; repo: string }; - const [loginInfo] = useLocalStorage<{ - token: string; - username: string; - name: string; - }>('githubLoginDetails'); const [pollingLog, setPollingLog] = useState(true); const [runStatus, setRunStatus] = useState([]); @@ -46,6 +41,9 @@ const ClusterPage: FC<{}> = () => { const [showProgress, setShowProgress] = useState(true); const api = useApi(gitOpsApiRef); + const githubAuth = useApi(githubAuthApiRef); + const [githubAccessToken, setGithubAccessToken] = useState(String); + const [githubUsername, setGithubUsername] = useState(String); const columns = [ { field: 'status', title: 'Status' }, @@ -53,11 +51,22 @@ const ClusterPage: FC<{}> = () => { ]; useEffect(() => { + const fetchGithubUserInfo = async () => { + const accessToken = await githubAuth.getAccessToken(); + const userInfo = await api.fetchUserInfo({ accessToken }); + setGithubAccessToken(accessToken); + setGithubUsername(userInfo.login); + }; + + if (!githubAccessToken || !githubUsername) { + fetchGithubUserInfo(); + } + if (pollingLog) { const interval = setInterval(async () => { const resp = await api.fetchLog({ - gitHubToken: loginInfo.token, - gitHubUser: loginInfo.username, + gitHubToken: githubAccessToken, + gitHubUser: githubUsername, targetOrg: params.owner, targetRepo: params.repo, }); @@ -72,12 +81,12 @@ const ClusterPage: FC<{}> = () => { return () => clearInterval(interval); } return () => {}; - }, [pollingLog, api, loginInfo, params]); + }, [pollingLog, api, params, githubAuth, githubAccessToken, githubUsername]); return (
- +
+ + + + +
+
+
+1
+2
+
+
+
+
pip install click-man
+click-man --target path/to/man/pages mkdocs
+
+
+
+ +

+ See the + click-man documentation + for an explanation of why manpages are not automatically + generated and installed by pip. +

+ +
+

Note

+

+ If you are using Windows, some of the above commands may not + work out-of-the-box. +

+

+ A quick solution may be to preface every Python command with + python -m like this: +

+ + + + + +
+
+
+1
+2
+
+
+
+
python -m pip install mkdocs
+python -m mkdocs
+
+
+
+ +

+ For a more permanent solution, you may need to edit your + PATH environment variable to include the + Scripts directory of your Python installation. + Recent versions of Python include a script to do this for you. + Navigate to your Python installation directory (for example + C:\Python38\), open the Tools, then + Scripts folder, and run the + win_add2path.py file by double clicking on it. + Alternatively, you can + download + the script and run it (python win_add2path.py). +

+
+
+

+ Getting Started +

+

Getting started is super easy.

+
+
mkdocs new my-project
+cd my-project
+
+
+ +

+ Take a moment to review the initial project that has been + created for you. +

+

+ The initial MkDocs layout +

+

+ There's a single configuration file named + mkdocs.yml, and a folder named + docs that will contain your documentation source + files. Right now the docs folder just contains a + single documentation page, named index.md. +

+

+ MkDocs comes with a built-in dev-server that lets you preview + your documentation as you work on it. Make sure you're in the + same directory as the mkdocs.yml configuration + file, and then start the server by running the + mkdocs serve command: +

+
+
$ mkdocs serve
+INFO    -  Building documentation...
+INFO    -  Cleaning site directory
+[I 160402 15:50:43 server:271] Serving on http://127.0.0.1:8000
+[I 160402 15:50:43 handlers:58] Start watching changes
+[I 160402 15:50:43 handlers:60] Start detecting changes
+
+
+ +

+ Open up http://127.0.0.1:8000/ in your browser, and + you'll see the default home page being displayed: +

+

+ The MkDocs live server +

+

+ The dev-server also supports auto-reloading, and will rebuild + your documentation whenever anything in the configuration file, + documentation directory, or theme directory changes. +

+

+ Open the docs/index.md document in your text editor + of choice, change the initial heading to MkLorum, + and save your changes. Your browser will auto-reload and you + should see your updated documentation immediately. +

+

+ Now try editing the configuration file: mkdocs.yml. + Change the + site_name + setting to MkLorum and save the file. +

+
+
site_name: MkLorum
+
+
+ +

+ Your browser should immediately reload, and you'll see your new + site name take effect. +

+

The site_name setting

+

+ Adding pages +

+

Now add a second page to your documentation:

+
+
curl 'https://jaspervdj.be/lorem-markdownum/markdown.txt' > docs/about.md
+
+
+ +

+ As our documentation site will include some navigation headers, + you may want to edit the configuration file and add some + information about the order, title, and nesting of each page in + the navigation header by adding a + nav + setting: +

+
+
site_name: MkLorum
+nav:
+    - Home: index.md
+    - About: about.md
+
+
+ +

+ Save your changes and you'll now see a navigation bar with + Home and About items on the left as + well as Search, Previous, and + Next items on the right. +

+

Screenshot

+

+ Try the menu items and navigate back and forth between pages. + Then click on Search. A search dialog will appear, + allowing you to search for any text on any page. Notice that the + search results include every occurrence of the search term on + the site and links directly to the section of the page in which + the search term appears. You get all of that with no effort or + configuration on your part! +

+

Screenshot

+

+ Theming our documentation +

+

+ Now change the configuration file to alter how the documentation + is displayed by changing the theme. Edit the + mkdocs.yml file and add a + theme + setting: +

+
+
site_name: MkLorum
+nav:
+    - Home: index.md
+    - About: about.md
+theme: readthedocs
+
+
+ +

+ Save your changes, and you'll see the ReadTheDocs theme being + used. +

+

Screenshot

+

+ Changing the Favicon Icon +

+

+ By default, MkDocs uses the + MkDocs favicon icon. To use a + different icon, create an img subdirectory in your + docs_dir and copy your custom + favicon.ico file to that directory. MkDocs will + automatically detect and use that file as your favicon icon. +

+

+ Building the site +

+

+ That's looking good. You're ready to deploy the first pass of + your MkLorum documentation. First build the + documentation: +

+
+
mkdocs build
+
+
+ +

+ This will create a new directory, named site. Take + a look inside the directory: +

+
+
$ ls site
+about  fonts  index.html  license  search.html
+css    img    js          mkdocs   sitemap.xml
+
+
+ +

+ Notice that your source documentation has been output as two + HTML files named index.html and + about/index.html. You also have various other media + that's been copied into the site directory as part + of the documentation theme. You even have a + sitemap.xml file and + mkdocs/search_index.json. +

+

+ If you're using source code control such as git you + probably don't want to check your documentation builds into the + repository. Add a line containing site/ to your + .gitignore file. +

+
+
echo "site/" >> .gitignore
+
+
+ +

+ If you're using another source code control tool you'll want to + check its documentation on how to ignore specific directories. +

+

+ After some time, files may be removed from the documentation but + they will still reside in the site directory. To + remove those stale files, just run mkdocs with the + --clean switch. +

+
+
mkdocs build --clean
+
+
+ +

+ Other Commands and Options +

+

+ There are various other commands and options available. For a + complete list of commands, use the --help flag: +

+
+
mkdocs --help
+
+
+ +

+ To view a list of options available on a given command, use the + --help flag with that command. For example, to get + a list of all options available for the + build command run the following: +

+
+
mkdocs build --help
+
+
+ +

+ Deploying +

+

+ The documentation site that you just built only uses static + files so you'll be able to host it from pretty much anywhere. + GitHub project pages + and + Amazon S3 + may be good hosting options, depending upon your needs. Upload + the contents of the entire site directory to + wherever you're hosting your website from and you're done. For + specific instructions on a number of common hosts, see the + Deploying your Docs + page. +

+

+ Getting help +

+

+ To get help with MkDocs, please use the + discussion group, + GitHub issues + or the MkDocs IRC channel #mkdocs on freenode. +

+ + + + + + + + + + + + + + + +`; diff --git a/plugins/techdocs/src/test-utils/index.ts b/plugins/techdocs/src/test-utils/index.ts new file mode 100644 index 0000000000..4cf9766b66 --- /dev/null +++ b/plugins/techdocs/src/test-utils/index.ts @@ -0,0 +1,66 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// @ts-ignore +import { JSDOM } from 'jsdom'; + +import FIXTURE_STANDARD_PAGE from './fixtures/mkdocs-index'; +import transformer from '../reader/transformers'; +import type { Transformer } from '../reader/transformers'; + +export const FIXTURES = { + FIXTURE_STANDARD_PAGE, +}; + +export type CreateTestShadowDomOptions = { + transformers: Transformer[]; +}; + +export const createTestShadowDom = ( + fixture: string, + opts: CreateTestShadowDomOptions = { transformers: [] }, +): ShadowRoot => { + const divElement = document.createElement('div'); + divElement.attachShadow({ mode: 'open' }); + document.body.appendChild(divElement); + + const domParser = new DOMParser().parseFromString(fixture, 'text/html'); + divElement.shadowRoot?.appendChild(domParser.documentElement); + + if (opts.transformers) { + transformer(divElement.shadowRoot!.children[0], opts.transformers); + } + + return divElement.shadowRoot!; +}; + +export const getSample = ( + shadowDom: ShadowRoot, + elementName: string, + elementAttribute: string, +) => { + const sampleSize = 2; + const rootElement = shadowDom.children[0]; + + return Array.from(rootElement.getElementsByTagName(elementName)) + .filter(elem => { + return elem.hasAttribute(elementAttribute); + }) + .slice(0, sampleSize) + .map(elem => { + return elem.getAttribute(elementAttribute); + }); +}; diff --git a/yarn.lock b/yarn.lock index 9b25df19cc..9a2c389161 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3555,6 +3555,15 @@ resolved "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.6.tgz#f1a1cb35aff47bc5cfb05cb0c441ca91e914c26f" integrity sha512-+oY0FDTO2GYKEV0YPvSshGq9t7YozVkgvXLty7zogQNuCxBhT9/3INX9Q7H1aRZ4SUDRXAKlJuA4EA5nTt7SNw== +"@types/jsdom@^16.2.3": + version "16.2.3" + resolved "https://registry.npmjs.org/@types/jsdom/-/jsdom-16.2.3.tgz#c6feadfe0836389b27f9c911cde82cd32e91c537" + integrity sha512-BREatezSn74rmLIDksuqGNFUTi9HNAWWQXYpFBFLK9U6wlMCO4M0QCa8CMpDsZQuqxSO9XifVLT5Q1P0vgKLqw== + dependencies: + "@types/node" "*" + "@types/parse5" "*" + "@types/tough-cookie" "*" + "@types/json-schema@*", "@types/json-schema@^7.0.3": version "7.0.4" resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339" @@ -3660,6 +3669,11 @@ resolved "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== +"@types/parse5@*": + version "5.0.3" + resolved "https://registry.npmjs.org/@types/parse5/-/parse5-5.0.3.tgz#e7b5aebbac150f8b5fdd4a46e7f0bd8e65e19109" + integrity sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw== + "@types/passport-github2@^1.2.4": version "1.2.4" resolved "https://registry.npmjs.org/@types/passport-github2/-/passport-github2-1.2.4.tgz#f56c386d1fe6435e359430e57adc1747a627bd86" @@ -3968,6 +3982,11 @@ dependencies: "@types/node" "*" +"@types/tough-cookie@*": + version "4.0.0" + resolved "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.0.tgz#fef1904e4668b6e5ecee60c52cc6a078ffa6697d" + integrity sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A== + "@types/uglify-js@*": version "3.0.4" resolved "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.0.4.tgz#96beae23df6f561862a830b4288a49e86baac082" From 60cb8b08793ec33eec9f84002a7acdbe621c47a6 Mon Sep 17 00:00:00 2001 From: Sebastian Qvarfordt Date: Tue, 30 Jun 2020 13:16:57 +0200 Subject: [PATCH 19/27] Added rewriteDocLinks test --- ...ener.test.tsx => addEventListener.test.ts} | 0 .../transformers/rewriteDocLinks.test.ts | 57 +++++++++++++++++++ .../reader/transformers/rewriteDocLinks.ts | 4 +- plugins/techdocs/src/test-utils/index.ts | 2 +- 4 files changed, 59 insertions(+), 4 deletions(-) rename plugins/techdocs/src/reader/transformers/{addEventListener.test.tsx => addEventListener.test.ts} (100%) create mode 100644 plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts diff --git a/plugins/techdocs/src/reader/transformers/addEventListener.test.tsx b/plugins/techdocs/src/reader/transformers/addEventListener.test.ts similarity index 100% rename from plugins/techdocs/src/reader/transformers/addEventListener.test.tsx rename to plugins/techdocs/src/reader/transformers/addEventListener.test.ts diff --git a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts new file mode 100644 index 0000000000..bcc45937b5 --- /dev/null +++ b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts @@ -0,0 +1,57 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createTestShadowDom, getSample } from '../../test-utils'; +import { rewriteDocLinks } from '../transformers'; + +describe('rewriteDocLinks', () => { + it('contains relative paths', () => { + const shadowDom = createTestShadowDom(` + Test + Test + Test + Test Sub Page + `); + + expect(getSample(shadowDom, 'a', 'href', 6)).toEqual([ + 'http://example.org/', + '../example', + 'example-docs', + 'example-docs/example-page', + ]); + }); + + it('contains transformed absolute paths', () => { + const shadowDom = createTestShadowDom( + ` + Test + Test + Test + Test Sub Page + `, + { + transformers: [rewriteDocLinks()], + }, + ); + + expect(getSample(shadowDom, 'a', 'href', 6)).toEqual([ + 'http://example.org/', + 'http://localhost/example', + 'http://localhost/example-docs', + 'http://localhost/example-docs/example-page', + ]); + }); +}); diff --git a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.ts b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.ts index 3f80dbe4e9..7b8495b6b7 100644 --- a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.ts +++ b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.ts @@ -17,9 +17,7 @@ import URLParser from '../urlParser'; import type { Transformer } from './index'; -type AddBaseUrlOptions = {}; - -export const rewriteDocLinks = ({}: AddBaseUrlOptions): Transformer => { +export const rewriteDocLinks = (): Transformer => { return dom => { const updateDom = ( list: Array, diff --git a/plugins/techdocs/src/test-utils/index.ts b/plugins/techdocs/src/test-utils/index.ts index 4cf9766b66..98472d75a4 100644 --- a/plugins/techdocs/src/test-utils/index.ts +++ b/plugins/techdocs/src/test-utils/index.ts @@ -51,8 +51,8 @@ export const getSample = ( shadowDom: ShadowRoot, elementName: string, elementAttribute: string, + sampleSize = 2, ) => { - const sampleSize = 2; const rootElement = shadowDom.children[0]; return Array.from(rootElement.getElementsByTagName(elementName)) From 3c8f32e45b81421a79d1ea90be76d1772b340b09 Mon Sep 17 00:00:00 2001 From: Sebastian Qvarfordt Date: Tue, 30 Jun 2020 13:19:18 +0200 Subject: [PATCH 20/27] Updated test descriptions --- .../techdocs/src/reader/transformers/rewriteDocLinks.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts index bcc45937b5..83ca846142 100644 --- a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts +++ b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts @@ -18,7 +18,7 @@ import { createTestShadowDom, getSample } from '../../test-utils'; import { rewriteDocLinks } from '../transformers'; describe('rewriteDocLinks', () => { - it('contains relative paths', () => { + it('should not do anything', () => { const shadowDom = createTestShadowDom(` Test Test @@ -34,7 +34,7 @@ describe('rewriteDocLinks', () => { ]); }); - it('contains transformed absolute paths', () => { + it('should transform a href with licalhost as baseUrl', () => { const shadowDom = createTestShadowDom( ` Test From 0ed5a1cd772351ce4d1e0b93ffffe257a17dc9c7 Mon Sep 17 00:00:00 2001 From: Sebastian Qvarfordt Date: Tue, 30 Jun 2020 13:22:10 +0200 Subject: [PATCH 21/27] Removed unused JSDOM dependency --- plugins/techdocs/package.json | 2 -- plugins/techdocs/src/test-utils/index.ts | 3 --- yarn.lock | 19 ------------------- 3 files changed, 24 deletions(-) diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index b50062f6ad..ca8e957a46 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -27,8 +27,6 @@ "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", - "@types/jsdom": "^16.2.3", - "jsdom": "^16.2.2", "react": "^16.13.1", "react-dom": "^16.13.1", "react-router": "^6.0.0-alpha.5", diff --git a/plugins/techdocs/src/test-utils/index.ts b/plugins/techdocs/src/test-utils/index.ts index 98472d75a4..f9ced5f930 100644 --- a/plugins/techdocs/src/test-utils/index.ts +++ b/plugins/techdocs/src/test-utils/index.ts @@ -14,9 +14,6 @@ * limitations under the License. */ -// @ts-ignore -import { JSDOM } from 'jsdom'; - import FIXTURE_STANDARD_PAGE from './fixtures/mkdocs-index'; import transformer from '../reader/transformers'; import type { Transformer } from '../reader/transformers'; diff --git a/yarn.lock b/yarn.lock index 9a2c389161..9b25df19cc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3555,15 +3555,6 @@ resolved "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.6.tgz#f1a1cb35aff47bc5cfb05cb0c441ca91e914c26f" integrity sha512-+oY0FDTO2GYKEV0YPvSshGq9t7YozVkgvXLty7zogQNuCxBhT9/3INX9Q7H1aRZ4SUDRXAKlJuA4EA5nTt7SNw== -"@types/jsdom@^16.2.3": - version "16.2.3" - resolved "https://registry.npmjs.org/@types/jsdom/-/jsdom-16.2.3.tgz#c6feadfe0836389b27f9c911cde82cd32e91c537" - integrity sha512-BREatezSn74rmLIDksuqGNFUTi9HNAWWQXYpFBFLK9U6wlMCO4M0QCa8CMpDsZQuqxSO9XifVLT5Q1P0vgKLqw== - dependencies: - "@types/node" "*" - "@types/parse5" "*" - "@types/tough-cookie" "*" - "@types/json-schema@*", "@types/json-schema@^7.0.3": version "7.0.4" resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339" @@ -3669,11 +3660,6 @@ resolved "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== -"@types/parse5@*": - version "5.0.3" - resolved "https://registry.npmjs.org/@types/parse5/-/parse5-5.0.3.tgz#e7b5aebbac150f8b5fdd4a46e7f0bd8e65e19109" - integrity sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw== - "@types/passport-github2@^1.2.4": version "1.2.4" resolved "https://registry.npmjs.org/@types/passport-github2/-/passport-github2-1.2.4.tgz#f56c386d1fe6435e359430e57adc1747a627bd86" @@ -3982,11 +3968,6 @@ dependencies: "@types/node" "*" -"@types/tough-cookie@*": - version "4.0.0" - resolved "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.0.tgz#fef1904e4668b6e5ecee60c52cc6a078ffa6697d" - integrity sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A== - "@types/uglify-js@*": version "3.0.4" resolved "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.0.4.tgz#96beae23df6f561862a830b4288a49e86baac082" From 6f5db82ef07471bdcbe65847580693e9e8cf9b5a Mon Sep 17 00:00:00 2001 From: Sebastian Qvarfordt Date: Tue, 30 Jun 2020 13:26:52 +0200 Subject: [PATCH 22/27] Removed argument to rewriteDocLinks --- plugins/techdocs/src/reader/components/Reader.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/plugins/techdocs/src/reader/components/Reader.tsx b/plugins/techdocs/src/reader/components/Reader.tsx index 1d7a85a6ba..12cec30c80 100644 --- a/plugins/techdocs/src/reader/components/Reader.tsx +++ b/plugins/techdocs/src/reader/components/Reader.tsx @@ -75,9 +75,7 @@ export const Reader = () => { componentId, path, }), - rewriteDocLinks({ - componentId, - }), + rewriteDocLinks(), modifyCss({ cssTransforms: { '.md-main__inner': [{ 'margin-top': '0' }], From 5e372b0671a864e4e5f5eb6232ae226a47c702cf Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 30 Jun 2020 14:52:06 +0200 Subject: [PATCH 23/27] chore(scaffolder): add tests for makeLogStream --- .../src/scaffolder/jobs/logger.test.ts | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 plugins/scaffolder-backend/src/scaffolder/jobs/logger.test.ts diff --git a/plugins/scaffolder-backend/src/scaffolder/jobs/logger.test.ts b/plugins/scaffolder-backend/src/scaffolder/jobs/logger.test.ts new file mode 100644 index 0000000000..5905d26ebb --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/jobs/logger.test.ts @@ -0,0 +1,48 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { makeLogStream } from './logger'; +describe('Logger', () => { + const mockMeta = { test: 'blob' }; + + it('should return empty log lines by default', async () => { + const { log } = makeLogStream(mockMeta); + + expect(log).toEqual([]); + }); + it('should add lines to the log when using the logger that is returned', async () => { + const { logger, log } = makeLogStream(mockMeta); + + logger.info('TEST LINE'); + logger.warn('WARN LINE'); + + const [first, second] = log; + expect(log.length).toBe(2); + + expect(first).toContain('info'); + expect(first).toContain('TEST LINE'); + + expect(second).toContain('warn'); + expect(second).toContain('WARN LINE'); + }); + + it('should add lines from writing to the stream that is returned', async () => { + const { stream, log } = makeLogStream(mockMeta); + const textLine = 'SOMETHING'; + stream.write(textLine); + + expect(log).toContain(textLine); + }); +}); From 2438aaa099341b3c4f42779f48db8c84478ea288 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 1 Jul 2020 10:10:04 +0200 Subject: [PATCH 24/27] feat(scaffolder): finally found a better name than storer. It's publisher --- .../src/scaffolder/index.ts | 3 +-- .../src/scaffolder/stages/index.ts | 18 +++++++++++++ .../scaffolder/stages/publish/github.test.ts | 16 ++++++++++++ .../stages/{store => publish}/github.ts | 22 ++++++++++------ .../src/scaffolder/stages/publish/index.ts | 16 ++++++++++++ .../stages/{store => publish}/types.ts | 17 +++++++------ .../scaffolder-backend/src/service/router.ts | 25 ++++++------------- 7 files changed, 84 insertions(+), 33 deletions(-) create mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/index.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts rename plugins/scaffolder-backend/src/scaffolder/stages/{store => publish}/github.ts (80%) create mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/publish/index.ts rename plugins/scaffolder-backend/src/scaffolder/stages/{store => publish}/types.ts (60%) diff --git a/plugins/scaffolder-backend/src/scaffolder/index.ts b/plugins/scaffolder-backend/src/scaffolder/index.ts index 2c689489e4..470bc230d6 100644 --- a/plugins/scaffolder-backend/src/scaffolder/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/index.ts @@ -13,6 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export * from './stages/templater'; -export * from './stages/prepare'; +export * from './stages'; export * from './jobs'; diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/index.ts b/plugins/scaffolder-backend/src/scaffolder/stages/index.ts new file mode 100644 index 0000000000..78ea30db79 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/stages/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './prepare'; +export * from './publish'; +export * from './templater'; diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts new file mode 100644 index 0000000000..f91e8bb40e --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +describe('Github Store', () => {}); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/store/github.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts similarity index 80% rename from plugins/scaffolder-backend/src/scaffolder/stages/store/github.ts rename to plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts index 8f5cc40ff9..9aed55fd98 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/store/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts @@ -14,25 +14,33 @@ * limitations under the License. */ -import { Storer } from './types'; +import { Publisher } from './types'; import { Octokit } from '@octokit/rest'; -import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; + import { JsonValue } from '@backstage/config'; import { RequiredTemplateValues } from '../templater'; import { Repository, Remote, Signature, Cred } from 'nodegit'; -export class GithubStorer implements Storer { +export class GithubPublisher implements Publisher { private client: Octokit; constructor({ client }: { client: Octokit }) { this.client = client; } - async createRemote({ + async publish({ values, + directory, }: { - entity: TemplateEntityV1alpha1; values: RequiredTemplateValues & Record; - }) { + directory: string; + }): Promise<{ remoteUrl: string }> { + const remoteUrl = await this.createRemote(values); + await this.pushToRemote(directory, remoteUrl); + + return { remoteUrl }; + } + + private async createRemote(values: RequiredTemplateValues) { const [owner, name] = values.storePath.split('/'); const { @@ -45,7 +53,7 @@ export class GithubStorer implements Storer { return cloneUrl; } - async pushToRemote(directory: string, remote: string): Promise { + private async pushToRemote(directory: string, remote: string): Promise { const repo = await Repository.init(directory, 0); const index = await repo.refreshIndex(); await index.addAll(); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/index.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/index.ts new file mode 100644 index 0000000000..dcfd2c9c34 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './github'; diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/store/types.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts similarity index 60% rename from plugins/scaffolder-backend/src/scaffolder/stages/store/types.ts rename to plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts index ae63a05197..a6db297bde 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/store/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/types.ts @@ -13,11 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { TemplateEntityV1alpha1 } from "@backstage/catalog-model"; -import { RequiredTemplateValues } from "../templater"; -import { JsonValue } from "@backstage/config"; +import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; +import { RequiredTemplateValues } from '../templater'; +import { JsonValue } from '@backstage/config'; -export type Storer = { - createRemote(opts: { entity: TemplateEntityV1alpha1, values: RequiredTemplateValues & Record}): Promise; - pushToRemote(directory: string, remote: string): Promise; -} +export type Publisher = { + publish(opts: { + entity: TemplateEntityV1alpha1; + values: RequiredTemplateValues & Record; + directory: string; + }): Promise<{ remoteUrl: string }>; +}; diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index efa2f9588d..633be88d4c 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -22,13 +22,14 @@ import { TemplaterBase, JobProcessor, RequiredTemplateValues, + StageContext, + GithubPublisher, } from '../scaffolder'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import Docker from 'dockerode'; import {} from '@backstage/backend-common'; -import { StageContext } from '../scaffolder/jobs/types'; import { Octokit } from '@octokit/rest'; -import { GithubStorer } from '../scaffolder/stages/store/github'; + import { JsonValue } from '@backstage/config'; export interface RouterOptions { preparers: PreparerBuilder; @@ -44,7 +45,7 @@ export async function createRouter( const githubClient = new Octokit({ auth: process.env.GITHUB_ACCESS_TOKEN }); const { preparers, templater, logger: parentLogger, dockerClient } = options; - const githubStorer = new GithubStorer({ client: githubClient }); + const githubPulisher = new GithubPublisher({ client: githubClient }); const logger = parentLogger.child({ plugin: 'scaffolder' }); const jobProcessor = new JobProcessor(); @@ -117,26 +118,16 @@ export async function createRouter( }, }, { - name: 'Create VCS Repo', + name: 'Publish template', handler: async (ctx: StageContext<{ resultDir: string }>) => { - ctx.logger.info('Should now create the VCS repo'); - const remoteUrl = await githubStorer.createRemote({ + ctx.logger.info('Should not store the template'); + const { remoteUrl } = await githubPulisher.publish({ values: ctx.values, - entity: ctx.entity, + directory: ctx.resultDir, }); - return { remoteUrl }; }, }, - { - name: 'Push to remote', - handler: async ( - ctx: StageContext<{ resultDir: string; remoteUrl: string }>, - ) => { - ctx.logger.info('Should now push to the remote'); - await githubStorer.pushToRemote(ctx.resultDir, ctx.remoteUrl); - }, - }, ], }); From cc4683d185bb5c139804ce2bce3ef7379720162a Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 1 Jul 2020 16:44:09 +0200 Subject: [PATCH 25/27] chore(scaffolder): making some tests nicer and a little more readable and typescript compliant --- plugins/scaffolder-backend/package.json | 1 + .../scaffolder/stages/prepare/github.test.ts | 1 - .../publish/__mocks__/@octokit/rest/index.ts | 28 ++++++++ .../stages/publish/__mocks__/nodegit/index.ts | 39 +++++++++++ .../scaffolder/stages/publish/github.test.ts | 68 ++++++++++++++++++- .../src/scaffolder/stages/publish/github.ts | 17 ++--- 6 files changed, 144 insertions(+), 10 deletions(-) create mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/publish/__mocks__/@octokit/rest/index.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/stages/publish/__mocks__/nodegit/index.ts diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index c6237bc726..30de3a8b6f 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -43,6 +43,7 @@ }, "devDependencies": { "@backstage/cli": "^0.1.1-alpha.12", + "@octokit/types": "^5.0.1", "@types/fs-extra": "^9.0.1", "@types/git-url-parse": "^9.0.0", "@types/nodegit": "0.26.5", diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts index 86377be6d0..f8552e063f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts @@ -19,7 +19,6 @@ const mocks = { CheckoutOptions: jest.fn(() => {}), }; jest.doMock('nodegit', () => mocks); -// require('nodegit'); import { GithubPreparer } from './github'; import { diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/__mocks__/@octokit/rest/index.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/__mocks__/@octokit/rest/index.ts new file mode 100644 index 0000000000..85bed3a223 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/__mocks__/@octokit/rest/index.ts @@ -0,0 +1,28 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const mockGithubClient = { + repos: { + createInOrg: jest.fn(), + createForAuthenticatedUser: jest.fn(), + }, +}; + +export class Octokit { + constructor() { + return mockGithubClient; + } +} diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/__mocks__/nodegit/index.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/__mocks__/nodegit/index.ts new file mode 100644 index 0000000000..aa0cb6a23b --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/__mocks__/nodegit/index.ts @@ -0,0 +1,39 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const mockIndex = { + addAll: jest.fn(), + write: jest.fn(), + writeTree: jest.fn().mockResolvedValue('mockoid'), +}; + +const mockRepo = { + refreshIndex: jest.fn().mockResolvedValue(mockIndex), + createCommit: jest.fn(), +}; + +const mockRemote = { + push: jest.fn(), +}; + +const Repository = { init: jest.fn().mockResolvedValue(mockRepo) }; +const Remote = { create: jest.fn().mockResolvedValue(mockRemote) }; +const Signature = { now: jest.fn() }; +const Cred = { + userpassPlaintextNew: jest.fn(), +}; + +export { Repository, Remote, Signature, Cred }; diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts index f91e8bb40e..aa439a4982 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts @@ -13,4 +13,70 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -describe('Github Store', () => {}); + +jest.mock('@octokit/rest'); +jest.mock('nodegit'); + +import { Octokit } from '@octokit/rest'; +import { OctokitResponse, ReposCreateInOrgResponseData } from '@octokit/types'; +import { GithubPublisher } from './github'; + +const { mockGithubClient } = require('@octokit/rest') as { + mockGithubClient: { repos: jest.Mocked }; +}; + +describe('Github Publisher', () => { + const publisher = new GithubPublisher({ client: new Octokit() }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('publish: createRemoteInGithub', () => { + it('should use octokit to create a repo in an organisation if the organisation property is set', async () => { + mockGithubClient.repos.createInOrg.mockResolvedValue({ + data: { + clone_url: 'mockclone', + }, + } as OctokitResponse); + + await publisher.publish({ + values: { + isOrg: true, + storePath: 'blam/test', + owner: 'bob', + }, + directory: '/tmp/test', + }); + + expect(mockGithubClient.repos.createInOrg).toHaveBeenCalledWith({ + org: 'blam', + name: 'test', + }); + }); + + it('should use octokit to create a repo in the authed user if the organisation property is not set', async () => { + mockGithubClient.repos.createForAuthenticatedUser.mockResolvedValue({ + data: { + clone_url: 'mockclone', + }, + } as OctokitResponse); + + await publisher.publish({ + values: { + storePath: 'blam/test', + owner: 'bob', + }, + directory: '/tmp/test', + }); + + expect( + mockGithubClient.repos.createForAuthenticatedUser, + ).toHaveBeenCalledWith({ + name: 'test', + }); + }); + }); + + describe('publish: createGitDirectory', () => {}); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts index 9aed55fd98..eba7def9af 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts @@ -40,17 +40,18 @@ export class GithubPublisher implements Publisher { return { remoteUrl }; } - private async createRemote(values: RequiredTemplateValues) { + private async createRemote( + values: RequiredTemplateValues & Record, + ) { const [owner, name] = values.storePath.split('/'); - const { - data: { clone_url: cloneUrl }, - } = await this.client.repos.createInOrg({ - name, - org: owner, - }); + const repoCreationPromise = values.isOrg + ? this.client.repos.createInOrg({ name, org: owner }) + : this.client.repos.createForAuthenticatedUser({ name }); - return cloneUrl; + const { data } = await repoCreationPromise; + + return data?.clone_url; } private async pushToRemote(directory: string, remote: string): Promise { From 365e5921b22348b3dccb959b78e9e8ee5ff49a6d Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 1 Jul 2020 18:30:53 +0200 Subject: [PATCH 26/27] chore(scaffolder): added some more tests for the github publisher --- .../stages/publish/__mocks__/nodegit/index.ts | 6 +- .../scaffolder/stages/publish/github.test.ts | 80 ++++++++++++++++++- 2 files changed, 82 insertions(+), 4 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/__mocks__/nodegit/index.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/__mocks__/nodegit/index.ts index aa0cb6a23b..e3fb5000d0 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/__mocks__/nodegit/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/__mocks__/nodegit/index.ts @@ -14,18 +14,18 @@ * limitations under the License. */ -const mockIndex = { +export const mockIndex = { addAll: jest.fn(), write: jest.fn(), writeTree: jest.fn().mockResolvedValue('mockoid'), }; -const mockRepo = { +export const mockRepo = { refreshIndex: jest.fn().mockResolvedValue(mockIndex), createCommit: jest.fn(), }; -const mockRemote = { +export const mockRemote = { push: jest.fn(), }; diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts index aa439a4982..5e886b13aa 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts @@ -18,6 +18,7 @@ jest.mock('@octokit/rest'); jest.mock('nodegit'); import { Octokit } from '@octokit/rest'; +import * as NodeGit from 'nodegit'; import { OctokitResponse, ReposCreateInOrgResponseData } from '@octokit/types'; import { GithubPublisher } from './github'; @@ -25,6 +26,16 @@ const { mockGithubClient } = require('@octokit/rest') as { mockGithubClient: { repos: jest.Mocked }; }; +const { Repository, mockRepo, mockIndex, Signature } = require('nodegit') as { + Repository: jest.Mocked<{ init: any }>; + Signature: jest.Mocked<{ now: any }>; + Cred: jest.Mocked<{ init: any }>; + Remote: jest.Mocked<{ push: any }>; + + mockIndex: jest.Mocked; + mockRepo: jest.Mocked; +}; + describe('Github Publisher', () => { const publisher = new GithubPublisher({ client: new Octokit() }); @@ -78,5 +89,72 @@ describe('Github Publisher', () => { }); }); - describe('publish: createGitDirectory', () => {}); + describe('publish: createGitDirectory', () => { + const values = { + isOrg: true, + storePath: 'blam/test', + owner: 'lols', + }; + + const mockDir = '/tmp/test/dir'; + + mockGithubClient.repos.createInOrg.mockResolvedValue({ + data: { + clone_url: 'mockclone', + }, + } as OctokitResponse); + it('should call init on the repo with the directory', async () => { + await publisher.publish({ + values, + directory: mockDir, + }); + + expect(Repository.init).toHaveBeenCalledWith(mockDir, 0); + }); + + it('should call refresh index on the index and write the new files', async () => { + await publisher.publish({ + values, + directory: mockDir, + }); + + expect(mockRepo.refreshIndex).toHaveBeenCalled(); + }); + + it('should call add all files and write', async () => { + await publisher.publish({ + values, + directory: mockDir, + }); + + expect(mockIndex.addAll).toHaveBeenCalled(); + expect(mockIndex.write).toHaveBeenCalled(); + expect(mockIndex.writeTree).toHaveBeenCalled(); + }); + + it('should create a commit with on head with the right name and commiter', async () => { + const mockSignature = { mockSignature: 'bloblly' }; + Signature.now.mockReturnValue(mockSignature); + + await publisher.publish({ + values, + directory: mockDir, + }); + + expect(Signature.now).toHaveBeenCalledTimes(2); + expect(Signature.now).toHaveBeenCalledWith( + 'Scaffolder', + 'scaffolder@backstage.io', + ); + + expect(mockRepo.createCommit).toHaveBeenCalledWith( + 'HEAD', + mockSignature, + mockSignature, + 'initial commit', + 'mockoid', + [], + ); + }); + }); }); From dcf062f9bf1feffc9550a9872ab9a293f0226ea5 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 1 Jul 2020 19:06:14 +0200 Subject: [PATCH 27/27] chore(scaffolder): finally fixed tests --- .../scaffolder/stages/publish/github.test.ts | 49 +++++++++++++++++-- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts index 5e886b13aa..164ec316df 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts @@ -26,14 +26,23 @@ const { mockGithubClient } = require('@octokit/rest') as { mockGithubClient: { repos: jest.Mocked }; }; -const { Repository, mockRepo, mockIndex, Signature } = require('nodegit') as { +const { + Repository, + mockRepo, + mockIndex, + Signature, + Remote, + mockRemote, + Cred, +} = require('nodegit') as { Repository: jest.Mocked<{ init: any }>; Signature: jest.Mocked<{ now: any }>; - Cred: jest.Mocked<{ init: any }>; - Remote: jest.Mocked<{ push: any }>; + Cred: jest.Mocked<{ userpassPlaintextNew: any }>; + Remote: jest.Mocked<{ create: any }>; mockIndex: jest.Mocked; mockRepo: jest.Mocked; + mockRemote: jest.Mocked; }; describe('Github Publisher', () => { @@ -156,5 +165,39 @@ describe('Github Publisher', () => { [], ); }); + + it('creates a remote with the repo and remote', async () => { + await publisher.publish({ + values, + directory: mockDir, + }); + + expect(Remote.create).toHaveBeenCalledWith( + mockRepo, + 'origin', + 'mockclone', + ); + }); + + it('shoud push to the remote repo', async () => { + await publisher.publish({ + values, + directory: mockDir, + }); + + const [remotes, { callbacks }] = mockRemote.push.mock + .calls[0] as NodeGit.PushOptions[]; + + expect(remotes).toEqual(['refs/heads/master:refs/heads/master']); + + process.env.GITHUb_ACCESS_TOKEN = 'blob'; + + callbacks?.credentials?.(); + + expect(Cred.userpassPlaintextNew).toHaveBeenCalledWith( + process.env.GITHUB_ACCESS_TOKEN, + 'x-oauth-basic', + ); + }); }); });