/* * Copyright 2016 ThoughtWorks, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.thoughtworks.go.config.materials.perforce; import com.thoughtworks.go.config.PasswordEncrypter; import com.thoughtworks.go.config.materials.PasswordAwareMaterial; import com.thoughtworks.go.config.materials.ScmMaterial; import com.thoughtworks.go.config.materials.ScmMaterialConfig; import com.thoughtworks.go.config.materials.SubprocessExecutionContext; import com.thoughtworks.go.domain.MaterialInstance; import com.thoughtworks.go.domain.MaterialRevision; import com.thoughtworks.go.domain.materials.*; import com.thoughtworks.go.domain.materials.perforce.P4Client; import com.thoughtworks.go.domain.materials.perforce.P4MaterialInstance; import com.thoughtworks.go.security.GoCipher; import com.thoughtworks.go.util.*; import com.thoughtworks.go.util.command.ConsoleOutputStreamConsumer; import com.thoughtworks.go.util.command.EnvironmentVariableContext; import com.thoughtworks.go.util.command.InMemoryStreamConsumer; import com.thoughtworks.go.util.command.UrlArgument; import org.apache.commons.io.FileUtils; import org.bouncycastle.crypto.InvalidCipherTextException; import javax.annotation.PostConstruct; import java.io.File; import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import static com.thoughtworks.go.util.ExceptionUtils.bomb; import static com.thoughtworks.go.util.ExceptionUtils.bombIfNull; import static com.thoughtworks.go.util.command.ProcessOutputStreamConsumer.inMemoryConsumer; import static java.lang.Long.parseLong; import static java.lang.String.format; public class P4Material extends ScmMaterial implements PasswordEncrypter, PasswordAwareMaterial { private String serverAndPort; private String userName; private String password; private String encryptedPassword; private Boolean useTickets = false; private P4MaterialView view; // Database stuff //TODO: use iBatis to set the type for us, and we can get rid of this field. public static final String TYPE = "P4Material"; private final GoCipher goCipher; private P4Material(GoCipher goCipher) { super(TYPE); this.goCipher = goCipher; } public P4Material(String serverAndPort, String view, GoCipher goCipher) { this(goCipher); bombIfNull(serverAndPort, "null serverAndPort"); this.serverAndPort = serverAndPort; setView(view); } public P4Material(String serverAndPort, String view) { this(serverAndPort, view, new GoCipher()); } public P4Material(String url, String view, String userName) { this(url, view); this.userName = userName; } public P4Material(String url, String view, String userName, String folder) { this(url, view, userName, folder, new GoCipher()); } public P4Material(P4MaterialConfig config) { this(config.getUrl(), config.getView(), config.getUserName(), config.getFolder(), config.getGoCipher()); this.name = config.getName(); this.autoUpdate = config.getAutoUpdate(); this.filter = config.rawFilter(); this.invertFilter = config.getInvertFilter(); setPassword(config.getPassword()); this.useTickets = config.getUseTickets(); } private P4Material(String serverAndPort, String view, String userName, String folder, GoCipher goCipher) { this(goCipher); bombIfNull(serverAndPort, "null serverAndPort"); this.serverAndPort = serverAndPort; setView(view); this.userName = userName; this.folder = folder; } @Override public MaterialConfig config() { return new P4MaterialConfig(serverAndPort, userName, getPassword(), useTickets, view == null ? null : view.getValue(), goCipher, name, autoUpdate, filter, invertFilter, folder); } public List<Modification> latestModification(File baseDir, final SubprocessExecutionContext execCtx) { P4Client p4 = getP4(execCtx.isServer() ? baseDir : workingdir(baseDir)); return p4.latestChange(); } public List<Modification> modificationsSince(File baseDir, Revision revision, final SubprocessExecutionContext execCtx) { P4Client p4 = getP4(execCtx.isServer() ? baseDir : workingdir(baseDir)); return p4.changesSince(revision); } public MaterialInstance createMaterialInstance() { return new P4MaterialInstance(serverAndPort, userName, view.getValue(), useTickets, UUID.randomUUID().toString()); } @Override protected void appendCriteria(Map<String, Object> parameters) { parameters.put(ScmMaterialConfig.URL, serverAndPort); parameters.put(ScmMaterialConfig.USERNAME, userName); parameters.put("view", view.getValue()); } @Override protected void appendAttributes(Map<String, Object> parameters) { appendCriteria(parameters); } protected P4Client getP4(File baseDir) { InMemoryStreamConsumer outputConsumer = inMemoryConsumer(); P4Client p4 = null; try { p4 = p4(baseDir, outputConsumer); } catch (Exception e) { bomb(e.getMessage() + " " + outputConsumer.getStdError(), e); } return p4; } public void updateTo(ConsoleOutputStreamConsumer outputConsumer, File baseDir, RevisionContext revisionContext, final SubprocessExecutionContext execCtx) { File workingDir = execCtx.isServer() ? baseDir : workingdir(baseDir); boolean cleaned = cleanDirectoryIfRepoChanged(workingDir, outputConsumer); String revision = revisionContext.getLatestRevision().getRevision(); try { outputConsumer.stdOutput(format("[%s] Start updating %s at revision %s from %s", GoConstants.PRODUCT_NAME, updatingTarget(), revision, serverAndPort)); p4(workingDir, outputConsumer).sync(parseLong(revision), cleaned, outputConsumer); outputConsumer.stdOutput(format("[%s] Done.\n", GoConstants.PRODUCT_NAME)); } catch (Exception e) { bomb(e); } } public ValidationBean checkConnection(final SubprocessExecutionContext execCtx) { File baseDir = new TempFiles().createUniqueFolder("for-p4"); try { getP4(baseDir).checkConnection(); return ValidationBean.valid(); } catch (Exception e) { return ValidationBean.notValid("Unable to connect to server " + serverAndPort + " : \n" + e.getMessage()); } finally{ FileUtil.deleteFolder(baseDir); } } public String getServerAndPort() { return serverAndPort; } public String getView() { return view == null ? null : view.getValue(); } public boolean isCheckExternals() { return false; } public String getUrl() { return serverAndPort; } @Override protected UrlArgument getUrlArgument() { return new UrlArgument(serverAndPort); } public String getLongDescription() { return format("URL: %s, View: %s, Username: %s", serverAndPort, view.getValue(), userName); } public String getUserName() { return userName; } public String getPassword() { return currentPassword(); } public void setPassword(String password) { resetPassword(password); } P4Client p4(File baseDir, ConsoleOutputStreamConsumer consumer) throws Exception { return _p4(baseDir, consumer, true); } /** * not for use externally, created for testing convenience */ P4Client _p4(File workDir, ConsoleOutputStreamConsumer consumer, boolean failOnError) throws Exception { String clientName = clientName(workDir); return P4Client.fromServerAndPort(getFingerprint(), serverAndPort, userName, getPassword(), clientName,this.useTickets, workDir, p4view(clientName), consumer, failOnError); } @Override public void populateEnvironmentContext(EnvironmentVariableContext environmentVariableContext, MaterialRevision materialRevision, final File baseDir) { super.populateEnvironmentContext(environmentVariableContext, materialRevision, baseDir); setVariableWithName(environmentVariableContext, clientName(baseDir), "GO_P4_CLIENT"); } @Override public Map<String, Object> getAttributes(boolean addSecureFields) { Map<String, Object> materialMap = new HashMap<>(); materialMap.put("type", "perforce"); Map<String, Object> configurationMap = new HashMap<>(); configurationMap.put("url", serverAndPort); configurationMap.put("username", userName); if (addSecureFields) { configurationMap.put("password", getPassword()); } configurationMap.put("view", getView()); configurationMap.put("use-tickets", useTickets); materialMap.put("perforce-configuration", configurationMap); return materialMap; } public Class getInstanceType() { return P4MaterialInstance.class; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } if (!super.equals(o)) { return false; } P4Material that = (P4Material) o; if (serverAndPort != null ? !serverAndPort.equals(that.serverAndPort) : that.serverAndPort != null) { return false; } if (useTickets != null ? !useTickets.equals(that.useTickets) : that.useTickets != null) { return false; } if (userName != null ? !userName.equals(that.userName) : that.userName != null) { return false; } if (view != null ? !view.equals(that.view) : that.view != null) { return false; } return true; } @Override public int hashCode() { int result = super.hashCode(); result = 31 * result + (serverAndPort != null ? serverAndPort.hashCode() : 0); result = 31 * result + (userName != null ? userName.hashCode() : 0); result = 31 * result + (useTickets != null ? useTickets.hashCode() : 0); result = 31 * result + (view != null ? view.hashCode() : 0); return result; } protected String getLocation() { return getServerAndPort(); } public String getTypeForDisplay() { return "Perforce"; } private String p4view(String clientName) { // rebuildViewIfJustLoadedFromDb(); return view.viewUsing(clientName); } public String clientName(File baseDir) { File workingdir = workingdir(baseDir); String hash = FileUtil.filesystemSafeFileHash(workingdir); return "cruise-" + SystemUtil.getLocalhostName() + "-" + workingdir.getName() + "-" + hash; } private boolean cleanDirectoryIfRepoChanged(File workingDirectory, ConsoleOutputStreamConsumer outputConsumer) { boolean cleaned = false; try { String p4RepoId = p4RepoId(); File file = new File(workingDirectory, ".cruise_p4repo"); if (!file.exists()) { FileUtils.writeStringToFile(file, p4RepoId); return true; } String existingRepoId = FileUtils.readFileToString(file); if (!p4RepoId.equals(existingRepoId)) { outputConsumer.stdOutput(format("[%s] Working directory has changed. Deleting and re-creating it.", GoConstants.PRODUCT_NAME)); FileUtils.deleteDirectory(workingDirectory); workingDirectory.mkdirs(); FileUtils.writeStringToFile(file, p4RepoId); cleaned = true; } return cleaned; } catch (IOException e) { throw bomb(e); } } private String p4RepoId() { return hasUser() ? userName + "@" + serverAndPort : serverAndPort; } private boolean hasUser() { return userName != null && !userName.trim().isEmpty(); } public boolean getUseTickets() { return this.useTickets; } public void setUseTickets(boolean useTickets) { this.useTickets = useTickets; } @Override public String toString() { return "P4Material{" + "serverAndPort='" + serverAndPort + '\'' + ", userName='" + userName + '\'' + ", view=" + view.getValue() + '}'; } public void setUsername(String userName) { this.userName = userName; } private void setView(String viewStr) { this.view = new P4MaterialView(viewStr); } private void resetPassword(String password) { if (StringUtil.isBlank(password)) { this.encryptedPassword = null; } setPasswordIfNotBlank(password); } private void setPasswordIfNotBlank(String password) { if (StringUtil.isBlank(password)) { return; } try { this.encryptedPassword = this.goCipher.encrypt(password); } catch (Exception e) { bomb("Password encryption failed. Please verify your cipher key.", e); } this.password = null; } @PostConstruct public void ensureEncrypted() { setPasswordIfNotBlank(password); } public String currentPassword() { try { return StringUtil.isBlank(encryptedPassword) ? null : this.goCipher.decrypt(encryptedPassword); } catch (InvalidCipherTextException e) { throw new RuntimeException("Could not decrypt the password to get the real password", e); } } public String getEncryptedPassword() { return encryptedPassword; } }