Git https auth round trip (#1720)
* feature/JENKINS-47247-https-roundtrip-2 * More WIP, might have to undo some of these in a new branch * feature/JENKINS-47247-https-roundtrip-3 * WIP, breaking at a good resumable point :) * feature/JENKINS-47247-https-roundtrip-3 * Successful round trip. Hacky, but successful * feature/JENKINS-47247-https-roundtrip-3 * WIP, need to switch branches * feature/JENKINS-47247-https-roundtrip-3 * Good steps towards bringing in the bb un/pw code for git * feature/JENKINS-47247-https-roundtrip-3 * Committing this non-compiling tree so I can share it with TSC team * feature/JENKINS-47247-https-roundtrip-3 * fix tsify version to 3.0.2 for support for case-sensitivity in TSC * feature/JENKINS-47247-https-roundtrip-3 * WIP need to do some experiments * feature/JENKINS-47247-https-roundtrip-3 import issues squashed * feature/JENKINS-47247-https-roundtrip-3 * more good progress, need to run some tests on master * feature/JENKINS-47247-https-roundtrip-3 * Still have a couple of loose ends in the editor, need to rebase * feature/JENKINS-47247-https-roundtrip-3 * Seems to be all working, now time to rebase and clean up * feature/JENKINS-47247-https-roundtrip-3 * Some cleanup * feature/JENKINS-47247-https-roundtrip-3 * Fix an issue exposed by GitUtilsTest * feature/JENKINS-47247-https-roundtrip-3 * Fix some test failures caused by my lazy debugging code :) * feature/JENKINS-47247-https-roundtrip-3 * Add some tests to GitSCM enough to please coverage nazi, and fix a small clientside issue I noticed during debugging * feature/JENKINS-47247-https-roundtrip-3 * Clean up some debugging, squash some TODOs * feature/JENKINS-47247-https-roundtrip-3 * Moar cleanup * feature/JENKINS-47247-https-roundtrip-3 * Add more unit tests, clean up some things * feature/JENKINS-47247-https-roundtrip-3 * Add a typedef for TypedError since it can't be TS, and do some more cleanup and docs * feature/JENKINS-47247-https-roundtrip-3 * Clean up some more TODOs and neaten some promise handling code * feature/JENKINS-47247-https-roundtrip-3 * Upgrade tsify to 4.0 * feature/JENKINS-47247-https-roundtrip-3 * Add a unit test for GitPWCredentialsManager * feature/JENKINS-47247-https-roundtrip-3 * Convert TypedError back to TypeScript again now I figured out how to fix the prototype chain * feature/JENKINS-47247-https-roundtrip-3 * Fix up ATH test for git creation flow, and fix a typo in a button style * feature/JENKINS-47247-https-roundtrip-3 * needs serious refucktoring. Compiles again but he ded. * feature/JENKINS-47247-https-roundtrip-3 * Add a couple of cases to GitScm test, improve error reporting a bit * feature/JENKINS-47247-https-roundtrip-3 * Move existingCredential into state obj, update tests * feature/JENKINS-47247-https-roundtrip-3 * Clean up a couple of issues and debounce checking existing creds within the component * feature/JENKINS-47247-https-roundtrip-3 * for some reason a test was relying on this not returning useful error info. I dunno. * feature/JENKINS-47247-https-roundtrip-3 * Change debounce * feature/JENKINS-47247-https-roundtrip-3 * Re-install tsify@4 to update shrinkwrap * feature/JENKINS-47247-https-roundtrip-3 * Explictly optionalise params to UrlBuilder.buildRestUrl * feature/JENKINS-47247-https-roundtrip-3 * Change generated credentialId to use a hash instead of raw normalized repo url * feature/JENKINS-47247-https-roundtrip-3 * Fix the blinking issue by doing some more sensible comparisons on props and selections * feature/JENKINS-47247-https-roundtrip-3 * Remove the transition slider because it doesn't make sense in this context, and clean up some more stuff * feature/JENKINS-47247-https-roundtrip-3 * Add some type annotations * feature/JENKINS-47247-https-roundtrip-3 * strip host/query/fragment from repo url during normalization * feature/JENKINS-47247-https-roundtrip-3 * Tweak icon behaviour on create credential button
This commit is contained in:
parent
d5b008d93b
commit
eeebe5087d
|
@ -1855,6 +1855,15 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.1.tgz",
|
||||
"integrity": "sha1-YuIA8DmVWmgQ2N8KM//A8BNmLZg=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"safe-buffer": "5.0.1"
|
||||
}
|
||||
},
|
||||
"string-width": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
|
||||
|
@ -1866,15 +1875,6 @@
|
|||
"strip-ansi": "3.0.1"
|
||||
}
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.1.tgz",
|
||||
"integrity": "sha1-YuIA8DmVWmgQ2N8KM//A8BNmLZg=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"safe-buffer": "5.0.1"
|
||||
}
|
||||
},
|
||||
"stringstream": {
|
||||
"version": "0.0.5",
|
||||
"resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz",
|
||||
|
@ -4430,6 +4430,11 @@
|
|||
"integrity": "sha1-pB6tGm1ggc63n2WwYZAbbY89HQ8=",
|
||||
"dev": true
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
|
||||
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
|
||||
"dev": true
|
||||
},
|
||||
"string-width": {
|
||||
"version": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
|
||||
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
|
||||
|
@ -4450,11 +4455,6 @@
|
|||
"function-bind": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz"
|
||||
}
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
|
||||
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
|
||||
"dev": true
|
||||
},
|
||||
"stringstream": {
|
||||
"version": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz",
|
||||
"integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=",
|
||||
|
|
|
@ -49,7 +49,7 @@ while true; do
|
|||
done
|
||||
|
||||
|
||||
EXECUTION="env JENKINS_JAVA_OPTS=\"${JENKINS_JAVA_OPTS}\" ${ATH_SERVER_HOST} ${ATH_SERVER_PORT} BROWSER=phantomjs LOCAL_SNAPSHOTS=${LOCAL_SNAPSHOTS} ${PLUGINS} PLUGINS_DIR=../runtime-plugins/runtime-deps/target/plugins-combined PATH=./node:./node/npm/bin:./node_modules/.bin:${PATH} mvn -Dhudson.model.UsageStatistics.disabled=true -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn -B -Dmaven.test.failure.ignore ${MAVEN_SETTINGS} test ${PROFILES} ${TEST_TO_RUN}"
|
||||
EXECUTION="env JENKINS_JAVA_OPTS=\"${JENKINS_JAVA_OPTS}\" ${ATH_SERVER_HOST} ${ATH_SERVER_PORT} BROWSER=phantomjs LOCAL_SNAPSHOTS=${LOCAL_SNAPSHOTS} ${PLUGINS} PLUGINS_DIR=../runtime-plugins/runtime-deps/target/plugins-combined PATH=\"./node:./node/npm/bin:./node_modules/.bin:${PATH}\" mvn -Dhudson.model.UsageStatistics.disabled=true -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn -B -Dmaven.test.failure.ignore ${MAVEN_SETTINGS} test ${PROFILES} ${TEST_TO_RUN}"
|
||||
|
||||
echo ""
|
||||
echo "> ${EXECUTION}"
|
||||
|
|
|
@ -47,27 +47,18 @@ public class GitCreationPage {
|
|||
return this;
|
||||
}
|
||||
|
||||
public MultiBranchPipeline createPipeline(SSEClientRule sseCLient, String pipelineName, String url, String sshPrivateKey, String user, String pass) throws IOException {
|
||||
public MultiBranchPipeline createPipelineSSH(SSEClientRule sseCLient, String pipelineName, String url, String sshPrivateKey) throws IOException {
|
||||
jobApi.deletePipeline(pipelineName);
|
||||
dashboardPage.clickNewPipelineBtn();
|
||||
clickGitCreationOption();
|
||||
wait.until(By.cssSelector("div.text-repository-url input")).sendKeys(url);
|
||||
wait.until(By.cssSelector("button.button-create-credential")).click();
|
||||
|
||||
if(!Strings.isNullOrEmpty(sshPrivateKey)) {
|
||||
wait.until(By.xpath("//span[text()='SSH Key']")).click();
|
||||
wait.until(By.cssSelector("textarea.TextArea-control")).sendKeys(sshPrivateKey);
|
||||
wait.until(By.cssSelector(".Dialog-button-bar button.button-create-credental")).click();
|
||||
wait.until(ExpectedConditions.invisibilityOfElementLocated(By.cssSelector(".create-credential-dialog")));
|
||||
logger.info("Created credential");
|
||||
} else if(!Strings.isNullOrEmpty(user) && !Strings.isNullOrEmpty(pass)) {
|
||||
wait.until(By.xpath("//span[text()='Username & Password']")).click();
|
||||
wait.until(By.cssSelector("div.text-username input")).sendKeys(user);
|
||||
wait.until(By.cssSelector("div.text-password input")).sendKeys(pass);
|
||||
wait.until(By.cssSelector(".Dialog-button-bar button.button-create-credental")).click();
|
||||
wait.until(ExpectedConditions.invisibilityOfElementLocated(By.cssSelector(".create-credential-dialog")));
|
||||
logger.info("Created user/pass credential");
|
||||
}
|
||||
wait.until(By.xpath("//span[text()='SSH Key']")).click();
|
||||
wait.until(By.cssSelector("textarea.TextArea-control")).sendKeys(sshPrivateKey);
|
||||
wait.until(By.cssSelector(".Dialog-button-bar button.button-create-credential")).click();
|
||||
wait.until(ExpectedConditions.invisibilityOfElementLocated(By.cssSelector(".create-credential-dialog")));
|
||||
logger.info("Created credential");
|
||||
|
||||
wait.until(By.cssSelector(".button-create-pipeline")).click();
|
||||
logger.info("Click create pipeline button");
|
||||
|
@ -79,4 +70,30 @@ public class GitCreationPage {
|
|||
pipeline.getActivityPage().checkUrl();
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
public MultiBranchPipeline createPipelinePW(SSEClientRule sseCLient, String pipelineName, String url, String user, String pass) throws IOException {
|
||||
jobApi.deletePipeline(pipelineName);
|
||||
dashboardPage.clickNewPipelineBtn();
|
||||
clickGitCreationOption();
|
||||
wait.until(By.cssSelector("div.text-repository-url input")).sendKeys(url);
|
||||
|
||||
wait.until(By.xpath("//*[contains(text(), 'Jenkins needs a user credential')]"));
|
||||
|
||||
wait.until(By.cssSelector("div.text-username input")).sendKeys(user);
|
||||
wait.until(By.cssSelector("div.text-password input")).sendKeys(pass);
|
||||
wait.until(By.cssSelector(".button-create-credential")).click();
|
||||
|
||||
wait.until(By.xpath("//*[contains(text(), 'Use existing credential')]"));
|
||||
|
||||
logger.info("Created user/pass credential");
|
||||
wait.until(By.cssSelector(".button-create-pipeline")).click();
|
||||
logger.info("Click create pipeline button");
|
||||
|
||||
MultiBranchPipeline pipeline = multiBranchPipelineFactory.pipeline(pipelineName);
|
||||
wait.until(ExpectedConditions.urlContains(pipeline.getUrl() + "/activity"), 30000);
|
||||
sseCLient.untilEvents(SSEEvents.activityComplete(pipeline.getName()));
|
||||
driver.navigate().refresh();
|
||||
pipeline.getActivityPage().checkUrl();
|
||||
return pipeline;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,8 @@ import java.util.Properties;
|
|||
public class GitCreationTest extends BlueOceanAcceptanceTest {
|
||||
private Logger logger = Logger.getLogger(GitCreationTest.class);
|
||||
|
||||
@Inject @Named("live")
|
||||
@Inject
|
||||
@Named("live")
|
||||
Properties liveProperties;
|
||||
|
||||
@Inject
|
||||
|
@ -41,7 +42,8 @@ public class GitCreationTest extends BlueOceanAcceptanceTest {
|
|||
@Inject
|
||||
WaitUtil wait;
|
||||
|
||||
@Inject @Rule
|
||||
@Inject
|
||||
@Rule
|
||||
public SSEClientRule sseClient;
|
||||
|
||||
@Test
|
||||
|
@ -56,7 +58,7 @@ public class GitCreationTest extends BlueOceanAcceptanceTest {
|
|||
Assert.assertNotNull(pipelineName);
|
||||
logger.info("PipelineNameHttps: " + pipelineName);
|
||||
logger.info("git repo - " + gitUrl);
|
||||
AbstractPipeline pipeline = gitCreationPage.createPipeline(sseClient, pipelineName, gitUrl, null, user, pass);
|
||||
AbstractPipeline pipeline = gitCreationPage.createPipelinePW(sseClient, pipelineName, gitUrl, user, pass);
|
||||
pipeline.getActivityPage().testNumberRunsComplete(1);
|
||||
}
|
||||
|
||||
|
@ -73,10 +75,7 @@ public class GitCreationTest extends BlueOceanAcceptanceTest {
|
|||
logger.info("git repo - " + gitUrl);
|
||||
String key = IOUtils.toString(new FileInputStream(privateKeyFile));
|
||||
|
||||
MultiBranchPipeline pipeline = gitCreationPage.createPipeline(sseClient, pipelineName, gitUrl, key, null, null);
|
||||
MultiBranchPipeline pipeline = gitCreationPage.createPipelineSSH(sseClient, pipelineName, gitUrl, key);
|
||||
pipeline.getActivityPage().testNumberRunsComplete(1);
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -90,8 +90,7 @@ public class BitbucketServerTest implements WebDriverMixin {
|
|||
wait.click(By.cssSelector(".button-next-step"));
|
||||
wait.sendKeys(By.cssSelector(".text-username input"),"admin");
|
||||
wait.sendKeys(By.cssSelector(".text-password input"),"admin");
|
||||
// TODO: fix spelling of credental to credential
|
||||
wait.click(By.cssSelector(".button-create-credental"));
|
||||
wait.click(By.cssSelector(".button-create-credential"));
|
||||
LOGGER.info("Bitbucket server created successfully");
|
||||
// Select project
|
||||
creationPage.selectOrganization(BB_PROJECT_NAME);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
"use strict";
|
||||
'use strict';
|
||||
|
||||
process.env.SKIP_BLUE_IMPORTS = 'YES';
|
||||
|
||||
|
@ -20,84 +20,85 @@ const tsProject = ts.createProject('./tsconfig.json');
|
|||
|
||||
const config = {
|
||||
react: {
|
||||
sources: ["src/**/*.{js,jsx}", "!**/__mocks__/**"],
|
||||
dest: "dist"
|
||||
sources: ['src/**/*.{js,jsx}', '!**/__mocks__/**'],
|
||||
dest: 'dist',
|
||||
},
|
||||
ts: {
|
||||
sources: ["src/**/*.{ts,tsx}"],
|
||||
dest: "dist",
|
||||
destBundle: "target/tstemp"
|
||||
sources: ['src/**/*.{ts,tsx}'],
|
||||
dest: 'dist',
|
||||
destBundle: 'target/tstemp',
|
||||
},
|
||||
less: {
|
||||
sources: "src/less/core.less",
|
||||
sources: 'src/less/core.less',
|
||||
watch: 'src/less/**/*.{less,css}',
|
||||
dest: "dist/assets/css",
|
||||
dest: 'dist/assets/css',
|
||||
},
|
||||
copy: {
|
||||
less_assets: {
|
||||
sources: "src/less/**/*.svg",
|
||||
dest: "dist/assets/css"
|
||||
sources: 'src/less/**/*.svg',
|
||||
dest: 'dist/assets/css',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
// Watch all
|
||||
|
||||
gulp.task("watch", ["build"], () => {
|
||||
gulp.watch(config.react.sources, ["compile-react"]);
|
||||
gulp.watch(config.less.watch, ["less"]);
|
||||
gulp.task('watch', ['build'], () => {
|
||||
gulp.watch(config.react.sources, ['compile-react']);
|
||||
gulp.watch(config.less.watch, ['less']);
|
||||
});
|
||||
|
||||
// Default to all
|
||||
|
||||
gulp.task("default", ["validate"]);
|
||||
gulp.task('default', ['validate']);
|
||||
|
||||
// Build all
|
||||
|
||||
gulp.task("build", ["compile-typescript", "compile-react", "less", "copy"]);
|
||||
gulp.task('build', ['compile-typescript', 'compile-react', 'less', 'copy']);
|
||||
|
||||
// Compile react sources
|
||||
|
||||
gulp.task("compile-react", () =>
|
||||
gulp.src(config.react.sources)
|
||||
gulp.task('compile-react', () =>
|
||||
gulp
|
||||
.src(config.react.sources)
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(babel(config.react.babel))
|
||||
.pipe(sourcemaps.write("."))
|
||||
.pipe(gulp.dest(config.react.dest)));
|
||||
.pipe(sourcemaps.write('.'))
|
||||
.pipe(gulp.dest(config.react.dest))
|
||||
);
|
||||
|
||||
gulp.task("compile-typescript", () =>
|
||||
gulp.src(config.ts.sources)
|
||||
gulp.task('compile-typescript', () =>
|
||||
gulp
|
||||
.src(config.ts.sources)
|
||||
.pipe(tsProject())
|
||||
.pipe(gulp.dest(config.ts.dest)));
|
||||
|
||||
gulp.task('copy-src', () =>
|
||||
gulp.src("src/js/**/*")
|
||||
.pipe(gulp.dest(config.ts.destBundle+'/js')));
|
||||
gulp.task("compile-typescript-bundle", ['copy-src'], () =>
|
||||
gulp.src(config.ts.sources)
|
||||
.pipe(tsProject())
|
||||
.pipe(gulp.dest(config.ts.destBundle)));
|
||||
.pipe(gulp.dest(config.ts.dest))
|
||||
);
|
||||
|
||||
gulp.task("less", () =>
|
||||
gulp.src(config.less.sources)
|
||||
gulp.task('copy-src', () => gulp.src('src/js/**/*').pipe(gulp.dest(config.ts.destBundle + '/js')));
|
||||
gulp.task('compile-typescript-bundle', ['copy-src'], () =>
|
||||
gulp
|
||||
.src(config.ts.sources)
|
||||
.pipe(tsProject())
|
||||
.pipe(gulp.dest(config.ts.destBundle))
|
||||
);
|
||||
|
||||
gulp.task('less', () =>
|
||||
gulp
|
||||
.src(config.less.sources)
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(less())
|
||||
.pipe(rename("blueocean-core-js.css"))
|
||||
.pipe(sourcemaps.write("."))
|
||||
.pipe(gulp.dest(config.less.dest)));
|
||||
.pipe(rename('blueocean-core-js.css'))
|
||||
.pipe(sourcemaps.write('.'))
|
||||
.pipe(gulp.dest(config.less.dest))
|
||||
);
|
||||
|
||||
gulp.task("copy", ["copy-less-assets"]);
|
||||
gulp.task('copy', ['copy-less-assets']);
|
||||
|
||||
gulp.task("copy-less-assets", () =>
|
||||
gulp.src(config.copy.less_assets.sources)
|
||||
.pipe(copy(config.copy.less_assets.dest, { prefix: 2 })));
|
||||
gulp.task('copy-less-assets', () => gulp.src(config.copy.less_assets.sources).pipe(copy(config.copy.less_assets.dest, { prefix: 2 })));
|
||||
|
||||
// Validate contents
|
||||
gulp.task("validate", ["lint", "test"], () => {
|
||||
const paths = [
|
||||
config.react.dest,
|
||||
];
|
||||
gulp.task('validate', ['lint', 'test'], () => {
|
||||
const paths = [config.react.dest];
|
||||
|
||||
for (const path of paths) {
|
||||
try {
|
||||
|
@ -109,29 +110,27 @@ gulp.task("validate", ["lint", "test"], () => {
|
|||
}
|
||||
});
|
||||
|
||||
|
||||
var builder = require('@jenkins-cd/js-builder');
|
||||
|
||||
builder.src([
|
||||
config.ts.destBundle,
|
||||
'less']);
|
||||
builder.src([config.ts.destBundle, 'less']);
|
||||
|
||||
//
|
||||
// Create the main bundle.
|
||||
//
|
||||
|
||||
builder.bundle('target/tstemp/js/index.js', 'blueocean-core-js.js')
|
||||
builder
|
||||
.bundle('target/tstemp/js/index.js', 'blueocean-core-js.js')
|
||||
.onStartup('./target/tstemp/js/bundleStartup.js')
|
||||
.inDir('target/classes/io/jenkins/blueocean')
|
||||
.less('src/less/blueocean-core-js.less')
|
||||
.import('react@any', {
|
||||
aliases: ['react/lib/React'] // in case a module requires react through the back door
|
||||
aliases: ['react/lib/React'], // in case a module requires react through the back door
|
||||
})
|
||||
.import('react-dom@any')
|
||||
.import("react-router@any")
|
||||
.export("@jenkins-cd/js-extensions")
|
||||
.export("@jenkins-cd/logging")
|
||||
.import('react-router@any')
|
||||
.export('@jenkins-cd/js-extensions')
|
||||
.export('@jenkins-cd/logging')
|
||||
.export('mobx');
|
||||
|
||||
//megaultrahax
|
||||
gulp.tasks['js_bundle_blueocean-core-js_bundle_1'].dep=['compile-typescript-bundle'];
|
||||
gulp.tasks['js_bundle_blueocean-core-js_bundle_1'].dep = ['compile-typescript-bundle'];
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -96,7 +96,7 @@
|
|||
"run-sequence": "1.2.2",
|
||||
"sinon": "1.17.6",
|
||||
"ts-jest": "19.0.2",
|
||||
"tsify": "3.0.4",
|
||||
"tsify": "4.0.0",
|
||||
"typescript": "2.7.2",
|
||||
"watchify": "3.7.0"
|
||||
},
|
||||
|
|
|
@ -280,7 +280,7 @@ export function toClassicJobPage(currentPageUrl, isMultibranch = false) {
|
|||
* @param runId (optional) identifies an individual run
|
||||
* @returns a URL string
|
||||
*/
|
||||
export function buildRestUrl(organizationName, pipelineFullName, branchName, runId) {
|
||||
export function buildRestUrl(organizationName: string, pipelineFullName?: string, branchName?: string, runId?: string) {
|
||||
const jenkinsUrl = AppConfig.getJenkinsRootURL();
|
||||
let url = `${jenkinsUrl}/blue/rest/organizations/${encodeURIComponent(organizationName)}`;
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ export function execute(done, config) {
|
|||
// have a special handling for core-js, must load the web bundle...
|
||||
const Extensions = require('@jenkins-cd/js-extensions');
|
||||
const appRoot = document.getElementsByTagName('head')[0].getAttribute('data-appurl');
|
||||
|
||||
|
||||
Extensions.init({
|
||||
extensionData: window.$blueocean.jsExtensions,
|
||||
classMetadataProvider: (type, cb) => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
import { LiveStatusIndicator as LiveStatusIndicatorJdl } from '@jenkins-cd/design-language';
|
||||
import { TimeHarmonizer} from '../components/TimeHarmonizer';
|
||||
import { TimeHarmonizer } from '../components/TimeHarmonizer';
|
||||
import { logging } from '../logging';
|
||||
const logger = logging.logger('io.jenkins.blueocean.core.LiveStatusIndicator');
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { RestPaths as rest } from './rest';
|
||||
export const Paths = {
|
||||
rest
|
||||
rest,
|
||||
};
|
||||
|
|
|
@ -8,7 +8,6 @@ import { assert } from 'chai';
|
|||
import { Utils } from '../../src/js/utils';
|
||||
import { User } from '../../src/js/User';
|
||||
|
||||
|
||||
describe('User', () => {
|
||||
describe('permissions', () => {
|
||||
it('User has pipeline permissions', () => {
|
||||
|
|
|
@ -5,10 +5,8 @@ import { shallow } from 'enzyme';
|
|||
import { Utils } from '../../../src/js/utils';
|
||||
import { ReplayButton } from '../../../src/js/components/ReplayButton';
|
||||
|
||||
|
||||
jest.mock('../../../src/js/i18n/i18n');
|
||||
|
||||
|
||||
describe('ReplayButton', () => {
|
||||
let pipeline;
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@ import { Utils } from '../../../src/js/utils';
|
|||
jest.unmock('../../../src/js/index');
|
||||
jest.mock('../../../src/js/i18n/i18n');
|
||||
|
||||
|
||||
import { RunButton } from '../../../src/js/components/RunButton';
|
||||
|
||||
describe('RunButton', () => {
|
||||
|
|
|
@ -7,29 +7,18 @@ import WithContext from '@jenkins-cd/design-language/dist/js/stories/WithContext
|
|||
import moment from 'moment';
|
||||
import { TimeHarmonizer, TimeHarmonizerUtil } from '../../../src/js/components/TimeHarmonizer';
|
||||
|
||||
|
||||
jest.mock('../../../src/js/i18n/i18n');
|
||||
|
||||
@TimeHarmonizer
|
||||
class UselessComponent extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
isRunning,
|
||||
startTime,
|
||||
endTime,
|
||||
durationInMillis,
|
||||
getTimes,
|
||||
getDuration,
|
||||
getI18nTitle,
|
||||
result,
|
||||
} = this.props;
|
||||
const { isRunning, startTime, endTime, durationInMillis, getTimes, getDuration, getI18nTitle, result } = this.props;
|
||||
|
||||
const processedTimes = getTimes({
|
||||
startTime,
|
||||
endTime,
|
||||
durationInMillis,
|
||||
result
|
||||
result,
|
||||
});
|
||||
|
||||
return (
|
||||
|
@ -37,37 +26,35 @@ class UselessComponent extends Component {
|
|||
<h3>Input</h3>
|
||||
<dl>
|
||||
<dt>startTime</dt>
|
||||
<dd className="in-startTime">{ String(startTime) }</dd>
|
||||
<dd className="in-startTime">{String(startTime)}</dd>
|
||||
<dt>endTime</dt>
|
||||
<dd className="in-endTime">{ String(endTime) }</dd>
|
||||
<dd className="in-endTime">{String(endTime)}</dd>
|
||||
<dt>durationInMillis</dt>
|
||||
<dd className="in-durationInMillis">{ String(durationInMillis) }</dd>
|
||||
<dd className="in-durationInMillis">{String(durationInMillis)}</dd>
|
||||
<dt>isRunning</dt>
|
||||
<dd className="in-isRunning">{ String(isRunning) }</dd>
|
||||
<dd className="in-isRunning">{String(isRunning)}</dd>
|
||||
</dl>
|
||||
<h3>Synced</h3>
|
||||
<dl>
|
||||
<dt>startTime</dt>
|
||||
<dd className="syn-startTime">{ String(processedTimes.startTime) }</dd>
|
||||
<dd className="syn-startTime">{String(processedTimes.startTime)}</dd>
|
||||
<dt>endTime</dt>
|
||||
<dd className="syn-endTime">{ String(processedTimes.endTime) }</dd>
|
||||
<dd className="syn-endTime">{String(processedTimes.endTime)}</dd>
|
||||
<dt>durationInMillis</dt>
|
||||
<dd className="syn-durationInMillis">{ String(processedTimes.durationInMillis) }</dd>
|
||||
<dd className="syn-durationInMillis">{String(processedTimes.durationInMillis)}</dd>
|
||||
<dt>getDuration</dt>
|
||||
<dd className="syn-getDuration">{ String(getDuration()) }</dd>
|
||||
<dd className="syn-getDuration">{String(getDuration())}</dd>
|
||||
<dt>getI18nTitle</dt>
|
||||
<dd className="syn-getI18nTitle">{ String(getI18nTitle(result)) }</dd>
|
||||
<dd className="syn-getI18nTitle">{String(getI18nTitle(result))}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
describe('TimeHarmonizer', () => {
|
||||
it('/ renders', () => {
|
||||
const wrapper = shallow(
|
||||
<UselessComponent/>
|
||||
);
|
||||
const wrapper = shallow(<UselessComponent />);
|
||||
});
|
||||
|
||||
const time1Europe = '2017-01-25T10:28:34.755+0100';
|
||||
|
@ -90,9 +77,7 @@ describe('TimeHarmonizer', () => {
|
|||
TimeHarmonizerUtil.timeManager.currentTime = oldClock;
|
||||
});
|
||||
|
||||
|
||||
it('/ renders with time props', () => {
|
||||
|
||||
TimeHarmonizerUtil.timeManager.currentTime = () => moment(time1ZuluPlus5m);
|
||||
|
||||
let timeRelatedProps = {
|
||||
|
@ -103,7 +88,7 @@ describe('TimeHarmonizer', () => {
|
|||
result: 'running',
|
||||
};
|
||||
|
||||
let wrapper = mount(<UselessComponent {...timeRelatedProps}/>);
|
||||
let wrapper = mount(<UselessComponent {...timeRelatedProps} />);
|
||||
|
||||
// Input
|
||||
|
||||
|
@ -138,15 +123,14 @@ describe('TimeHarmonizer', () => {
|
|||
});
|
||||
|
||||
it('/ renders with time props and context without drift', () => {
|
||||
|
||||
TimeHarmonizerUtil.timeManager.currentTime = () => moment(time2ZuluPlus10m);
|
||||
|
||||
let ctx = {
|
||||
config: {
|
||||
getServerBrowserTimeSkewMillis: () => {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let timeRelatedProps = {
|
||||
|
@ -159,7 +143,7 @@ describe('TimeHarmonizer', () => {
|
|||
|
||||
let wrapper = mount(
|
||||
<WithContext context={ctx}>
|
||||
<UselessComponent {...timeRelatedProps}/>
|
||||
<UselessComponent {...timeRelatedProps} />
|
||||
</WithContext>
|
||||
);
|
||||
|
||||
|
@ -196,15 +180,14 @@ describe('TimeHarmonizer', () => {
|
|||
});
|
||||
|
||||
it('/ renders with time props and context with drift and endTime', () => {
|
||||
|
||||
TimeHarmonizerUtil.timeManager.currentTime = () => moment(time2ZuluPlus10m);
|
||||
|
||||
let ctx = {
|
||||
config: {
|
||||
getServerBrowserTimeSkewMillis: () => {
|
||||
return -5 * 60 * 60 * 1000; // -5 hours
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let timeRelatedProps = {
|
||||
|
@ -217,7 +200,7 @@ describe('TimeHarmonizer', () => {
|
|||
|
||||
let wrapper = mount(
|
||||
<WithContext context={ctx}>
|
||||
<UselessComponent {...timeRelatedProps}/>
|
||||
<UselessComponent {...timeRelatedProps} />
|
||||
</WithContext>
|
||||
);
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@ import nock from 'nock';
|
|||
import { TestUtils } from '../../src/js/testutils';
|
||||
import { Fetch } from '../../src/js/fetch';
|
||||
|
||||
|
||||
describe('Fetch', () => {
|
||||
describe('fetchJSON', () => {
|
||||
let nockServer = null;
|
||||
|
@ -22,75 +21,59 @@ describe('Fetch', () => {
|
|||
|
||||
describe('2xx success', () => {
|
||||
it('with simple object response body', () => {
|
||||
nockServer
|
||||
.get('/success/simple')
|
||||
.reply(200, { foo: 'bar' });
|
||||
nockServer.get('/success/simple').reply(200, { foo: 'bar' });
|
||||
|
||||
requestUrl += '/success/simple';
|
||||
|
||||
return Fetch.fetchJSON(requestUrl)
|
||||
.then(
|
||||
response => {
|
||||
assert.isOk(response);
|
||||
assert.equal(response.foo, 'bar');
|
||||
}
|
||||
);
|
||||
return Fetch.fetchJSON(requestUrl).then(response => {
|
||||
assert.isOk(response);
|
||||
assert.equal(response.foo, 'bar');
|
||||
});
|
||||
});
|
||||
|
||||
it('with empty response body', () => {
|
||||
nockServer
|
||||
.get('/success/empty')
|
||||
.reply(200, null);
|
||||
nockServer.get('/success/empty').reply(200, null);
|
||||
|
||||
requestUrl += '/success/empty';
|
||||
|
||||
return Fetch.fetchJSON(requestUrl)
|
||||
.then(
|
||||
response => {
|
||||
assert.isOk(response);
|
||||
}
|
||||
);
|
||||
return Fetch.fetchJSON(requestUrl).then(response => {
|
||||
assert.isOk(response);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('4xx failure', () => {
|
||||
it('with simple object response body', () => {
|
||||
nockServer
|
||||
.get('/failure/simple')
|
||||
.reply(400, { message: 'validation' });
|
||||
nockServer.get('/failure/simple').reply(400, { message: 'validation' });
|
||||
|
||||
requestUrl += '/failure/simple';
|
||||
|
||||
return Fetch.fetchJSON(requestUrl)
|
||||
.then(
|
||||
() => {
|
||||
assert.fail(null, null, 'should not call success handler');
|
||||
},
|
||||
error => {
|
||||
assert.isOk(error);
|
||||
assert.isOk(error.responseBody);
|
||||
assert.equal(error.responseBody.message, 'validation');
|
||||
}
|
||||
);
|
||||
return Fetch.fetchJSON(requestUrl).then(
|
||||
() => {
|
||||
assert.fail(null, null, 'should not call success handler');
|
||||
},
|
||||
error => {
|
||||
assert.isOk(error);
|
||||
assert.isOk(error.responseBody);
|
||||
assert.equal(error.responseBody.message, 'validation');
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('with empty response body', () => {
|
||||
nockServer
|
||||
.get('/failure/empty')
|
||||
.reply(400, null);
|
||||
nockServer.get('/failure/empty').reply(400, null);
|
||||
|
||||
requestUrl += '/failure/empty';
|
||||
|
||||
return Fetch.fetchJSON(requestUrl)
|
||||
.then(
|
||||
() => {
|
||||
assert.fail(null, null, 'should not call success handler');
|
||||
},
|
||||
error => {
|
||||
assert.isOk(error);
|
||||
assert.isNull(error.responseBody);
|
||||
}
|
||||
);
|
||||
return Fetch.fetchJSON(requestUrl).then(
|
||||
() => {
|
||||
assert.fail(null, null, 'should not call success handler');
|
||||
},
|
||||
error => {
|
||||
assert.isOk(error);
|
||||
assert.isNull(error.responseBody);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -98,6 +81,5 @@ describe('Fetch', () => {
|
|||
// TODO: dedupe?
|
||||
// TODO: preloader
|
||||
// TODO: loading indicator
|
||||
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,7 +12,6 @@ function setAppUrl(url) {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
describe('urlconfig', () => {
|
||||
beforeEach(() => {
|
||||
UrlConfig.enableReload();
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -54,7 +54,7 @@
|
|||
"skin-deep": "0.16.0",
|
||||
"ts-jest": "19.0.2",
|
||||
"ts-node": "5.0.1",
|
||||
"tsify": "3.0.4",
|
||||
"tsify": "4.0.0",
|
||||
"typescript": "2.7.2"
|
||||
},
|
||||
"dependencies": {
|
||||
|
@ -68,6 +68,7 @@
|
|||
"immutable": "3.8.1",
|
||||
"isomorphic-fetch": "2.2.1",
|
||||
"keymirror": "0.1.1",
|
||||
"lodash.debounce": "4.0.8",
|
||||
"mobx": "2.6.0",
|
||||
"mobx-react": "3.5.7",
|
||||
"mobx-utils": "1.1.2",
|
||||
|
|
|
@ -189,7 +189,7 @@ export class CreateCredentialDialog extends React.Component {
|
|||
const disabled = this.state.creationPending;
|
||||
|
||||
const buttons = [
|
||||
<button className="button-create-credental" disabled={disabled} onClick={() => this._onCreateClick()}>
|
||||
<button className="button-create-credential" disabled={disabled} onClick={() => this._onCreateClick()}>
|
||||
{t('creation.git.create_credential.button_create')}
|
||||
</button>,
|
||||
<button className="btn-secondary" disabled={disabled} onClick={() => this._onCloseClick()}>
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
import { CredentialsApi } from './CredentialsApi';
|
||||
import { CredentialsManager } from './CredentialsManager';
|
||||
|
||||
const api = new CredentialsApi();
|
||||
const manager = new CredentialsManager(api);
|
||||
|
||||
export { manager as credentialsManager };
|
|
@ -2,12 +2,10 @@ import React, { PropTypes } from 'react';
|
|||
import { observer } from 'mobx-react';
|
||||
import debounce from 'lodash.debounce';
|
||||
import Extensions from '@jenkins-cd/js-extensions';
|
||||
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
|
||||
import { Alerts, Dropdown, FormElement, TextInput } from '@jenkins-cd/design-language';
|
||||
import { FormElement, TextInput } from '@jenkins-cd/design-language';
|
||||
|
||||
import FlowStep from '../flow2/FlowStep';
|
||||
|
||||
import { CreateCredentialDialog } from '../credentials/CreateCredentialDialog';
|
||||
import { CreatePipelineOutcome } from './GitCreationApi';
|
||||
import STATE from './GitCreationState';
|
||||
|
||||
|
@ -33,13 +31,6 @@ export function isSshRepositoryUrl(url) {
|
|||
return false;
|
||||
}
|
||||
|
||||
function isNonSshRepositoryUrl(url) {
|
||||
if (!validateUrl(url)) {
|
||||
return false;
|
||||
}
|
||||
return !isSshRepositoryUrl(url) && /[^@:]+:\/\/.*/.test(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component that accepts repository URL and credentials to initiate
|
||||
* creation of a new pipeline.
|
||||
|
@ -54,19 +45,13 @@ export default class GitConnectStep extends React.Component {
|
|||
repositoryErrorMsg: null,
|
||||
credentialErrorMsg: null,
|
||||
selectedCredential: null,
|
||||
showCreateCredentialDialog: false,
|
||||
};
|
||||
|
||||
t = this.props.flowManager.translate;
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
const { noCredentialsOption } = this.props.flowManager;
|
||||
this._selectedCredentialChange(noCredentialsOption);
|
||||
}
|
||||
|
||||
_bindDropdown(dropdown) {
|
||||
this.dropdown = dropdown;
|
||||
this._selectedCredentialChange(this.props.flowManager.noCredentialsOption);
|
||||
}
|
||||
|
||||
_repositoryUrlChange(value) {
|
||||
|
@ -96,6 +81,12 @@ export default class GitConnectStep extends React.Component {
|
|||
}, 200);
|
||||
|
||||
_selectedCredentialChange(credential) {
|
||||
const oldId = this.state.selectedCredential && this.state.selectedCredential.id;
|
||||
const newId = credential && credential.id;
|
||||
if (oldId === newId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
selectedCredential: credential,
|
||||
});
|
||||
|
@ -111,30 +102,9 @@ export default class GitConnectStep extends React.Component {
|
|||
return null;
|
||||
}
|
||||
|
||||
_onCreateCredentialClick() {
|
||||
this.setState({
|
||||
showCreateCredentialDialog: true,
|
||||
});
|
||||
}
|
||||
|
||||
_onCreateCredentialClosed(credential) {
|
||||
const newState = {
|
||||
showCreateCredentialDialog: false,
|
||||
};
|
||||
|
||||
if (credential) {
|
||||
newState.selectedCredential = credential;
|
||||
}
|
||||
|
||||
this.setState(newState);
|
||||
|
||||
// TODO: control this more cleanly via a future 'selectedOption' prop on Dropdown
|
||||
if (this.dropdown) {
|
||||
this.dropdown.setState({
|
||||
selectedOption: credential,
|
||||
});
|
||||
}
|
||||
}
|
||||
_onCreateCredentialClosed = credential => {
|
||||
this._selectedCredentialChange(credential || this.props.flowManager.noCredentialsOption);
|
||||
};
|
||||
|
||||
_performValidation() {
|
||||
if (!validateUrl(this.state.repositoryUrl)) {
|
||||
|
@ -159,8 +129,8 @@ export default class GitConnectStep extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { noCredentialsOption } = this.props.flowManager;
|
||||
const { flowManager } = this.props;
|
||||
const { repositoryUrl } = this.state;
|
||||
const repositoryErrorMsg = this._getRepositoryErrorMsg(flowManager.outcome);
|
||||
const credentialErrorMsg = this._getCredentialErrorMsg(flowManager.outcome);
|
||||
|
||||
|
@ -180,60 +150,18 @@ export default class GitConnectStep extends React.Component {
|
|||
<TextInput className="text-repository-url" onChange={val => this._repositoryUrlChange(val)} />
|
||||
</FormElement>
|
||||
|
||||
<ReactCSSTransitionGroup
|
||||
transitionName="slide-down"
|
||||
transitionAppear
|
||||
transitionAppearTimeout={300}
|
||||
transitionEnterTimeout={300}
|
||||
transitionLeaveTimeout={300}
|
||||
>
|
||||
{isSshRepositoryUrl(this.state.repositoryUrl) && (
|
||||
<Extensions.Renderer
|
||||
extensionPoint="jenkins.credentials.selection"
|
||||
onComplete={credential => this._onCreateCredentialClosed(credential)}
|
||||
type="git"
|
||||
repositoryUrl={this.state.repositoryUrl}
|
||||
/>
|
||||
)}
|
||||
<Extensions.Renderer
|
||||
extensionPoint="jenkins.credentials.selection"
|
||||
className="credentials-selection-git"
|
||||
onComplete={this._onCreateCredentialClosed}
|
||||
type="git"
|
||||
repositoryUrl={repositoryUrl}
|
||||
/>
|
||||
|
||||
{isNonSshRepositoryUrl(this.state.repositoryUrl) && (
|
||||
<div>
|
||||
<div style={{ marginTop: 16, marginBottom: 10 }}>
|
||||
<Alerts
|
||||
type="Warning"
|
||||
message={
|
||||
<div style={{ marginTop: 6, marginBottom: 6 }}>
|
||||
Saving Pipelines is unsupported using http/https repositories. Please use SSH instead.
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<FormElement title={t('creation.git.step1.credentials')} errorMessage={credentialErrorMsg}>
|
||||
<Dropdown
|
||||
ref={dropdown => this._bindDropdown(dropdown)}
|
||||
className="dropdown-credentials"
|
||||
options={flowManager.credentials}
|
||||
defaultOption={noCredentialsOption}
|
||||
labelField="displayName"
|
||||
onChange={opt => this._selectedCredentialChange(opt)}
|
||||
/>
|
||||
|
||||
<button className="button-create-credential btn-secondary" onClick={() => this._onCreateCredentialClick()}>
|
||||
{t('creation.git.step1.create_credential_button')}
|
||||
</button>
|
||||
</FormElement>
|
||||
</div>
|
||||
)}
|
||||
</ReactCSSTransitionGroup>
|
||||
|
||||
{this.state.showCreateCredentialDialog && (
|
||||
<CreateCredentialDialog flowManager={flowManager} onClose={cred => this._onCreateCredentialClosed(cred)} />
|
||||
)}
|
||||
|
||||
{isSshRepositoryUrl(this.state.repositoryUrl) &&
|
||||
{isSshRepositoryUrl(repositoryUrl) &&
|
||||
credentialErrorMsg && <FormElement className="public-key-display" errorMessage={t('creation.git.step1.credentials_publickey_invalid')} />}
|
||||
|
||||
<button className="button-create-pipeline" onClick={() => this._beginCreation()} disabled={!validateUrl(this.state.repositoryUrl)}>
|
||||
<button className="button-create-pipeline" onClick={() => this._beginCreation()} disabled={!validateUrl(repositoryUrl)}>
|
||||
{createButtonLabel}
|
||||
</button>
|
||||
</FlowStep>
|
||||
|
|
|
@ -45,7 +45,8 @@ class CredentialsPicker extends React.Component {
|
|||
} else if (type === 'bitbucket-cloud' || type === 'bitbucket-server') {
|
||||
children = <BbCredentialsPicker scmId={scmSource.id} apiUrl={scmSource.apiUrl} />;
|
||||
} else if (type === 'git') {
|
||||
children = <GitCredentialsPicker scmId={scmSource.id} />;
|
||||
const repositoryUrl = this.props.repositoryUrl || scmSource.apiUrl;
|
||||
children = <GitCredentialsPicker repositoryUrl={repositoryUrl} />;
|
||||
} else {
|
||||
children = <div>No credential picker could be found for type={type}</div>;
|
||||
}
|
||||
|
@ -63,6 +64,7 @@ CredentialsPicker.propTypes = {
|
|||
dialog: PropTypes.bool,
|
||||
pipeline: PropTypes.object,
|
||||
repositoryUrl: PropTypes.string,
|
||||
existingFailed: PropTypes.bool,
|
||||
scmSource: PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
apiUrl: PropTypes.string,
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
/**
|
||||
* Utlity class for "typing" of server errors
|
||||
*/
|
||||
class TypedError {
|
||||
constructor(type, serverError) {
|
||||
const { code, message, errors } = serverError || {};
|
||||
this.type = type;
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
this.errors = errors;
|
||||
}
|
||||
}
|
||||
|
||||
export default TypedError;
|
|
@ -0,0 +1,47 @@
|
|||
/**
|
||||
* Utlity class for "typing" of server errors
|
||||
*/
|
||||
|
||||
export interface ServerError {
|
||||
code?: number,
|
||||
message?: string,
|
||||
errors?: Array<string>
|
||||
}
|
||||
|
||||
export class TypedError extends Error {
|
||||
|
||||
type: string;
|
||||
code: number;
|
||||
errors: Array<string>;
|
||||
|
||||
constructor(type?: string, serverError?: ServerError) {
|
||||
super();
|
||||
|
||||
// When subclassing Error and targetting ES5, super() wrecks our prototype chain
|
||||
this.constructor = TypedError;
|
||||
if ((Object as any).setPrototypeOf) {
|
||||
(Object as any).setPrototypeOf(this, TypedError.prototype);
|
||||
} else {
|
||||
(this as any).__proto__ = TypedError.prototype;
|
||||
}
|
||||
|
||||
this.name = 'TypedError';
|
||||
return this.populate(type || 'TypedError', serverError);
|
||||
}
|
||||
|
||||
populate(type: string, serverError?: ServerError) {
|
||||
|
||||
const {
|
||||
code = -1,
|
||||
message = undefined,
|
||||
errors = []
|
||||
} = serverError || {};
|
||||
|
||||
this.type = type;
|
||||
this.code = code;
|
||||
this.message = message || String(serverError || type);
|
||||
this.errors = errors;
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import { Fetch, UrlConfig, Utils, AppConfig } from '@jenkins-cd/blueocean-core-js';
|
||||
import TypedError from '../TypedError';
|
||||
import { TypedError } from '../TypedError';
|
||||
|
||||
export const LoadError = {
|
||||
TOKEN_NOT_FOUND: 'TOKEN_NOT_FOUND',
|
||||
|
|
|
@ -2,7 +2,7 @@ import { action, observable } from 'mobx';
|
|||
|
||||
import PromiseDelayUtils from '../../util/PromiseDelayUtils';
|
||||
import BbCredentialsApi from './BbCredentialsApi';
|
||||
import BbCredentialState from './BbCredentialsState';
|
||||
import BbCredentialsState from './BbCredentialsState';
|
||||
import { LoadError, SaveError } from './BbCredentialsApi';
|
||||
|
||||
const MIN_DELAY = 500;
|
||||
|
@ -28,7 +28,7 @@ class BbCredentialsManager {
|
|||
|
||||
@action
|
||||
findExistingCredential() {
|
||||
this.stateId = BbCredentialState.PENDING_LOADING_CREDS;
|
||||
this.stateId = BbCredentialsState.PENDING_LOADING_CREDS;
|
||||
return this._credentialsApi
|
||||
.findExistingCredential(this.apiUrl)
|
||||
.then(...delayBoth(MIN_DELAY))
|
||||
|
@ -38,13 +38,13 @@ class BbCredentialsManager {
|
|||
@action
|
||||
_findExistingCredentialFailure(error) {
|
||||
if (error.type === LoadError.TOKEN_NOT_FOUND) {
|
||||
this.stateId = BbCredentialState.NEW_REQUIRED;
|
||||
this.stateId = BbCredentialsState.NEW_REQUIRED;
|
||||
} else if (error.type === LoadError.TOKEN_INVALID) {
|
||||
this.stateId = BbCredentialState.INVALID_CREDENTIAL;
|
||||
this.stateId = BbCredentialsState.INVALID_CREDENTIAL;
|
||||
} else if (error.type === LoadError.TOKEN_REVOKED) {
|
||||
this.stateId = BbCredentialState.REVOKED_CREDENTIAL;
|
||||
this.stateId = BbCredentialsState.REVOKED_CREDENTIAL;
|
||||
} else {
|
||||
this.stateId = BbCredentialState.UNEXPECTED_ERROR_CREDENTIAL;
|
||||
this.stateId = BbCredentialsState.UNEXPECTED_ERROR_CREDENTIAL;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -62,7 +62,7 @@ class BbCredentialsManager {
|
|||
@action
|
||||
_createCredentialSuccess(credential) {
|
||||
this.pendingValidation = false;
|
||||
this.stateId = BbCredentialState.SAVE_SUCCESS;
|
||||
this.stateId = BbCredentialsState.SAVE_SUCCESS;
|
||||
return credential;
|
||||
}
|
||||
|
||||
|
@ -71,7 +71,7 @@ class BbCredentialsManager {
|
|||
this.pendingValidation = false;
|
||||
|
||||
if (error.type === SaveError.INVALID_CREDENTIAL) {
|
||||
this.stateId = BbCredentialState.INVALID_CREDENTIAL;
|
||||
this.stateId = BbCredentialsState.INVALID_CREDENTIAL;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
|
|
|
@ -162,7 +162,7 @@ class BbCredentialsPicker extends React.Component {
|
|||
<PasswordInput className="text-password" onChange={val => this._passwordChange(val)} />
|
||||
</FormElement>
|
||||
</FormElement>
|
||||
<Button className="button-create-credental" status={status} onClick={() => this._createCredential()}>
|
||||
<Button className="button-create-credential" status={status} onClick={() => this._createCredential()}>
|
||||
Connect
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Enum } from '../../creation/flow2/Enum';
|
|||
/**
|
||||
* Valid stateId's for BitBucketCredentialsStep
|
||||
*/
|
||||
const BbCredentialState = new Enum({
|
||||
export const BbCredentialsState = new Enum({
|
||||
PENDING_LOADING_CREDS: 'PENDING_LOADING_CREDS',
|
||||
NEW_REQUIRED: 'new_required',
|
||||
SAVE_SUCCESS: 'save_success',
|
||||
|
@ -12,4 +12,4 @@ const BbCredentialState = new Enum({
|
|||
UNEXPECTED_ERROR_CREDENTIAL: 'unexpected_error_credential',
|
||||
});
|
||||
|
||||
export default BbCredentialState;
|
||||
export default BbCredentialsState;
|
||||
|
|
|
@ -1,151 +1,41 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
import { FormElement } from '@jenkins-cd/design-language';
|
||||
import { Fetch, AppConfig, i18nTranslator } from '@jenkins-cd/blueocean-core-js';
|
||||
import { Button } from '../../creation/github/Button';
|
||||
const t = i18nTranslator('blueocean-dashboard');
|
||||
import { GitCredentialsPickerSSH } from './GitCredentialsPickerSSH';
|
||||
import { GitCredentialsPickerPassword } from './GitCredentialsPickerPassword';
|
||||
|
||||
function copySelectionText() {
|
||||
let copysuccess; // var to check whether execCommand successfully executed
|
||||
try {
|
||||
copysuccess = document.execCommand('copy'); // copy selected text to clipboard
|
||||
} catch (_) {
|
||||
copysuccess = false;
|
||||
function isSshRepositoryUrl(url) {
|
||||
if (typeof url !== 'string' || url.trim().length === 0) {
|
||||
return false;
|
||||
}
|
||||
return copysuccess;
|
||||
|
||||
if (/^ssh:\/\/.*/.test(url)) {
|
||||
// is ssh:// protocol
|
||||
return true;
|
||||
}
|
||||
|
||||
if (/^[^@:]+@.*/.test(url)) {
|
||||
// No protocol, but has a "user@host[...]" format
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
if (window.getSelection) {
|
||||
window.getSelection().removeAllRanges();
|
||||
} else if (document.selection) {
|
||||
document.selection.empty();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Just a wrapper to decide between the SSH component and username/pw component based on repositoryUrl
|
||||
*/
|
||||
const GitCredentialsPicker = props => {
|
||||
const { repositoryUrl } = props;
|
||||
|
||||
class GitCredentialsPicker extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
credential: null,
|
||||
credentialError: null,
|
||||
};
|
||||
this.restOrgPrefix = AppConfig.getRestRoot() + '/organizations/' + AppConfig.getOrganizationName();
|
||||
if (!repositoryUrl) {
|
||||
return null; // Repo URL decides wether we show certificate or un/pw
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
const { onStatus, dialog, onComplete } = this.props;
|
||||
if (onStatus) {
|
||||
onStatus('promptLoading');
|
||||
}
|
||||
Fetch.fetchJSON(this.restOrgPrefix + '/user/publickey/').then(credential => {
|
||||
this.setState({ credential });
|
||||
if (onStatus) {
|
||||
onStatus('promptReady');
|
||||
}
|
||||
if (!dialog) {
|
||||
onComplete(credential);
|
||||
}
|
||||
});
|
||||
if (isSshRepositoryUrl(repositoryUrl)) {
|
||||
return <GitCredentialsPickerSSH {...props} />;
|
||||
}
|
||||
|
||||
copyPublicKeyToClipboard() {
|
||||
const textBox = this.publicKeyElement;
|
||||
textBox.select();
|
||||
copySelectionText();
|
||||
clearSelection();
|
||||
textBox.blur();
|
||||
}
|
||||
|
||||
testCredentialAndCloseDialog() {
|
||||
const { onComplete, repositoryUrl, pipeline, requirePush, branch } = this.props;
|
||||
const body = {
|
||||
repositoryUrl,
|
||||
pipeline,
|
||||
credentialId: this.state.credential.id,
|
||||
};
|
||||
if (requirePush) {
|
||||
body.requirePush = true;
|
||||
body.branch = branch || 'master';
|
||||
}
|
||||
const fetchOptions = {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
};
|
||||
this.setState({ connectStatus: { result: 'running' } });
|
||||
return Fetch.fetchJSON(this.restOrgPrefix + '/scm/git/validate', { fetchOptions })
|
||||
.then(() => {
|
||||
this.setState({
|
||||
credentialError: null,
|
||||
connectStatus: {
|
||||
result: 'success',
|
||||
reset: false,
|
||||
},
|
||||
});
|
||||
onComplete(this.state.credential);
|
||||
})
|
||||
.catch(error => {
|
||||
const message = error.responseBody ? error.responseBody.message : 'An unknown error occurred';
|
||||
this.setState({
|
||||
credentialError: message && t('creation.git.step1.credentials_publickey_invalid'),
|
||||
connectStatus: {
|
||||
reset: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
closeDialog() {
|
||||
this.context.router.goBack();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.state.credential) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="credentials-picker-git">
|
||||
<p className="instructions">
|
||||
{t('creation.git.credentials.register_ssh_key_instructions')}{' '}
|
||||
<a target="jenkins-docs" href="https://jenkins.io/doc/book/blueocean/creating-pipelines/#creating-a-pipeline-for-a-git-repository">
|
||||
learn more
|
||||
</a>.
|
||||
</p>
|
||||
<FormElement>
|
||||
<textarea
|
||||
className="TextArea-control"
|
||||
ref={e => {
|
||||
this.publicKeyElement = e;
|
||||
}}
|
||||
readOnly
|
||||
onChange={e => e}
|
||||
value={this.state.credential.publickey}
|
||||
/>
|
||||
</FormElement>
|
||||
<a
|
||||
href="#"
|
||||
className="copy-key-link"
|
||||
onClick={e => {
|
||||
this.copyPublicKeyToClipboard();
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
{t('creation.git.credentials.copy_to_clipboard')}
|
||||
</a>
|
||||
{this.props.dialog && (
|
||||
<FormElement errorMessage={this.state.credentialError} className="action-buttons">
|
||||
<Button status={this.state.connectStatus} onClick={() => this.testCredentialAndCloseDialog()}>
|
||||
{t('creation.git.credentials.connect_and_validate')}
|
||||
</Button>
|
||||
<Button onClick={() => this.closeDialog()} className="btn-secondary">
|
||||
{t('creation.git.create_credential.button_close')}
|
||||
</Button>
|
||||
</FormElement>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
return <GitCredentialsPickerPassword {...props} />;
|
||||
};
|
||||
|
||||
GitCredentialsPicker.propTypes = {
|
||||
onStatus: PropTypes.func,
|
||||
|
|
|
@ -0,0 +1,283 @@
|
|||
import * as React from 'react';
|
||||
import {observer} from 'mobx-react';
|
||||
import {i18nTranslator} from '@jenkins-cd/blueocean-core-js';
|
||||
import {GitPWCredentialsManager, ManagerState, Credential} from './GitPWCredentialsManager';
|
||||
import * as debounce from 'lodash.debounce';
|
||||
|
||||
import {Button} from '../../creation/github/Button';
|
||||
|
||||
import {
|
||||
FormElement,
|
||||
PasswordInput,
|
||||
TextInput,
|
||||
RadioButtonGroup,
|
||||
} from '@jenkins-cd/design-language';
|
||||
|
||||
const t = i18nTranslator('blueocean-dashboard');
|
||||
|
||||
interface Props {
|
||||
onStatus?: (status: string) => void,
|
||||
onComplete?: (selectedCredential: Credential | undefined, cause?: string) => void,
|
||||
repositoryUrl: string,
|
||||
branch?: string,
|
||||
existingFailed?: boolean, // if true we shouldn't bother looking it up
|
||||
requirePush?: boolean,
|
||||
}
|
||||
|
||||
interface State {
|
||||
usernameValue: string | null,
|
||||
usernameErrorMsg: string | null,
|
||||
passwordValue: string | null,
|
||||
passwordErrorMsg: string | null,
|
||||
selectedRadio: RadioOption
|
||||
}
|
||||
|
||||
enum RadioOption {
|
||||
USE_EXISTING = 'useExisting',
|
||||
CREATE_NEW = 'createNew',
|
||||
}
|
||||
|
||||
const radioOptions = Object.values(RadioOption);
|
||||
|
||||
function getErrorMessage(state: ManagerState) {
|
||||
if (state === ManagerState.INVALID_CREDENTIAL) {
|
||||
return t('creation.git.create_credential.invalid_username_password');
|
||||
} else if (state === ManagerState.REVOKED_CREDENTIAL) {
|
||||
return t('creation.git.create_credential.revoked_credential');
|
||||
} else if (state === ManagerState.UNEXPECTED_ERROR_CREDENTIAL) {
|
||||
return t('creation.git.create_credential.unexpected_error');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component to handle lookup / creation of username+password credentials for git repositories over http(s)
|
||||
*/
|
||||
@observer
|
||||
export class GitCredentialsPickerPassword extends React.Component<Props, State> {
|
||||
|
||||
credentialsManager: GitPWCredentialsManager;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.credentialsManager = new GitPWCredentialsManager();
|
||||
|
||||
this.state = {
|
||||
usernameValue: null,
|
||||
usernameErrorMsg: null,
|
||||
passwordValue: null,
|
||||
passwordErrorMsg: null,
|
||||
selectedRadio: RadioOption.USE_EXISTING,
|
||||
};
|
||||
|
||||
const {repositoryUrl, branch, existingFailed = false} = this.props;
|
||||
this._repositoryChanged(repositoryUrl, branch, existingFailed);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
if (this.props.onStatus) {
|
||||
this.props.onStatus('promptLoading');
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps: Props) {
|
||||
const {repositoryUrl, branch, existingFailed} = nextProps;
|
||||
if (branch !== this.props.branch
|
||||
|| repositoryUrl !== this.props.repositoryUrl
|
||||
|| existingFailed !== this.props.existingFailed) {
|
||||
|
||||
this._repositoryChanged(repositoryUrl, branch, !!existingFailed);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {existingFailed, onStatus} = this.props;
|
||||
if (existingFailed) {
|
||||
if (onStatus) {
|
||||
onStatus('promptReady');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_repositoryChanged = debounce((repositoryUrl: string, branch: string | undefined, existingFailed: boolean) => {
|
||||
|
||||
const {onStatus, onComplete} = this.props;
|
||||
const credentialsManager = this.credentialsManager;
|
||||
|
||||
credentialsManager.configure(repositoryUrl, branch);
|
||||
|
||||
if (!existingFailed) {
|
||||
credentialsManager.findExistingCredential()
|
||||
.then(credential => {
|
||||
if (credential && onComplete) {
|
||||
this.setState({selectedRadio: RadioOption.USE_EXISTING});
|
||||
|
||||
if (onComplete) {
|
||||
onComplete(credential, 'autoSelected');
|
||||
}
|
||||
|
||||
} else if (onStatus) {
|
||||
onStatus('promptReady');
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 500);
|
||||
|
||||
_createCredential() {
|
||||
const valid = this._performValidation();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {usernameValue, passwordValue} = this.state;
|
||||
const {requirePush = false} = this.props;
|
||||
|
||||
this.credentialsManager
|
||||
.createCredential(usernameValue, passwordValue, requirePush)
|
||||
.catch(error => {
|
||||
return undefined; // Error details handled by manager state
|
||||
})
|
||||
.then(credential => {
|
||||
this.setState({
|
||||
selectedRadio: RadioOption.USE_EXISTING,
|
||||
usernameValue: null,
|
||||
passwordValue: null,
|
||||
});
|
||||
|
||||
if (this.props.onComplete) {
|
||||
// Notify even if credential undefined, so owner knows "unselected"
|
||||
this.props.onComplete(credential, 'userSelected');
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
_performValidation() {
|
||||
let result = true;
|
||||
if (!this.state.usernameValue) {
|
||||
this.setState({
|
||||
usernameErrorMsg: t('creation.git.create_credential.username_error'),
|
||||
});
|
||||
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (!this.state.passwordValue) {
|
||||
this.setState({
|
||||
passwordErrorMsg: t('creation.git.create_credential.password_error'),
|
||||
});
|
||||
|
||||
result = false;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
_usernameChange(value) {
|
||||
this.setState({
|
||||
usernameValue: value,
|
||||
});
|
||||
|
||||
this._updateUsernameErrorMsg(false);
|
||||
}
|
||||
|
||||
_updateUsernameErrorMsg = debounce(reset => {
|
||||
if (reset || (this.state.usernameErrorMsg && this.state.usernameValue)) {
|
||||
this.setState({
|
||||
usernameErrorMsg: null,
|
||||
});
|
||||
}
|
||||
}, 200);
|
||||
|
||||
_passwordChange(value) {
|
||||
this.setState({
|
||||
passwordValue: value,
|
||||
});
|
||||
|
||||
this._updatePasswordErrorMsg(false);
|
||||
}
|
||||
|
||||
_updatePasswordErrorMsg = debounce(reset => {
|
||||
if (reset || (this.state.passwordErrorMsg && this.state.passwordValue)) {
|
||||
this.setState({
|
||||
passwordErrorMsg: null,
|
||||
});
|
||||
}
|
||||
}, 200);
|
||||
|
||||
_radioLabel = (option) => {
|
||||
const {existingCredential} = this.credentialsManager;
|
||||
const displayName = existingCredential && existingCredential.displayName || '';
|
||||
|
||||
switch (option) {
|
||||
case RadioOption.CREATE_NEW:
|
||||
return t('creation.git.create_credential.option_create_new');
|
||||
case RadioOption.USE_EXISTING:
|
||||
return t('creation.git.create_credential.option_existing', [displayName]);
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
_radioChanged = (selectedRadio: RadioOption) => {
|
||||
const {onComplete} = this.props;
|
||||
|
||||
if (onComplete) {
|
||||
onComplete(selectedRadio === RadioOption.USE_EXISTING ? this.credentialsManager.existingCredential : undefined);
|
||||
}
|
||||
|
||||
this.setState({selectedRadio});
|
||||
};
|
||||
|
||||
render() {
|
||||
const managerState: ManagerState = this.credentialsManager.state;
|
||||
|
||||
if (managerState === ManagerState.PENDING_LOADING_CREDS) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const errorMessage = getErrorMessage(managerState);
|
||||
const isPendingValidation = managerState == ManagerState.PENDING_VALIDATION;
|
||||
|
||||
const connectButtonStatus = {result: null as string | null};
|
||||
const {selectedRadio} = this.state;
|
||||
|
||||
const {existingCredential} = this.credentialsManager;
|
||||
|
||||
const hasExistingCredential = !!existingCredential;
|
||||
const useExistingCredential = hasExistingCredential && selectedRadio === RadioOption.USE_EXISTING;
|
||||
const disableForm = isPendingValidation || useExistingCredential;
|
||||
|
||||
if (isPendingValidation) {
|
||||
connectButtonStatus.result = 'running';
|
||||
} else if (managerState === ManagerState.SAVE_SUCCESS && useExistingCredential) {
|
||||
connectButtonStatus.result = 'success';
|
||||
}
|
||||
|
||||
const labelInstructions = t('creation.git.create_credential.pw_instructions');
|
||||
const labelUsername = t('creation.git.create_credential.username_title');
|
||||
const labelPassword = t('creation.git.create_credential.password_title');
|
||||
const labelButton = t('creation.git.create_credential.button_create');
|
||||
|
||||
return (
|
||||
<div className="credentials-picker-git">
|
||||
<p className="instructions">{labelInstructions}</p>
|
||||
{hasExistingCredential && (
|
||||
<RadioButtonGroup options={radioOptions}
|
||||
labelFunction={this._radioLabel}
|
||||
defaultOption={RadioOption.USE_EXISTING}
|
||||
onChange={this._radioChanged} />
|
||||
)}
|
||||
<FormElement className="credentials-new" errorMessage={errorMessage} verticalLayout>
|
||||
<FormElement title={labelUsername} errorMessage={this.state.usernameErrorMsg}>
|
||||
<TextInput disabled={disableForm} className="text-username" onChange={val => this._usernameChange(val)} />
|
||||
</FormElement>
|
||||
<FormElement title={labelPassword} errorMessage={this.state.passwordErrorMsg}>
|
||||
<PasswordInput disabled={disableForm} className="text-password" onChange={val => this._passwordChange(val)} />
|
||||
</FormElement>
|
||||
</FormElement>
|
||||
<Button disabled={disableForm} className="button-create-credential" status={connectButtonStatus} onClick={() => this._createCredential()}>
|
||||
{labelButton}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,166 @@
|
|||
import React, { PropTypes, Component } from 'react';
|
||||
import { FormElement } from '@jenkins-cd/design-language';
|
||||
import { Fetch, AppConfig, i18nTranslator } from '@jenkins-cd/blueocean-core-js';
|
||||
import { Button } from '../../creation/github/Button';
|
||||
const t = i18nTranslator('blueocean-dashboard');
|
||||
|
||||
function copySelectionText() {
|
||||
let copysuccess; // var to check whether execCommand successfully executed
|
||||
try {
|
||||
copysuccess = document.execCommand('copy'); // copy selected text to clipboard
|
||||
} catch (_) {
|
||||
copysuccess = false;
|
||||
}
|
||||
return copysuccess;
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
if (window.getSelection) {
|
||||
window.getSelection().removeAllRanges();
|
||||
} else if (document.selection) {
|
||||
document.selection.empty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Public key credentials UI for git repos via ssh:// or git://
|
||||
*/
|
||||
export class GitCredentialsPickerSSH extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
credential: null,
|
||||
credentialError: null,
|
||||
};
|
||||
this.restOrgPrefix = AppConfig.getRestRoot() + '/organizations/' + AppConfig.getOrganizationName();
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
const { onStatus, dialog, onComplete } = this.props;
|
||||
if (onStatus) {
|
||||
onStatus('promptLoading');
|
||||
}
|
||||
Fetch.fetchJSON(this.restOrgPrefix + '/user/publickey/').then(credential => {
|
||||
this.setState({ credential });
|
||||
if (onStatus) {
|
||||
onStatus('promptReady');
|
||||
}
|
||||
if (!dialog) {
|
||||
onComplete(credential);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
copyPublicKeyToClipboard() {
|
||||
const textBox = this.publicKeyElement;
|
||||
textBox.select();
|
||||
copySelectionText();
|
||||
clearSelection();
|
||||
textBox.blur();
|
||||
}
|
||||
|
||||
testCredentialAndCloseDialog() {
|
||||
const { onComplete, repositoryUrl, pipeline, requirePush, branch } = this.props;
|
||||
const body = {
|
||||
repositoryUrl,
|
||||
pipeline,
|
||||
credentialId: this.state.credential.id,
|
||||
};
|
||||
if (requirePush) {
|
||||
body.requirePush = true;
|
||||
body.branch = branch || 'master';
|
||||
}
|
||||
const fetchOptions = {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
};
|
||||
this.setState({ connectStatus: { result: 'running' } });
|
||||
return Fetch.fetchJSON(this.restOrgPrefix + '/scm/git/validate', { fetchOptions })
|
||||
.then(() => {
|
||||
this.setState({
|
||||
credentialError: null,
|
||||
connectStatus: {
|
||||
result: 'success',
|
||||
reset: false,
|
||||
},
|
||||
});
|
||||
onComplete(this.state.credential);
|
||||
})
|
||||
.catch(error => {
|
||||
const message = error.responseBody ? error.responseBody.message : 'An unknown error occurred';
|
||||
this.setState({
|
||||
credentialError: message && t('creation.git.step1.credentials_publickey_invalid'),
|
||||
connectStatus: {
|
||||
reset: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
closeDialog() {
|
||||
this.context.router.goBack();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.state.credential) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="credentials-picker-git">
|
||||
<p className="instructions">
|
||||
{t('creation.git.credentials.register_ssh_key_instructions')}{' '}
|
||||
<a target="jenkins-docs" href="https://jenkins.io/doc/book/blueocean/creating-pipelines/#creating-a-pipeline-for-a-git-repository">
|
||||
learn more
|
||||
</a>.
|
||||
</p>
|
||||
<FormElement>
|
||||
<textarea
|
||||
className="TextArea-control"
|
||||
ref={e => {
|
||||
this.publicKeyElement = e;
|
||||
}}
|
||||
readOnly
|
||||
onChange={e => e}
|
||||
value={this.state.credential.publickey}
|
||||
/>
|
||||
</FormElement>
|
||||
<a
|
||||
href="#"
|
||||
className="copy-key-link"
|
||||
onClick={e => {
|
||||
this.copyPublicKeyToClipboard();
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
{t('creation.git.credentials.copy_to_clipboard')}
|
||||
</a>
|
||||
{this.props.dialog && (
|
||||
<FormElement errorMessage={this.state.credentialError} className="action-buttons">
|
||||
<Button status={this.state.connectStatus} onClick={() => this.testCredentialAndCloseDialog()}>
|
||||
{t('creation.git.credentials.connect_and_validate')}
|
||||
</Button>
|
||||
<Button onClick={() => this.closeDialog()} className="btn-secondary">
|
||||
{t('creation.git.create_credential.button_close')}
|
||||
</Button>
|
||||
</FormElement>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
GitCredentialsPickerSSH.propTypes = {
|
||||
onStatus: PropTypes.func,
|
||||
onComplete: PropTypes.func,
|
||||
requirePush: PropTypes.bool,
|
||||
branch: PropTypes.string,
|
||||
scmId: PropTypes.string,
|
||||
dialog: PropTypes.bool,
|
||||
repositoryUrl: PropTypes.string,
|
||||
pipeline: PropTypes.object,
|
||||
};
|
||||
|
||||
GitCredentialsPickerSSH.contextTypes = {
|
||||
router: React.PropTypes.object,
|
||||
};
|
|
@ -0,0 +1,114 @@
|
|||
import {
|
||||
Fetch,
|
||||
UrlConfig,
|
||||
Utils,
|
||||
AppConfig,
|
||||
UrlBuilder,
|
||||
} from '@jenkins-cd/blueocean-core-js';
|
||||
|
||||
import {TypedError} from "../TypedError";
|
||||
|
||||
import {
|
||||
LoadError,
|
||||
SaveError,
|
||||
} from '../bitbucket/BbCredentialsApi';
|
||||
|
||||
export interface GitPWCredentialsApiPublic {
|
||||
findExistingCredential(repositoryUrl);
|
||||
createCredential(repositoryUrl, userName, password, branchName, requirePush);
|
||||
}
|
||||
|
||||
/**
|
||||
* Api class to interact with GitScm class when working with username+password credentials for http(s) repos
|
||||
*/
|
||||
export class GitPWCredentialsApi implements GitPWCredentialsApiPublic {
|
||||
|
||||
_fetch: Function;
|
||||
organization: string;
|
||||
|
||||
constructor() {
|
||||
this._fetch = Fetch.fetchJSON;
|
||||
this.organization = AppConfig.getOrganizationName();
|
||||
}
|
||||
|
||||
findExistingCredential(repositoryUrl) {
|
||||
const root = UrlConfig.getJenkinsRootURL();
|
||||
const credUrl = Utils.cleanSlashes(`${root}/blue/rest/organizations/${this.organization}/scm/git/?repositoryUrl=${repositoryUrl}`);
|
||||
|
||||
// Create error in sync code for better stack trace
|
||||
const possibleError = new TypedError();
|
||||
|
||||
return this._fetch(credUrl)
|
||||
.then(
|
||||
result => this._findExistingCredentialSuccess(result),
|
||||
error => {
|
||||
const {responseBody} = error;
|
||||
|
||||
if (responseBody.message.indexOf('Existing credential failed') >= 0) {
|
||||
throw possibleError.populate(LoadError.TOKEN_REVOKED, responseBody);
|
||||
}
|
||||
|
||||
throw possibleError.populate(LoadError.TOKEN_INVALID, responseBody);
|
||||
});
|
||||
}
|
||||
|
||||
_findExistingCredentialSuccess(gitScm) {
|
||||
const credentialId = gitScm && gitScm.credentialId;
|
||||
|
||||
if (!credentialId) {
|
||||
throw new TypedError(LoadError.TOKEN_NOT_FOUND);
|
||||
}
|
||||
|
||||
return this._getCredential(credentialId);
|
||||
}
|
||||
|
||||
_getCredential(credentialId) {
|
||||
const orgUrl = UrlBuilder.buildRestUrl(this.organization);
|
||||
const credentialUrl = `${orgUrl}credentials/user/domains/blueocean-git-domain/credentials/${encodeURIComponent(credentialId)}/`;
|
||||
|
||||
return this._fetch(credentialUrl);
|
||||
}
|
||||
|
||||
createCredential(repositoryUrl, userName, password, branchName, requirePush) {
|
||||
const path = UrlConfig.getJenkinsRootURL();
|
||||
const validateCredUrl = Utils.cleanSlashes(`${path}/blue/rest/organizations/${this.organization}/scm/git/validate`);
|
||||
|
||||
const requestBody: any = {
|
||||
userName,
|
||||
password,
|
||||
repositoryUrl,
|
||||
};
|
||||
|
||||
if (branchName) {
|
||||
requestBody.branch = branchName;
|
||||
}
|
||||
|
||||
if (requirePush) {
|
||||
requestBody.repositoryUrl = true; // Only set if true!
|
||||
}
|
||||
|
||||
const fetchOptions = {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
};
|
||||
|
||||
// Create error in sync code for better stack trace
|
||||
const possibleError = new TypedError();
|
||||
|
||||
return this._fetch(validateCredUrl, {fetchOptions})
|
||||
.catch(error => {
|
||||
const {code = -1} = error.responseBody || {};
|
||||
|
||||
if (code === 401) {
|
||||
throw possibleError.populate(SaveError.INVALID_CREDENTIAL, error);
|
||||
}
|
||||
|
||||
throw possibleError.populate(SaveError.UNKNOWN_ERROR, error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
import {action, observable} from "mobx";
|
||||
|
||||
import {
|
||||
LoadError,
|
||||
SaveError,
|
||||
} from '../bitbucket/BbCredentialsApi';
|
||||
|
||||
import {GitPWCredentialsApi} from './GitPWCredentialsApi';
|
||||
|
||||
import PromiseDelayUtils from '../../util/PromiseDelayUtils';
|
||||
|
||||
const MIN_DELAY = 500;
|
||||
const {delayBoth} = PromiseDelayUtils;
|
||||
|
||||
export enum ManagerState {
|
||||
PENDING_LOADING_CREDS = 'PENDING_LOADING_CREDS',
|
||||
EXISTING_FOUND = 'EXISTING_FOUND',
|
||||
NEW_REQUIRED = 'NEW_REQUIRED',
|
||||
SAVE_SUCCESS = 'SAVE_SUCCESS',
|
||||
INVALID_CREDENTIAL = 'INVALID_CREDENTIAL',
|
||||
REVOKED_CREDENTIAL = 'REVOKED_CREDENTIAL',
|
||||
UNEXPECTED_ERROR_CREDENTIAL = 'UNEXPECTED_ERROR_CREDENTIAL',
|
||||
PENDING_VALIDATION = 'PENDING_VALIDATION',
|
||||
}
|
||||
|
||||
export interface Credential { // FIXME: Canonical types in core-js
|
||||
id: string,
|
||||
displayName: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Acts as a mobx store and api intermediary on behalf of GitCredentialsPickerPassword
|
||||
*/
|
||||
export class GitPWCredentialsManager {
|
||||
|
||||
repositoryUrl?: string;
|
||||
branch: string = 'master';
|
||||
|
||||
@observable state: ManagerState = ManagerState.PENDING_LOADING_CREDS;
|
||||
@observable existingCredential?: Credential;
|
||||
|
||||
private api: GitPWCredentialsApi;
|
||||
|
||||
constructor(api?: GitPWCredentialsApi) {
|
||||
this.api = api || new GitPWCredentialsApi();
|
||||
}
|
||||
|
||||
configure(repositoryUrl: string, branch?: string) {
|
||||
this.repositoryUrl = repositoryUrl;
|
||||
|
||||
if (typeof branch === 'string') {
|
||||
this.branch = branch;
|
||||
} else {
|
||||
this.branch = 'master';
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
findExistingCredential() {
|
||||
this.state = ManagerState.PENDING_LOADING_CREDS;
|
||||
return this.api
|
||||
.findExistingCredential(this.repositoryUrl)
|
||||
.then(r => {
|
||||
return r;
|
||||
})
|
||||
.then(...delayBoth(MIN_DELAY))
|
||||
.then(r => {
|
||||
return r;
|
||||
})
|
||||
.then(action((credential: Credential) => {
|
||||
this.state = ManagerState.EXISTING_FOUND;
|
||||
this.existingCredential = credential;
|
||||
return credential;
|
||||
}))
|
||||
.catch(action((error: any) => {
|
||||
if (error.type === LoadError.TOKEN_NOT_FOUND) {
|
||||
this.state = ManagerState.NEW_REQUIRED;
|
||||
} else if (error.type === LoadError.TOKEN_INVALID) {
|
||||
this.state = ManagerState.INVALID_CREDENTIAL;
|
||||
} else if (error.type === LoadError.TOKEN_REVOKED) {
|
||||
this.state = ManagerState.REVOKED_CREDENTIAL;
|
||||
} else {
|
||||
this.state = ManagerState.UNEXPECTED_ERROR_CREDENTIAL;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@action
|
||||
createCredential(userName, password, requirePush: boolean) {
|
||||
this.state = ManagerState.PENDING_VALIDATION;
|
||||
this.existingCredential = undefined;
|
||||
|
||||
const repositoryUrl = this.repositoryUrl;
|
||||
const branchName = this.branch;
|
||||
|
||||
return this.api
|
||||
.createCredential(repositoryUrl, userName, password, branchName, requirePush)
|
||||
.then(...delayBoth(MIN_DELAY))
|
||||
.then(action(() => {
|
||||
// Need to look up the existing credential because create service doesn't return the new id
|
||||
// Need to use _api directly rather than this.findExistingCredential because we don't want
|
||||
// the state to change until it's done
|
||||
return this.api.findExistingCredential(repositoryUrl);
|
||||
}))
|
||||
.then(action((credential: Credential) => {
|
||||
this.state = ManagerState.SAVE_SUCCESS;
|
||||
this.existingCredential = credential;
|
||||
return credential
|
||||
}))
|
||||
.catch(action((error: any) => {
|
||||
if (error.type === SaveError.INVALID_CREDENTIAL) {
|
||||
this.state = ManagerState.INVALID_CREDENTIAL;
|
||||
} else {
|
||||
this.state = ManagerState.UNEXPECTED_ERROR_CREDENTIAL;
|
||||
}
|
||||
throw error;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { Fetch, UrlConfig, Utils, AppConfig } from '@jenkins-cd/blueocean-core-js';
|
||||
|
||||
import GithubApiUtils from '../../creation/github/api/GithubApiUtils';
|
||||
import TypedError from '../TypedError';
|
||||
import { TypedError } from '../TypedError';
|
||||
|
||||
export const LoadError = {
|
||||
TOKEN_NOT_FOUND: 'TOKEN_NOT_FOUND',
|
||||
|
|
|
@ -11,6 +11,10 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.credentials-selection-git {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.dropdown-credentials {
|
||||
margin-right: 10px;
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ creation.core.error.unexpected.try_again=An unknown error has occurred. You may
|
|||
|
||||
# create credential dialog
|
||||
creation.git.create_credential.button_close=Cancel
|
||||
creation.git.create_credential.button_create=Create credential
|
||||
creation.git.create_credential.button_create=Create Credential
|
||||
creation.git.create_credential.error_msg=An error occurred while creating the credential. You may try again.
|
||||
creation.git.create_credential.credential_type=Credential Type
|
||||
creation.git.create_credential.credential_type_ssh_key=SSH Key
|
||||
|
@ -50,6 +50,12 @@ creation.git.create_credential.sshkey_title=SSH Private Key
|
|||
creation.git.create_credential.username_error=Please enter a valid username
|
||||
creation.git.create_credential.username_title=Username
|
||||
creation.git.create_credential.title=Add credential
|
||||
creation.git.create_credential.pw_instructions=Jenkins needs a user credential to authorize itself with git.
|
||||
creation.git.create_credential.option_existing=Use existing credential: {0}
|
||||
creation.git.create_credential.option_create_new=Create new credential
|
||||
creation.git.create_credential.invalid_username_password=Invalid username and/or password
|
||||
creation.git.create_credential.revoked_credential=Existing username / password invalid. Please enter new credentials.
|
||||
creation.git.create_credential.unexpected_error=Git credential validation failed with unexpected error. Please try again.
|
||||
|
||||
# creation for git
|
||||
creation.git.step1.create_button=Create Pipeline
|
||||
|
@ -60,7 +66,7 @@ creation.git.step1.credentials_error_invalid=Please select a valid credential.
|
|||
creation.git.step1.credentials_publickey_invalid=Unable to connect. Please make sure the Git server allows this SSH key.
|
||||
creation.git.step1.credentials_placeholder=System Default
|
||||
creation.git.step1.instructions=Any repository containing a Jenkinsfile will be built automatically. Not sure what we are talking about?
|
||||
creation.git.step1.instructions_link=Learn more about Jenkinsfile's.
|
||||
creation.git.step1.instructions_link=Learn more about Jenkinsfiles.
|
||||
creation.git.step1.repo_error_invalid=Please enter a valid URL.
|
||||
creation.git.step1.repo_error_required=Please enter a URL.
|
||||
creation.git.step1.repo_title=Repository URL
|
||||
|
@ -77,6 +83,7 @@ creation.git.step3.title_pipeline_create=Creating Pipeline...
|
|||
creation.git.credentials.register_ssh_key_instructions=You need to register this public SSH key with your Git server to continue -
|
||||
creation.git.credentials.copy_to_clipboard=Copy to clipboard
|
||||
creation.git.credentials.connect_and_validate=Connect
|
||||
|
||||
## github enterprise add server dialog
|
||||
creation.githubent.add_server.button_cancel=Cancel
|
||||
creation.githubent.add_server.button_create=Add Server
|
||||
|
|
|
@ -0,0 +1,260 @@
|
|||
import { assert } from 'chai';
|
||||
|
||||
import { mockExtensionsForI18n } from '../../mock-extensions-i18n';
|
||||
import { GitPWCredentialsManager, ManagerState } from '../../../../main/js/credentials/git/GitPWCredentialsManager';
|
||||
import { TypedError } from '../../../../main/js/credentials/TypedError';
|
||||
|
||||
import { LoadError, SaveError } from '../../../../main/js/credentials/bitbucket/BbCredentialsApi';
|
||||
|
||||
mockExtensionsForI18n();
|
||||
|
||||
// Using jest expect - https://facebook.github.io/jest/docs/en/expect.html
|
||||
|
||||
describe('GitPWCredentialsManager', () => {
|
||||
|
||||
const repoUrl = 'https://example.org/git/project.git';
|
||||
|
||||
let manager;
|
||||
let apiMock;
|
||||
|
||||
beforeEach(() => {
|
||||
apiMock = new GitPWCredentialsApiMock();
|
||||
manager = new GitPWCredentialsManager(apiMock);
|
||||
manager.configure(repoUrl, 'master');
|
||||
});
|
||||
|
||||
describe('findExistingCredential', () => {
|
||||
|
||||
it('behaves when not found', () => {
|
||||
expect.assertions(5);
|
||||
|
||||
expect(manager.state).toBe(ManagerState.PENDING_LOADING_CREDS);
|
||||
expect(manager.existingCredential).not.toBeDefined();
|
||||
|
||||
return manager.findExistingCredential()
|
||||
.then(credential => {
|
||||
expect(manager.state).toBe(ManagerState.NEW_REQUIRED);
|
||||
expect(manager.existingCredential).not.toBeDefined();
|
||||
expect(credential).not.toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('behaves when found', () => {
|
||||
apiMock.findExistingCredentialShouldSucceed = true;
|
||||
|
||||
expect.assertions(7);
|
||||
expect(manager.state).toBe(ManagerState.PENDING_LOADING_CREDS);
|
||||
expect(manager.existingCredential).not.toBeDefined();
|
||||
|
||||
return manager.findExistingCredential()
|
||||
.then(credential => {
|
||||
expect(manager.state).toBe(ManagerState.EXISTING_FOUND);
|
||||
expect(credential).toBeDefined();
|
||||
expect(credential.credentialId).toBe(apiMock.credentialId);
|
||||
expect(manager.existingCredential).toBeDefined();
|
||||
expect(manager.existingCredential.credentialId).toBe(apiMock.credentialId);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createCredential', () => {
|
||||
|
||||
it('updates state when create succeeds', () => {
|
||||
apiMock.findExistingCredentialShouldSucceed = true;
|
||||
|
||||
expect.assertions(10);
|
||||
|
||||
expect(manager.state).toBe(ManagerState.PENDING_LOADING_CREDS);
|
||||
expect(manager.existingCredential).not.toBeDefined();
|
||||
|
||||
const promise = manager.createCredential('userNameX', 'passwordX');
|
||||
|
||||
expect(manager.state).toBe(ManagerState.PENDING_VALIDATION);
|
||||
expect(manager.existingCredential).not.toBeDefined();
|
||||
|
||||
return promise.then(credential => {
|
||||
expect(manager.state).toBe(ManagerState.SAVE_SUCCESS);
|
||||
expect(credential).toBeDefined();
|
||||
expect(credential.credentialId).toBe(apiMock.credentialId);
|
||||
expect(manager.existingCredential).toBeDefined();
|
||||
expect(manager.existingCredential.credentialId).toBe(apiMock.credentialId);
|
||||
expect(apiMock.capturedCreateParams).toMatchObject({
|
||||
repositoryUrl: repoUrl,
|
||||
userName: 'userNameX',
|
||||
password: 'passwordX',
|
||||
branchName: 'master',
|
||||
requirePush: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('passes requirePush', () => {
|
||||
apiMock.findExistingCredentialShouldSucceed = true;
|
||||
|
||||
expect.assertions(6);
|
||||
|
||||
expect(manager.state).toBe(ManagerState.PENDING_LOADING_CREDS);
|
||||
|
||||
const promise = manager.createCredential('userNameX', 'passwordX', true);
|
||||
|
||||
expect(manager.state).toBe(ManagerState.PENDING_VALIDATION);
|
||||
|
||||
return promise.then(credential => {
|
||||
expect(manager.state).toBe(ManagerState.SAVE_SUCCESS);
|
||||
expect(credential).toBeDefined();
|
||||
expect(credential.credentialId).toBe(apiMock.credentialId);
|
||||
expect(apiMock.capturedCreateParams).toMatchObject({
|
||||
repositoryUrl: repoUrl,
|
||||
userName: 'userNameX',
|
||||
password: 'passwordX',
|
||||
branchName: 'master',
|
||||
requirePush: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('clears existing while creating', () => {
|
||||
apiMock.createCredentialShouldSucceed = true;
|
||||
apiMock.findExistingCredentialShouldSucceed = true;
|
||||
|
||||
expect.assertions(13);
|
||||
|
||||
return manager.findExistingCredential()
|
||||
.then(credential => {
|
||||
expect(manager.state).toBe(ManagerState.EXISTING_FOUND);
|
||||
expect(credential).toBeDefined();
|
||||
expect(credential.credentialId).toBe(apiMock.credentialId);
|
||||
expect(manager.existingCredential).toBeDefined();
|
||||
expect(manager.existingCredential.credentialId).toBe(apiMock.credentialId);
|
||||
|
||||
apiMock.credentialId = "newId";
|
||||
|
||||
const nextPromise = manager.createCredential('userNameX', 'passwordX', true);
|
||||
|
||||
expect(manager.state).toBe(ManagerState.PENDING_VALIDATION);
|
||||
expect(manager.existingCredential).not.toBeDefined();
|
||||
|
||||
return nextPromise;
|
||||
})
|
||||
.then(credential => {
|
||||
expect(manager.state).toBe(ManagerState.SAVE_SUCCESS);
|
||||
expect(credential).toBeDefined();
|
||||
expect(credential.credentialId).toBe(apiMock.credentialId);
|
||||
expect(manager.existingCredential).toBeDefined();
|
||||
expect(manager.existingCredential.credentialId).toBe(apiMock.credentialId);
|
||||
expect(apiMock.capturedCreateParams).toMatchObject({
|
||||
repositoryUrl: repoUrl,
|
||||
userName: 'userNameX',
|
||||
password: 'passwordX',
|
||||
branchName: 'master',
|
||||
requirePush: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('updates state when create fails with not found', () => {
|
||||
apiMock.createCredentialShouldSucceed = false;
|
||||
|
||||
expect.assertions(7);
|
||||
|
||||
expect(manager.state).toBe(ManagerState.PENDING_LOADING_CREDS);
|
||||
|
||||
const promise = manager.createCredential('userNameX', 'passwordX', true);
|
||||
|
||||
expect(manager.state).toBe(ManagerState.PENDING_VALIDATION);
|
||||
|
||||
return promise.catch(error => {
|
||||
expect(error).toBeDefined();
|
||||
expect(error).toBeInstanceOf(TypedError);
|
||||
expect(manager.state).toBe(ManagerState.INVALID_CREDENTIAL);
|
||||
expect(manager.existingCredential).not.toBeDefined();
|
||||
expect(apiMock.capturedCreateParams).toMatchObject({
|
||||
repositoryUrl: repoUrl,
|
||||
userName: 'userNameX',
|
||||
password: 'passwordX',
|
||||
branchName: 'master',
|
||||
requirePush: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('updates state when create fails with mystery meat', () => {
|
||||
apiMock.createCredentialShouldSucceed = false;
|
||||
apiMock.createCredentialShouldHaveWeirdFailure = true;
|
||||
|
||||
expect.assertions(7);
|
||||
|
||||
expect(manager.state).toBe(ManagerState.PENDING_LOADING_CREDS);
|
||||
|
||||
const promise = manager.createCredential('userNameX', 'passwordX', true);
|
||||
|
||||
expect(manager.state).toBe(ManagerState.PENDING_VALIDATION);
|
||||
|
||||
return promise.catch(error => {
|
||||
expect(error).toBeDefined();
|
||||
expect(error).toBeInstanceOf(TypedError);
|
||||
expect(manager.state).toBe(ManagerState.UNEXPECTED_ERROR_CREDENTIAL);
|
||||
expect(manager.existingCredential).not.toBeDefined();
|
||||
expect(apiMock.capturedCreateParams).toMatchObject({
|
||||
repositoryUrl: repoUrl,
|
||||
userName: 'userNameX',
|
||||
password: 'passwordX',
|
||||
branchName: 'master',
|
||||
requirePush: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// Helpers
|
||||
|
||||
function later(promiseResolver) {
|
||||
return new Promise((resolve, reject) => {
|
||||
process.nextTick(() => {
|
||||
try {
|
||||
resolve(promiseResolver());
|
||||
}
|
||||
catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class GitPWCredentialsApiMock /* FIXME: implements GitPWCredentialsApiPublic */ {
|
||||
|
||||
findExistingCredentialShouldSucceed = false;
|
||||
createCredentialShouldSucceed = true;
|
||||
createCredentialShouldHaveWeirdFailure = false;
|
||||
|
||||
credentialId = 'someCredentialId';
|
||||
|
||||
capturedCreateParams = {};
|
||||
|
||||
findExistingCredential(repositoryUrl) {
|
||||
return later(() => {
|
||||
if (this.findExistingCredentialShouldSucceed) {
|
||||
return {
|
||||
credentialId: this.credentialId,
|
||||
};
|
||||
}
|
||||
throw new TypedError(LoadError.TOKEN_NOT_FOUND);
|
||||
});
|
||||
}
|
||||
|
||||
createCredential(repositoryUrl, userName, password, branchName, requirePush) {
|
||||
this.capturedCreateParams = { repositoryUrl, userName, password, branchName, requirePush };
|
||||
|
||||
return later(() => {
|
||||
if (this.createCredentialShouldSucceed) {
|
||||
return {};
|
||||
}
|
||||
if (this.createCredentialShouldHaveWeirdFailure) {
|
||||
throw new TypedError(SaveError.UNKNOWN_ERROR);
|
||||
}
|
||||
throw new TypedError(SaveError.INVALID_CREDENTIAL);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -24,6 +24,7 @@
|
|||
package io.jenkins.blueocean.blueocean_git_pipeline;
|
||||
|
||||
import com.cloudbees.plugins.credentials.common.StandardCredentials;
|
||||
import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
|
||||
import hudson.model.User;
|
||||
import io.jenkins.blueocean.commons.ServiceException;
|
||||
import io.jenkins.blueocean.credential.CredentialsUtils;
|
||||
|
@ -61,17 +62,26 @@ abstract class GitReadSaveRequest {
|
|||
this.contents = contents == null ? null : contents.clone(); // grr findbugs
|
||||
}
|
||||
|
||||
@CheckForNull StandardCredentials getCredential() {
|
||||
@CheckForNull
|
||||
StandardCredentials getCredential() {
|
||||
StandardCredentials credential = null;
|
||||
|
||||
User user = User.current();
|
||||
if (user == null) {
|
||||
throw new ServiceException.UnauthorizedException("Not authenticated");
|
||||
}
|
||||
|
||||
// Get committer info and credentials
|
||||
if (GitUtils.isSshUrl(gitSource.getRemote()) || GitUtils.isLocalUnixFileUrl(gitSource.getRemote())) {
|
||||
// Get committer info and credentials
|
||||
User user = User.current();
|
||||
if (user == null) {
|
||||
throw new ServiceException.UnauthorizedException("Not authenticated");
|
||||
}
|
||||
credential = UserSSHKeyManager.getOrCreate(user);
|
||||
} else {
|
||||
throw new ServiceException.UnauthorizedException("Editing only supported for repositories using SSH");
|
||||
String credentialId = GitScm.makeCredentialId(gitSource.getRemote());
|
||||
|
||||
if (credentialId != null) {
|
||||
credential = CredentialsUtils.findCredential(credentialId,
|
||||
StandardCredentials.class,
|
||||
new BlueOceanDomainRequirement());
|
||||
}
|
||||
}
|
||||
return credential;
|
||||
}
|
||||
|
|
|
@ -23,15 +23,20 @@
|
|||
*/
|
||||
package io.jenkins.blueocean.blueocean_git_pipeline;
|
||||
|
||||
import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
|
||||
import hudson.Extension;
|
||||
import hudson.model.Item;
|
||||
import hudson.model.User;
|
||||
import hudson.remoting.Base64;
|
||||
import io.jenkins.blueocean.commons.ServiceException;
|
||||
import io.jenkins.blueocean.credential.CredentialsUtils;
|
||||
import io.jenkins.blueocean.rest.impl.pipeline.ScmContentProvider;
|
||||
import io.jenkins.blueocean.rest.impl.pipeline.credential.BlueOceanDomainRequirement;
|
||||
import io.jenkins.blueocean.rest.impl.pipeline.scm.GitContent;
|
||||
|
||||
import java.io.IOException;
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
import jenkins.branch.MultiBranchProject;
|
||||
import jenkins.plugins.git.GitSCMSource;
|
||||
import jenkins.scm.api.SCMSource;
|
||||
|
@ -46,7 +51,8 @@ import org.kohsuke.stapler.StaplerRequest;
|
|||
*/
|
||||
@Extension
|
||||
public class GitReadSaveService extends ScmContentProvider {
|
||||
@Nonnull private static ReadSaveType TYPE = ReadSaveType.DEFAULT;
|
||||
@Nonnull
|
||||
private static ReadSaveType TYPE = ReadSaveType.DEFAULT;
|
||||
|
||||
/**
|
||||
* Type of git interaction to use
|
||||
|
@ -90,20 +96,20 @@ public class GitReadSaveService extends ScmContentProvider {
|
|||
}
|
||||
|
||||
static GitReadSaveRequest makeSaveRequest(
|
||||
Item item,String branch, String commitMessage,
|
||||
String sourceBranch, String filePath, byte[] contents) {
|
||||
Item item, String branch, String commitMessage,
|
||||
String sourceBranch, String filePath, byte[] contents) {
|
||||
String defaultBranch = "master";
|
||||
GitSCMSource gitSource = null;
|
||||
if (item instanceof MultiBranchProject<?,?>) {
|
||||
MultiBranchProject<?,?> mbp = (MultiBranchProject<?,?>)item;
|
||||
if (item instanceof MultiBranchProject<?, ?>) {
|
||||
MultiBranchProject<?, ?> mbp = (MultiBranchProject<?, ?>) item;
|
||||
for (SCMSource s : mbp.getSCMSources()) {
|
||||
if (s instanceof GitSCMSource) {
|
||||
gitSource = (GitSCMSource)s;
|
||||
gitSource = (GitSCMSource) s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch(TYPE) {
|
||||
switch (TYPE) {
|
||||
case CLONE:
|
||||
return new GitCloneReadSaveRequest(
|
||||
gitSource,
|
||||
|
@ -137,11 +143,11 @@ public class GitReadSaveService extends ScmContentProvider {
|
|||
private GitReadSaveRequest makeSaveRequest(Item item, StaplerRequest req) {
|
||||
String branch = req.getParameter("branch");
|
||||
return makeSaveRequest(item,
|
||||
branch,
|
||||
req.getParameter("commitMessage"),
|
||||
ObjectUtils.defaultIfNull(req.getParameter("sourceBranch"), branch),
|
||||
req.getParameter("path"),
|
||||
Base64.decode(req.getParameter("contents"))
|
||||
branch,
|
||||
req.getParameter("commitMessage"),
|
||||
ObjectUtils.defaultIfNull(req.getParameter("sourceBranch"), branch),
|
||||
req.getParameter("path"),
|
||||
Base64.decode(req.getParameter("contents"))
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -149,11 +155,11 @@ public class GitReadSaveService extends ScmContentProvider {
|
|||
JSONObject content = json.getJSONObject("content");
|
||||
String branch = content.getString("branch");
|
||||
return makeSaveRequest(item,
|
||||
branch,
|
||||
content.getString("message"),
|
||||
content.has("sourceBranch") ? content.getString("sourceBranch") : branch,
|
||||
content.getString("path"),
|
||||
Base64.decode(content.getString("base64Data"))
|
||||
branch,
|
||||
content.getString("message"),
|
||||
content.has("sourceBranch") ? content.getString("sourceBranch") : branch,
|
||||
content.getString("path"),
|
||||
Base64.decode(content.getString("base64Data"))
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -166,13 +172,15 @@ public class GitReadSaveService extends ScmContentProvider {
|
|||
}
|
||||
|
||||
GitReadSaveRequest r = makeSaveRequest(item, req);
|
||||
|
||||
try {
|
||||
String encoded = Base64.encode(r.read());
|
||||
return new GitFile(
|
||||
new GitContent(r.filePath, user.getId(), r.gitSource.getRemote(), r.filePath, 0, "sha", encoded, "", r.branch, r.sourceBranch, true, "")
|
||||
);
|
||||
final byte[] reqData = r.read();
|
||||
String encoded = Base64.encode(reqData);
|
||||
|
||||
final GitContent content = new GitContent(r.filePath, user.getId(), r.gitSource.getRemote(), r.filePath, 0, "sha", encoded, "", r.branch, r.sourceBranch, true, "");
|
||||
final GitFile gitFile = new GitFile(content);
|
||||
return gitFile;
|
||||
} catch (ServiceException.UnauthorizedException e) {
|
||||
//if (r.gitSource.getRemote().matches("([^@:]+@.*|ssh://.*)"))
|
||||
throw new ServiceException.PreconditionRequired("Invalid credential", e);
|
||||
} catch (IOException e) {
|
||||
throw new ServiceException.UnexpectedErrorException("Unable to get file content", e);
|
||||
|
@ -204,4 +212,29 @@ public class GitReadSaveService extends ScmContentProvider {
|
|||
public boolean support(@Nonnull Item item) {
|
||||
return getApiUrl(item) != null;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
protected StandardUsernamePasswordCredentials getCredentialForUser(@Nonnull Item item, @Nonnull String repositoryUrl) {
|
||||
|
||||
User user = User.current();
|
||||
if (user == null) { //ensure this session has authenticated user
|
||||
throw new ServiceException.UnauthorizedException("No logged in user found");
|
||||
}
|
||||
|
||||
String credentialId = GitScm.makeCredentialId(repositoryUrl);
|
||||
StandardUsernamePasswordCredentials credential = null;
|
||||
|
||||
if (credentialId != null) {
|
||||
credential = CredentialsUtils.findCredential(credentialId,
|
||||
StandardUsernamePasswordCredentials.class,
|
||||
new BlueOceanDomainRequirement());
|
||||
}
|
||||
|
||||
if (credential == null) {
|
||||
throw new ServiceException.UnauthorizedException("No credential found for " + credentialId + " for user " + user.getDisplayName());
|
||||
}
|
||||
|
||||
return credential;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -2,15 +2,24 @@ package io.jenkins.blueocean.blueocean_git_pipeline;
|
|||
|
||||
import com.cloudbees.plugins.credentials.CredentialsMatchers;
|
||||
import com.cloudbees.plugins.credentials.CredentialsProvider;
|
||||
import com.cloudbees.plugins.credentials.CredentialsScope;
|
||||
import com.cloudbees.plugins.credentials.common.StandardCredentials;
|
||||
import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
|
||||
import com.cloudbees.plugins.credentials.domains.DomainRequirement;
|
||||
import com.cloudbees.plugins.credentials.domains.DomainSpecification;
|
||||
import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import hudson.Extension;
|
||||
import hudson.model.User;
|
||||
import hudson.util.HttpResponses;
|
||||
import io.jenkins.blueocean.commons.ErrorMessage;
|
||||
import io.jenkins.blueocean.commons.ServiceException;
|
||||
import io.jenkins.blueocean.credential.CredentialsUtils;
|
||||
import io.jenkins.blueocean.rest.Reachable;
|
||||
import io.jenkins.blueocean.rest.hal.Link;
|
||||
import io.jenkins.blueocean.rest.impl.pipeline.credential.BlueOceanDomainRequirement;
|
||||
import io.jenkins.blueocean.rest.impl.pipeline.credential.BlueOceanDomainSpecification;
|
||||
import io.jenkins.blueocean.rest.impl.pipeline.scm.AbstractScm;
|
||||
import io.jenkins.blueocean.rest.impl.pipeline.scm.Scm;
|
||||
import io.jenkins.blueocean.rest.impl.pipeline.scm.ScmFactory;
|
||||
|
@ -23,23 +32,105 @@ import jenkins.plugins.git.GitSCMFileSystem;
|
|||
import jenkins.scm.api.SCMSourceOwner;
|
||||
import net.sf.json.JSONException;
|
||||
import net.sf.json.JSONObject;
|
||||
import org.apache.commons.codec.digest.DigestUtils;
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
import org.kohsuke.stapler.HttpResponse;
|
||||
import org.kohsuke.stapler.Stapler;
|
||||
import org.kohsuke.stapler.StaplerRequest;
|
||||
import org.kohsuke.stapler.json.JsonBody;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class GitScm extends AbstractScm {
|
||||
|
||||
public static final String ID = "git";
|
||||
|
||||
static final String CREDENTIAL_DOMAIN_NAME = "blueocean-git-domain";
|
||||
static final String CREDENTIAL_DESCRIPTION_PW = "Git username/password";
|
||||
|
||||
protected final Reachable parent;
|
||||
|
||||
public GitScm(Reachable parent) {
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the credentialId for a specific repositoryUrl (which will be normalized)
|
||||
* @param repositoryUrl
|
||||
* @return credentialId string
|
||||
*/
|
||||
public static String makeCredentialId(String repositoryUrl) {
|
||||
|
||||
final String normalizedUrl = normalizeServerUrl(repositoryUrl);
|
||||
|
||||
try {
|
||||
final java.net.URI uri = new URI(normalizedUrl);
|
||||
|
||||
// Require a host
|
||||
String host = uri.getHost();
|
||||
if (host == null || host.length() == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Only http(s) urls have a default credential ID keyed to the repo right now
|
||||
String scheme = uri.getScheme();
|
||||
if (scheme != null && scheme.startsWith("http")) {
|
||||
return String.format("%s:%s", ID, DigestUtils.sha256Hex(normalizedUrl));
|
||||
}
|
||||
} catch (URISyntaxException e) {
|
||||
// Fall through
|
||||
}
|
||||
|
||||
// Bad URL, or not a http(s) url
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize the URL to protocol, port, and host to use as part of the credential id
|
||||
* @param repositoryUrl
|
||||
* @return a normalized url without path, query, or fragment components
|
||||
*/
|
||||
private static String normalizeServerUrl(String repositoryUrl) {
|
||||
|
||||
if (repositoryUrl == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
java.net.URI uri = new URI(repositoryUrl).normalize();
|
||||
String scheme = uri.getScheme();
|
||||
|
||||
String host = uri.getHost() == null ? null : uri.getHost().toLowerCase(Locale.ENGLISH);
|
||||
int port = uri.getPort();
|
||||
if ("http".equals(scheme) && port == 80) {
|
||||
port = -1;
|
||||
} else if ("https".equals(scheme) && port == 443) {
|
||||
port = -1;
|
||||
} else if ("ssh".equals(scheme) && port == 22) {
|
||||
port = -1;
|
||||
} else if ("git".equals(scheme) && port == 9418) {
|
||||
port = -1;
|
||||
}
|
||||
return new URI(
|
||||
scheme,
|
||||
uri.getUserInfo(),
|
||||
host,
|
||||
port,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
).toASCIIString().replaceAll("/$", "");
|
||||
} catch (URISyntaxException e) {
|
||||
// Bad URL
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Link getLink() {
|
||||
return parent.getLink().rel("git");
|
||||
|
@ -57,11 +148,46 @@ public class GitScm extends AbstractScm {
|
|||
return "";
|
||||
}
|
||||
|
||||
protected StaplerRequest getStaplerRequest() {
|
||||
StaplerRequest request = Stapler.getCurrentRequest();
|
||||
Preconditions.checkNotNull(request, "Must be called in HTTP request context");
|
||||
return request;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCredentialId() {
|
||||
// Only return the generated id if we actually have a credential that matches it
|
||||
StandardCredentials credential = getCredentialForCurrentRequest();
|
||||
if (credential != null) {
|
||||
return credential.getId();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected StandardCredentials getCredentialForCurrentRequest() {
|
||||
final StaplerRequest request = getStaplerRequest();
|
||||
|
||||
String credentialId = null;
|
||||
|
||||
if (request.hasParameter("credentialId")) {
|
||||
credentialId = request.getParameter("credentialId");
|
||||
} else {
|
||||
if (!request.hasParameter("repositoryUrl")) {
|
||||
// No linked credential unless a specific repo
|
||||
return null;
|
||||
}
|
||||
|
||||
String repositoryUrl = request.getParameter("repositoryUrl");
|
||||
credentialId = makeCredentialId(repositoryUrl);
|
||||
}
|
||||
|
||||
if (credentialId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return CredentialsUtils.findCredential(credentialId, StandardCredentials.class, new BlueOceanDomainRequirement());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Container<ScmOrganization> getOrganizations() {
|
||||
return null;
|
||||
|
@ -74,9 +200,14 @@ public class GitScm extends AbstractScm {
|
|||
|
||||
@Override
|
||||
public HttpResponse validateAndCreate(@JsonBody JSONObject request) {
|
||||
|
||||
boolean requirePush = request.has("requirePush");
|
||||
|
||||
// --[ Grab repo url and SCMSource ]----------------------------------------------------------
|
||||
|
||||
final String repositoryUrl;
|
||||
final AbstractGitSCMSource scmSource;
|
||||
|
||||
if (request.has("repositoryUrl")) {
|
||||
scmSource = null;
|
||||
repositoryUrl = request.getString("repositoryUrl");
|
||||
|
@ -90,60 +221,130 @@ public class GitScm extends AbstractScm {
|
|||
} else {
|
||||
return HttpResponses.errorJSON("No repository found for: " + fullName);
|
||||
}
|
||||
} catch(JSONException e) {
|
||||
} catch (JSONException e) {
|
||||
return HttpResponses.errorJSON("No repositoryUrl or pipeline.fullName specified in request.");
|
||||
} catch(RuntimeException e) {
|
||||
} catch (RuntimeException e) {
|
||||
return HttpResponses.errorWithoutStack(ServiceException.INTERNAL_SERVER_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
String credentialId = request.getString("credentialId");
|
||||
User user = User.current();
|
||||
if (user == null) {
|
||||
throw new ServiceException.UnauthorizedException("Not authenticated");
|
||||
}
|
||||
final StandardCredentials creds = CredentialsMatchers.firstOrNull(
|
||||
CredentialsProvider.lookupCredentials(
|
||||
StandardCredentials.class,
|
||||
Jenkins.getInstance(),
|
||||
Jenkins.getAuthentication(),
|
||||
(List<DomainRequirement>) null),
|
||||
CredentialsMatchers.allOf(CredentialsMatchers.withId(credentialId))
|
||||
);
|
||||
// --[ Grab user ]-------------------------------------------------------------------------------------
|
||||
|
||||
if (creds == null) {
|
||||
throw new ServiceException.NotFoundException("No credentials found for: " + credentialId);
|
||||
}
|
||||
User user = User.current();
|
||||
if (user == null) {
|
||||
throw new ServiceException.UnauthorizedException("Not authenticated");
|
||||
}
|
||||
|
||||
// --[ Get credential id from request or create from repo url ]----------------------------------------
|
||||
|
||||
String credentialId = null;
|
||||
|
||||
if (request.has("credentialId")) {
|
||||
credentialId = request.getString("credentialId");
|
||||
}
|
||||
|
||||
if (credentialId == null) {
|
||||
credentialId = makeCredentialId(repositoryUrl);
|
||||
}
|
||||
|
||||
if (credentialId == null) {
|
||||
// Still null? Must be a bad repoURL
|
||||
throw new ServiceException.BadRequestException("Invalid URL \"" + repositoryUrl + "\"");
|
||||
}
|
||||
|
||||
// --[ Load or create credentials ]--------------------------------------------------------------------
|
||||
|
||||
// Create new is only for username + password
|
||||
if (request.has("userName") || request.has("password")) {
|
||||
createPWCredentials(credentialId, user, request, repositoryUrl);
|
||||
}
|
||||
|
||||
final StandardCredentials creds = CredentialsMatchers.firstOrNull(
|
||||
CredentialsProvider.lookupCredentials(
|
||||
StandardCredentials.class,
|
||||
Jenkins.getInstance(),
|
||||
Jenkins.getAuthentication(),
|
||||
(List<DomainRequirement>) null),
|
||||
CredentialsMatchers.allOf(CredentialsMatchers.withId(credentialId))
|
||||
);
|
||||
|
||||
if (creds == null) {
|
||||
throw new ServiceException.NotFoundException("No credentials found for: " + credentialId);
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
if (requirePush) {
|
||||
String branch = request.getString("branch");
|
||||
new GitBareRepoReadSaveRequest(scmSource, branch, null, branch, null, null)
|
||||
.invokeOnScm(new GitSCMFileSystem.FSFunction<Void>() {
|
||||
@Override
|
||||
public Void invoke(Repository repository) throws IOException, InterruptedException {
|
||||
GitUtils.validatePushAccess(repository, repositoryUrl, creds);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
@Override
|
||||
public Void invoke(Repository repository) throws IOException, InterruptedException {
|
||||
GitUtils.validatePushAccess(repository, repositoryUrl, creds);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
List<ErrorMessage.Error> errors = GitUtils.validateCredentials(repositoryUrl, creds);
|
||||
if (!errors.isEmpty()) {
|
||||
throw new ServiceException.UnauthorizedException(errors.get(0).getMessage());
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
return HttpResponses.errorWithoutStack(ServiceException.PRECONDITION_REQUIRED, e.getMessage());
|
||||
} catch (Exception e) {
|
||||
String message = e.getMessage();
|
||||
if (message != null && message.contains("TransportException")) {
|
||||
throw new ServiceException.PreconditionRequired("Repository URL unreachable: " + repositoryUrl);
|
||||
}
|
||||
|
||||
return HttpResponses.errorWithoutStack(ServiceException.PRECONDITION_REQUIRED, message);
|
||||
}
|
||||
|
||||
return HttpResponses.okJSON();
|
||||
}
|
||||
|
||||
private void createPWCredentials(String credentialId, User user, @JsonBody JSONObject request, String repositoryUrl) {
|
||||
|
||||
StandardUsernamePasswordCredentials existingCredential =
|
||||
CredentialsUtils.findCredential(credentialId,
|
||||
StandardUsernamePasswordCredentials.class,
|
||||
new BlueOceanDomainRequirement());
|
||||
|
||||
String requestUsername = request.getString("userName");
|
||||
String requestPassword = request.getString("password");
|
||||
|
||||
// Un-normalized repositoryUrl so the description matches user input.
|
||||
String description = String.format("%s for %s", CREDENTIAL_DESCRIPTION_PW, repositoryUrl);
|
||||
|
||||
final StandardUsernamePasswordCredentials newCredential =
|
||||
new UsernamePasswordCredentialsImpl(CredentialsScope.USER,
|
||||
credentialId,
|
||||
description,
|
||||
requestUsername,
|
||||
requestPassword);
|
||||
|
||||
try {
|
||||
if (existingCredential == null) {
|
||||
CredentialsUtils.createCredentialsInUserStore(newCredential,
|
||||
user,
|
||||
CREDENTIAL_DOMAIN_NAME,
|
||||
ImmutableList.<DomainSpecification>of(new BlueOceanDomainSpecification()));
|
||||
} else {
|
||||
CredentialsUtils.updateCredentialsInUserStore(existingCredential,
|
||||
newCredential,
|
||||
user,
|
||||
CREDENTIAL_DOMAIN_NAME,
|
||||
ImmutableList.<DomainSpecification>of(new BlueOceanDomainSpecification()));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new ServiceException.UnexpectedErrorException("Could not persist credential", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Extension
|
||||
public static class GitScmFactory extends ScmFactory {
|
||||
@Override
|
||||
public Scm getScm(@Nonnull String id, @Nonnull Reachable parent) {
|
||||
if(id.equals(ID)){
|
||||
if (id.equals(ID)) {
|
||||
return new GitScm(parent);
|
||||
}
|
||||
return null;
|
||||
|
@ -155,4 +356,6 @@ public class GitScm extends AbstractScm {
|
|||
return new GitScm(parent);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -39,6 +39,11 @@ import static org.junit.Assert.*;
|
|||
*/
|
||||
@RunWith(Parameterized.class)
|
||||
public class GitScmTest extends PipelineBaseTest {
|
||||
public static final String HTTPS_GITHUB_NO_JENKINSFILE = "https://github.com/vivek/test-no-jenkins-file.git";
|
||||
public static final String HTTPS_GITHUB_PUBLIC = "https://github.com/cloudbeers/multibranch-demo.git";
|
||||
public static final String HTTPS_GITHUB_PUBLIC_HASH = "996e1f714b08e971ec79e3bea686287e66441f043177999a13dbc546d8fe402a";
|
||||
// ^ is DigestUtils.sha256Hex(normalizedUrl)
|
||||
|
||||
@Rule
|
||||
public GitSampleRepoRule sampleRepo = new GitSampleRepoRule();
|
||||
|
||||
|
@ -99,8 +104,8 @@ public class GitScmTest extends PipelineBaseTest {
|
|||
.post("/organizations/" + getOrgName() + "/pipelines/")
|
||||
.data(ImmutableMap.of("name", "demo",
|
||||
"$class", "io.jenkins.blueocean.blueocean_git_pipeline.GitPipelineCreateRequest",
|
||||
"scmConfig", ImmutableMap.of("uri", "https://github.com/vivek/test-no-jenkins-file.git",
|
||||
"credentialId", credentialId)
|
||||
"scmConfig", ImmutableMap.of("uri", HTTPS_GITHUB_NO_JENKINSFILE,
|
||||
"credentialId", credentialId)
|
||||
)).build(Map.class);
|
||||
|
||||
assertEquals("demo", r.get("name"));
|
||||
|
@ -354,6 +359,135 @@ public class GitScmTest extends PipelineBaseTest {
|
|||
assertNull(getOrgRoot().getItem("demo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotProvideIdForMissingCredentials() throws Exception {
|
||||
User user = login();
|
||||
String scmPath = "/organizations/" + getOrgName() + "/scm/git/";
|
||||
String repoPath = scmPath + "?repositoryUrl=" + HTTPS_GITHUB_PUBLIC;
|
||||
|
||||
Map resp = new RequestBuilder(baseUrl)
|
||||
.status(200)
|
||||
.jwtToken(getJwtToken(j.jenkins,user.getId(), user.getId()))
|
||||
.get(repoPath)
|
||||
.build(Map.class);
|
||||
|
||||
assertEquals(null, resp.get("credentialId"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldBePoliteAboutBadUrl() throws Exception {
|
||||
User user = login();
|
||||
String scmPath = "/organizations/" + getOrgName() + "/scm/git/";
|
||||
// Let's say the user has only started typing a url
|
||||
String repoPath = scmPath + "?repositoryUrl=htt";
|
||||
|
||||
Map resp = new RequestBuilder(baseUrl)
|
||||
.status(200)
|
||||
.jwtToken(getJwtToken(j.jenkins,user.getId(), user.getId()))
|
||||
.get(repoPath)
|
||||
.build(Map.class);
|
||||
|
||||
assertEquals(null, resp.get("credentialId"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCreateCredentialsWithDefaultId() throws Exception {
|
||||
User user = login();
|
||||
|
||||
String scmPath = "/organizations/" + getOrgName() + "/scm/git/";
|
||||
|
||||
// First create a credential
|
||||
String scmValidatePath = scmPath + "validate";
|
||||
|
||||
// We're relying on github letting you do a git-ls for repos with bad creds so long as they're public
|
||||
Map params = ImmutableMap.of(
|
||||
"userName", "someguy",
|
||||
"password", "password",
|
||||
"repositoryUrl", HTTPS_GITHUB_PUBLIC
|
||||
);
|
||||
|
||||
Map resp = new RequestBuilder(baseUrl)
|
||||
.status(200)
|
||||
.jwtToken(getJwtToken(j.jenkins,user.getId(), user.getId()))
|
||||
.data(params)
|
||||
.put(scmValidatePath)
|
||||
.build(Map.class);
|
||||
|
||||
assertEquals("ok", resp.get("status"));
|
||||
|
||||
// Now get the default credentialId
|
||||
|
||||
String repoPath = scmPath + "?repositoryUrl=" + HTTPS_GITHUB_PUBLIC;
|
||||
|
||||
Map resp2 = new RequestBuilder(baseUrl)
|
||||
.status(200)
|
||||
.jwtToken(getJwtToken(j.jenkins,user.getId(), user.getId()))
|
||||
.get(repoPath)
|
||||
.build(Map.class);
|
||||
|
||||
assertEquals("git:" + HTTPS_GITHUB_PUBLIC_HASH, resp2.get("credentialId"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that we get an error when using an invalid URL
|
||||
* @throws Exception
|
||||
*/
|
||||
@Test
|
||||
public void shouldNotCreateCredentialsForBadUrl1() throws Exception {
|
||||
User user = login();
|
||||
|
||||
String scmPath = "/organizations/" + getOrgName() + "/scm/git/";
|
||||
|
||||
// First create a credential
|
||||
String scmValidatePath = scmPath + "validate";
|
||||
|
||||
// We're relying on github letting you do a git-ls for repos with bad creds so long as they're public
|
||||
Map params = ImmutableMap.of(
|
||||
"userName", "someguy",
|
||||
"password", "password",
|
||||
"repositoryUrl", "htt"
|
||||
);
|
||||
|
||||
Map resp = new RequestBuilder(baseUrl)
|
||||
.status(400)
|
||||
.jwtToken(getJwtToken(j.jenkins,user.getId(), user.getId()))
|
||||
.data(params)
|
||||
.put(scmValidatePath)
|
||||
.build(Map.class);
|
||||
|
||||
assertTrue(resp.get("message").toString().toLowerCase().contains("invalid url"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that we get an error when using a valid but non-answering URL
|
||||
* @throws Exception
|
||||
*/
|
||||
@Test
|
||||
public void shouldNotCreateCredentialsForBadUrl2() throws Exception {
|
||||
User user = login();
|
||||
|
||||
String scmPath = "/organizations/" + getOrgName() + "/scm/git/";
|
||||
|
||||
// First create a credential
|
||||
String scmValidatePath = scmPath + "validate";
|
||||
|
||||
// We're relying on github letting you do a git-ls for repos with bad creds so long as they're public
|
||||
Map params = ImmutableMap.of(
|
||||
"userName", "someguy",
|
||||
"password", "password",
|
||||
"repositoryUrl", "http://example.org/has/no/repos.git"
|
||||
);
|
||||
|
||||
Map resp = new RequestBuilder(baseUrl)
|
||||
.status(428)
|
||||
.jwtToken(getJwtToken(j.jenkins,user.getId(), user.getId()))
|
||||
.data(params)
|
||||
.put(scmValidatePath)
|
||||
.build(Map.class);
|
||||
|
||||
assertTrue(resp.get("message").toString().toLowerCase().contains("url unreachable"));
|
||||
}
|
||||
|
||||
private String createMbp(User user) throws UnirestException {
|
||||
Map<String,Object> resp = new RequestBuilder(baseUrl)
|
||||
.status(201)
|
||||
|
@ -395,8 +529,8 @@ public class GitScmTest extends PipelineBaseTest {
|
|||
sampleRepo.write("file", "subsequent content1");
|
||||
sampleRepo.git("commit", "--all", "--message=tweaked1");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private String getOrgName() {
|
||||
return OrganizationFactory.getInstance().list().iterator().next().getName();
|
||||
|
@ -408,9 +542,9 @@ public class GitScmTest extends PipelineBaseTest {
|
|||
|
||||
@TestExtension
|
||||
public static class TestOrganizationFactoryImpl extends OrganizationFactoryImpl {
|
||||
|
||||
|
||||
public static String orgRoot;
|
||||
|
||||
|
||||
private OrganizationImpl instance;
|
||||
|
||||
public TestOrganizationFactoryImpl() {
|
||||
|
@ -426,7 +560,7 @@ public class GitScmTest extends PipelineBaseTest {
|
|||
} catch (IOException e) {
|
||||
throw new RuntimeException("Test setup failed!", e);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
else {
|
||||
instance = new OrganizationImpl("jenkins", Jenkins.getInstance());
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
package io.jenkins.blueocean.blueocean_git_pipeline;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* Unit tests fot GitScm that don't run via network
|
||||
*/
|
||||
public class GitScmTest2 {
|
||||
|
||||
@Test
|
||||
public void shouldMakeCredentialIdForHttp() {
|
||||
String result = GitScm.makeCredentialId("http://example.org/git/foo.git");
|
||||
Assert.assertEquals("git:http://example.org/git/foo.git", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldMakeCredentialIdForHttps() {
|
||||
String result = GitScm.makeCredentialId("https://example.org/git/foo.git");
|
||||
Assert.assertEquals("git:https://example.org/git/foo.git", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotMakeCredentialIdForSsh() {
|
||||
String result = GitScm.makeCredentialId("ssh://example.org/git/foo.git");
|
||||
Assert.assertEquals(null, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotMakeCredentialIdForGit() {
|
||||
String result = GitScm.makeCredentialId("git://example.org/git/foo.git");
|
||||
Assert.assertEquals(null, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotMakeCredentialIdForBadUrls() {
|
||||
Assert.assertEquals(null, GitScm.makeCredentialId("this is not a url worth mentioning"));
|
||||
Assert.assertEquals(null, GitScm.makeCredentialId("git://"));
|
||||
Assert.assertEquals(null, GitScm.makeCredentialId("http://"));
|
||||
Assert.assertEquals(null, GitScm.makeCredentialId(""));
|
||||
Assert.assertEquals(null, GitScm.makeCredentialId(null));
|
||||
}
|
||||
}
|
|
@ -14,6 +14,7 @@ import org.junit.Rule;
|
|||
import org.junit.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.Serializable;
|
||||
import java.util.Map;
|
||||
|
||||
public class GitUtilsTest extends PipelineBaseTest {
|
||||
|
@ -110,12 +111,12 @@ public class GitUtilsTest extends PipelineBaseTest {
|
|||
String id = (String)resp.get("id");
|
||||
Assert.assertTrue(id != null);
|
||||
|
||||
resp = put("/organizations/jenkins/scm/git/validate/",
|
||||
ImmutableMap.of(
|
||||
"repositoryUrl", "git@github.com:vivek/capability-annotation.git",
|
||||
"credentialId", id,
|
||||
"requirePush", true,
|
||||
"branch", "master")
|
||||
, 428);
|
||||
final Map<String, Object> body = ImmutableMap.of(
|
||||
"repositoryUrl", "git@github.com:vivek/capability-annotation.git",
|
||||
"credentialId", id,
|
||||
"requirePush", true,
|
||||
"branch", "master");
|
||||
|
||||
put("/organizations/jenkins/scm/git/validate/", body, 428);
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -52,7 +52,7 @@
|
|||
"nock": "8.0.0",
|
||||
"react-addons-test-utils": "15.4.2",
|
||||
"ts-jest": "19.0.2",
|
||||
"tsify": "3.0.4",
|
||||
"tsify": "4.0.0",
|
||||
"typescript": "2.7.2"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
|
@ -460,10 +460,10 @@ class PipelineLoader extends React.Component {
|
|||
const pipeline = pipelineService.getPipeline(this.href);
|
||||
const { scmSource } = pipeline;
|
||||
|
||||
if (!scmSource || !scmSource.id || (scmSource.id === 'git' && !isSshRepositoryUrl(scmSource.apiUrl))) {
|
||||
this.showLoadingError('', 'Saving Pipelines is unsupported using http/https repositories. Please use SSH instead.', 'No save access');
|
||||
return;
|
||||
}
|
||||
// if (!scmSource || !scmSource.id || (scmSource.id === 'git' && !isSshRepositoryUrl(scmSource.apiUrl))) {
|
||||
// this.showLoadingError('', 'Saving Pipelines is unsupported using http/https repositories. Please use SSH instead.', 'No save access');
|
||||
// return;
|
||||
// }
|
||||
|
||||
// if showing this dialog with a credential, the write test failed
|
||||
// except for git, where we need to prompt with the user's public key so they can continue
|
||||
|
@ -483,6 +483,8 @@ class PipelineLoader extends React.Component {
|
|||
// hide the dialog until it reports as ready (i.e. credential fetch is done)
|
||||
const dialogClassName = `dialog-token ${loading ? 'loading' : ''}`;
|
||||
|
||||
//FIXME: should show a message about existing credentials failing
|
||||
|
||||
this.setState({
|
||||
dialog: (
|
||||
<Dialog title={title} className={dialogClassName} buttons={[]} onDismiss={() => this.cancel()}>
|
||||
|
@ -496,6 +498,7 @@ class PipelineLoader extends React.Component {
|
|||
requirePush
|
||||
branch={branch}
|
||||
dialog
|
||||
existingFailed
|
||||
/>
|
||||
</Dialog>
|
||||
),
|
||||
|
@ -613,6 +616,7 @@ class PipelineLoader extends React.Component {
|
|||
if (branch || repo) {
|
||||
title += ' / ' + (branch || repo);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pipeline-page">
|
||||
<Extensions.Renderer extensionPoint="pipeline.editor.css" />
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Fetch, UrlBuilder } from '@jenkins-cd/blueocean-core-js';
|
||||
import TypedError from './TypedError';
|
||||
import { TypedError } from './TypedError';
|
||||
|
||||
const Base64 = {
|
||||
encode: data => btoa(data),
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* Utlity class for "typing" of server errors
|
||||
*/
|
||||
class TypedError {
|
||||
export class TypedError {
|
||||
constructor(type, serverError) {
|
||||
this.type = type;
|
||||
|
||||
|
@ -12,5 +12,3 @@ class TypedError {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default TypedError;
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -37,7 +37,7 @@
|
|||
"ncp": "2.0.0",
|
||||
"reactify": "1.1.1",
|
||||
"ts-jest": "19.0.2",
|
||||
"tsify": "3.0.4",
|
||||
"tsify": "4.0.0",
|
||||
"typescript": "2.7.2",
|
||||
"zombie": "4.2.1"
|
||||
},
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -93,7 +93,7 @@
|
|||
"react-addons-test-utils": "15.4.2",
|
||||
"run-sequence": "1.2.2",
|
||||
"ts-jest": "19.0.2",
|
||||
"tsify": "3.0.4",
|
||||
"tsify": "4.0.0",
|
||||
"typescript": "2.7.2",
|
||||
"watchify": "3.7.0"
|
||||
},
|
||||
|
|
|
@ -36,11 +36,12 @@ exports.install = function(builder) {
|
|||
// a bit painful.
|
||||
//
|
||||
process.env.NODE_ENV = builder.args.argvValue('--NODE_ENV', 'production');
|
||||
builder.onPreBundle(function (bundler) { // See https://github.com/jenkinsci/js-builder#onprebundle-listeners
|
||||
builder.onPreBundle(function(bundler) {
|
||||
// See https://github.com/jenkinsci/js-builder#onprebundle-listeners
|
||||
bundler.transform(require('envify'));
|
||||
});
|
||||
|
||||
builder.onPreBundle(function (bundler) {
|
||||
builder.onPreBundle(function(bundler) {
|
||||
var basedir = bundler._mdeps.basedir; // TODO is there a better way to get this info?
|
||||
var packageJson = require(basedir + '/package.json');
|
||||
var bundle = this;
|
||||
|
@ -67,12 +68,11 @@ exports.install = function(builder) {
|
|||
.import('@jenkins-cd/blueocean-core-js@any')
|
||||
.import('@jenkins-cd/logging')
|
||||
.import('react@any', {
|
||||
aliases: ['react/lib/React'] // in case a module requires react through the back door
|
||||
aliases: ['react/lib/React'], // in case a module requires react through the back door
|
||||
})
|
||||
.import('react-dom@any')
|
||||
.import('react-addons-css-transition-group@any')
|
||||
.import('redux@any')
|
||||
;
|
||||
.import('redux@any');
|
||||
}
|
||||
|
||||
if (!packageJson.name.startsWith('@jenkins-cd')) {
|
||||
|
|
|
@ -9,7 +9,7 @@ export function execute(done, config) {
|
|||
const Extensions = require('@jenkins-cd/js-extensions');
|
||||
const appRoot = document.getElementsByTagName('head')[0].getAttribute('data-appurl');
|
||||
const CoreJs = modules.requireModule('jenkins-cd-blueocean-core-js:jenkins-cd-blueocean-core-js@any');
|
||||
|
||||
|
||||
Extensions.init({
|
||||
extensionData: window.$blueocean.jsExtensions,
|
||||
classMetadataProvider: (type, cb) => {
|
||||
|
|
Loading…
Reference in New Issue