| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You 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 org.apache.ignite.tcservice; |
| |
| import com.google.common.base.Strings; |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.io.StringReader; |
| import java.io.UncheckedIOException; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.SortedSet; |
| import java.util.concurrent.atomic.AtomicReference; |
| import java.util.stream.Collectors; |
| import javax.annotation.Nonnull; |
| import javax.annotation.Nullable; |
| import javax.inject.Inject; |
| import javax.xml.bind.JAXBException; |
| import org.apache.ignite.tcbot.common.conf.IDataSourcesConfigSupplier; |
| import org.apache.ignite.tcbot.common.conf.ITcServerConfig; |
| import org.apache.ignite.tcbot.common.conf.TcBotWorkDir; |
| import org.apache.ignite.tcbot.common.exeption.ExceptionUtil; |
| import org.apache.ignite.tcbot.common.exeption.ServiceConflictException; |
| import org.apache.ignite.tcbot.common.interceptor.AutoProfiling; |
| import org.apache.ignite.tcbot.common.util.HttpUtil; |
| import org.apache.ignite.tcservice.http.ITeamcityHttpConnection; |
| import org.apache.ignite.tcservice.model.agent.Agent; |
| import org.apache.ignite.tcservice.model.agent.AgentsRef; |
| import org.apache.ignite.tcservice.model.changes.Change; |
| import org.apache.ignite.tcservice.model.changes.ChangesList; |
| import org.apache.ignite.tcservice.model.conf.BuildType; |
| import org.apache.ignite.tcservice.model.conf.Project; |
| import org.apache.ignite.tcservice.model.conf.ProjectsList; |
| import org.apache.ignite.tcservice.model.conf.bt.BuildTypeFull; |
| import org.apache.ignite.tcservice.model.hist.BuildRef; |
| import org.apache.ignite.tcservice.model.hist.Builds; |
| import org.apache.ignite.tcservice.model.mute.MuteInfo; |
| import org.apache.ignite.tcservice.model.mute.Mutes; |
| import org.apache.ignite.tcservice.model.result.Build; |
| import org.apache.ignite.tcservice.model.result.problems.ProblemOccurrences; |
| import org.apache.ignite.tcservice.model.result.stat.Statistics; |
| import org.apache.ignite.tcservice.model.result.tests.TestOccurrencesFull; |
| import org.apache.ignite.tcservice.model.user.User; |
| import org.apache.ignite.tcservice.model.user.Users; |
| import org.apache.ignite.tcservice.util.XmlUtil; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * Class for access to Teamcity REST API without any caching. |
| * |
| * See more info about API |
| * https://confluence.jetbrains.com/display/TCD10/REST+API |
| * https://developer.github.com/v3/ |
| */ |
| public class TeamcityServiceConnection implements ITeamcity { |
| /** Logger. */ |
| private static final Logger logger = LoggerFactory.getLogger(TeamcityServiceConnection.class); |
| |
| /** TeamCity authorization token. */ |
| private String basicAuthTok; |
| |
| /** Teamcity http connection. */ |
| @Inject private ITeamcityHttpConnection teamcityHttpConn; |
| |
| @Inject private IDataSourcesConfigSupplier cfg; |
| |
| private String srvCode; |
| |
| public void init(@Nullable String srvCode) { |
| this.srvCode = srvCode; |
| } |
| |
| @Override public ITcServerConfig config() { |
| return cfg.getTeamcityConfig(this.srvCode); |
| } |
| |
| /** {@inheritDoc} */ |
| @Override public void setAuthToken(String tok) { |
| basicAuthTok = tok; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override public boolean isTeamCityTokenAvailable() { |
| return basicAuthTok != null; |
| } |
| |
| /** {@inheritDoc} */ |
| @AutoProfiling |
| @Override public List<Agent> agents(boolean connected, boolean authorized) { |
| String url = "app/rest/agents?locator=connected:" + connected + ",authorized:" + authorized; |
| |
| return getJaxbUsingHref(url, AgentsRef.class) |
| .getAgent() |
| .stream() |
| .parallel() |
| .map(v -> getJaxbUsingHref(v.getHref(), Agent.class)) |
| .collect(Collectors.toList()); |
| } |
| |
| /** {@inheritDoc} */ |
| @AutoProfiling |
| public File downloadAndCacheBuildLog(int buildId) { |
| String buildIdStr = Integer.toString(buildId); |
| File file = new File(logsDir(), "build" + buildIdStr + ".log.zip"); |
| |
| if (file.exists() && file.canRead() && file.length() > 0) { |
| logger.info("Nothing to do, file is cached locally: [" + file + "]"); |
| |
| return file; |
| } |
| String url = host() + "downloadBuildLog.html" + "?buildId=" + buildIdStr + "&archived=true"; |
| |
| try { |
| HttpUtil.sendGetCopyToFile(basicAuthTok, url, file); |
| } |
| catch (IOException e) { |
| throw new UncheckedIOException(e); |
| } |
| return file; |
| } |
| |
| private static File resolveLogs(File workDir, String logsProp) { |
| final File logsDirFileConfigured = new File(logsProp); |
| return logsDirFileConfigured.isAbsolute() ? logsDirFileConfigured : new File(workDir, logsProp); |
| } |
| |
| private File logsDir() { |
| File logsDirFile = resolveLogs( |
| TcBotWorkDir.resolveWorkDir(), |
| config().logsDirectory()); |
| |
| return TcBotWorkDir.ensureDirExist(logsDirFile); |
| } |
| |
| |
| /** {@inheritDoc} */ |
| @AutoProfiling |
| @Override public Build triggerBuild( |
| String buildTypeId, |
| @Nonnull String branchName, |
| boolean cleanRebuild, |
| boolean queueAtTop, |
| @Nullable Map<String, Object> buildParms, |
| String freeTextComments) { |
| String triggeringOptions = |
| " <triggeringOptions" + |
| " cleanSources=\"" + cleanRebuild + "\"" + |
| " rebuildAllDependencies=\"" + cleanRebuild + "\"" + |
| " queueAtTop=\"" + queueAtTop + "\"" + |
| "/>\n"; |
| |
| String comments = " <comment><text>" + |
| Strings.nullToEmpty(freeTextComments) + ", " + |
| "Build triggered from Ignite TC Bot" + |
| " [cleanRebuild=" + cleanRebuild + ", top=" + queueAtTop + "]" + |
| "</text></comment>\n"; |
| |
| Map<String, Object> props = new HashMap<>(); |
| |
| if (buildParms != null) |
| props.putAll(buildParms); |
| |
| props.put(ITeamcity.TCBOT_TRIGGER_TIME, System.currentTimeMillis()); // |
| |
| StringBuilder sb = new StringBuilder(); |
| sb.append("<build branchName=\"").append(XmlUtil.xmlEscapeText(branchName)).append("\">\n"); |
| sb.append(" <buildType id=\"").append(buildTypeId).append("\"/>\n"); |
| sb.append(comments); |
| sb.append(triggeringOptions); |
| sb.append(" <properties>\n"); |
| |
| props.forEach((k, v) -> { |
| sb.append(" <property name=\"").append(k).append("\""); |
| sb.append(" value=\"").append(XmlUtil.xmlEscapeText(Objects.toString(v))).append("\"/>\n"); |
| }); |
| |
| sb.append(" </properties>\n"); |
| sb.append("</build>"); |
| |
| String url = host() + "app/rest/buildQueue"; |
| |
| try { |
| logger.info("Triggering build: buildTypeId={}, branchName={}, cleanRebuild={}, queueAtTop={}, buildParms={}", |
| buildTypeId, branchName, cleanRebuild, queueAtTop, props); |
| |
| String body = sb.toString(); |
| |
| if (logger.isDebugEnabled()) |
| logger.debug("(TRIGGER REQUEST):\n" + body); |
| |
| try (StringReader reader = new StringReader(HttpUtil.sendPostAsString(basicAuthTok, url, body))) { |
| return XmlUtil.load(Build.class, reader); |
| } |
| catch (JAXBException e) { |
| throw ExceptionUtil.propagateException(e); |
| } |
| } |
| catch (IOException e) { |
| throw new UncheckedIOException(e); |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| @AutoProfiling |
| @Override public ProblemOccurrences getProblems(int buildId) { |
| return getJaxbUsingHref("app/rest/latest/problemOccurrences" + |
| "?locator=build:(id:" + buildId + ")" + |
| "&fields=problemOccurrence(id,type,identity,href,details,build(id))", ProblemOccurrences.class); |
| } |
| |
| /** {@inheritDoc} */ |
| @AutoProfiling |
| @Override public Statistics getStatistics(int buildId) { |
| return getJaxbUsingHref("app/rest/latest/builds/id:" + buildId + "/statistics", Statistics.class); |
| } |
| |
| /** {@inheritDoc} */ |
| @AutoProfiling |
| @Override public ChangesList getChangesList(int buildId) { |
| String href = "app/rest/latest/changes" + |
| "?locator=build:(id:" + + buildId +")" + |
| "&fields=change(id)"; |
| |
| return getJaxbUsingHref(href, ChangesList.class); |
| } |
| |
| /** {@inheritDoc} */ |
| @AutoProfiling |
| @Override public Change getChange(int changeId) { |
| String href = "app/rest/latest/changes/id:" + +changeId; |
| |
| return getJaxbUsingHref(href, Change.class); |
| } |
| |
| /** {@inheritDoc} */ |
| @Override public List<Project> getProjects() { |
| return sendGetXmlParseJaxb(host() + "app/rest/latest/projects", ProjectsList.class).projects(); |
| } |
| |
| /** {@inheritDoc} */ |
| @Override public List<BuildType> getBuildTypes(String projectId) { |
| return sendGetXmlParseJaxb(host() + "app/rest/latest/projects/" + projectId, Project.class) |
| .getBuildTypesNonNull(); |
| } |
| |
| /** |
| * @param url Url. |
| * @param rootElem Root elem. |
| * |
| * @throws UncheckedIOException caused by FileNotFoundException - If not found (404) was returned from service. |
| * @throws ServiceConflictException If conflict (409) was returned from service. |
| * @throws IllegalStateException if some unexpected HTTP error returned. |
| * @throws UncheckedIOException in case communication failed. |
| */ |
| private <T> T sendGetXmlParseJaxb(String url, Class<T> rootElem) { |
| try { |
| try (InputStream inputStream = teamcityHttpConn.sendGet(basicAuthTok, url)) { |
| final InputStreamReader reader = new InputStreamReader(inputStream); |
| |
| return loadXml(rootElem, reader); |
| } |
| } |
| catch (IOException e) { |
| throw new UncheckedIOException(e); |
| } |
| catch (JAXBException e) { |
| throw ExceptionUtil.propagateException(e); |
| } |
| } |
| |
| @SuppressWarnings("WeakerAccess") |
| @AutoProfiling |
| protected <T> T loadXml(Class<T> rootElem, InputStreamReader reader) throws JAXBException { |
| return XmlUtil.load(rootElem, reader); |
| } |
| |
| /** {@inheritDoc} */ |
| @AutoProfiling |
| @Override public BuildTypeFull getBuildType(String buildTypeId) { |
| return sendGetXmlParseJaxb(host() + "app/rest/latest/buildTypes/id:" + |
| buildTypeId, BuildTypeFull.class); |
| } |
| |
| /** {@inheritDoc} */ |
| @AutoProfiling |
| @Override public Build getBuild(int buildId) { |
| return getJaxbUsingHref("app/rest/latest/builds/id:" + buildId, Build.class); |
| } |
| |
| /** |
| * @param href Href. |
| * @param elem Element class. |
| */ |
| private <T> T getJaxbUsingHref(String href, Class<T> elem) { |
| return sendGetXmlParseJaxb(host() + (href.startsWith("/") ? href.substring(1) : href), elem); |
| } |
| |
| /** {@inheritDoc} */ |
| @Override public String serverCode() { |
| return srvCode; |
| } |
| |
| |
| /** |
| * |
| * @throws RuntimeException in case loading failed. See details in {@link ITeamcityConn}. |
| */ |
| @AutoProfiling |
| public Users getUsers() { |
| return getJaxbUsingHref("app/rest/latest/users", Users.class); |
| } |
| |
| /** {@inheritDoc} */ |
| @AutoProfiling |
| @Override public User getUserByUsername(String username) { |
| return getJaxbUsingHref("app/rest/latest/users/username:" + username, User.class); |
| } |
| |
| /** |
| * @param teamcityHttpConn Teamcity http connection. |
| */ |
| public void setHttpConn(ITeamcityHttpConnection teamcityHttpConn) { |
| this.teamcityHttpConn = teamcityHttpConn; |
| } |
| |
| /** {@inheritDoc} */ |
| @AutoProfiling |
| @Override public List<BuildRef> getBuildRefsPage(String fullUrl, AtomicReference<String> outNextPage) { |
| String relPath = "app/rest/latest/builds?locator=defaultFilter:false"; |
| String relPathSelected = Strings.isNullOrEmpty(fullUrl) ? relPath : fullUrl; |
| String url = host() + (relPathSelected.startsWith("/") ? relPathSelected.substring(1) : relPathSelected); |
| Builds builds = sendGetXmlParseJaxb(url, Builds.class); |
| |
| outNextPage.set(Strings.emptyToNull(builds.nextHref())); |
| |
| return builds.getBuildsNonNull(); |
| } |
| |
| /** {@inheritDoc} */ |
| @Override public SortedSet<MuteInfo> getMutesPage(String buildTypeId, String fullUrl, AtomicReference<String> nextPage) { |
| String relPath = "app/rest/mutes?locator=project:(id:" + buildTypeId + ')'; |
| String relPathSelected = Strings.isNullOrEmpty(fullUrl) ? relPath : fullUrl; |
| String url = host() + (relPathSelected.startsWith("/") ? relPathSelected.substring(1) : relPathSelected); |
| |
| Mutes mutes = sendGetXmlParseJaxb(url, Mutes.class); |
| |
| nextPage.set(Strings.emptyToNull(mutes.nextHref())); |
| |
| return mutes.getMutesNonNull(); |
| } |
| |
| /** {@inheritDoc} */ |
| @AutoProfiling |
| @Override public TestOccurrencesFull getTestsPage(int buildId, @Nullable String href, boolean testDtls) { |
| String relPathSelected = Strings.isNullOrEmpty(href) ? testsStartHref(buildId, testDtls) : href; |
| String url = host() + (relPathSelected.startsWith("/") ? relPathSelected.substring(1) : relPathSelected); |
| return sendGetXmlParseJaxb(url, TestOccurrencesFull.class); |
| } |
| |
| /** {@inheritDoc} */ |
| @Override public boolean isTestOccurrencesInOtherBranches(Long testId, String branchName) { |
| String url = host() + "app/rest/latest/testOccurrences?locator=test:id:" + testId + |
| ",count:100&fields=testOccurrence(id,name,test,build)"; |
| TestOccurrencesFull testOccurrencesFull = sendGetXmlParseJaxb(url, TestOccurrencesFull.class); |
| boolean testMatch = testOccurrencesFull.getTests().stream() |
| .anyMatch(test -> !test.build.branchName().contains(branchName)); |
| return testMatch; |
| } |
| |
| /** |
| * @param buildId Build id. |
| * @param testDtls request test details string |
| */ |
| @Nonnull |
| private String testsStartHref(int buildId, boolean testDtls) { |
| String fieldList = "id,name," + |
| (testDtls ? "details," : "") + |
| "status,duration,muted,currentlyMuted,currentlyInvestigated,ignored,test(id),build(id)"; |
| |
| return "app/rest/latest/testOccurrences?locator=build:(id:" + |
| buildId + ")" + |
| "&fields=testOccurrence(" + fieldList + ")" + |
| "&count=1000)"; |
| } |
| } |