diff --git a/gradle.properties b/gradle.properties index 237555864..f1e913813 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ #Fri Feb 07 21:30:06 UTC 2020 fiatVersion=1.13.1 enablePublishing=false -spinnakerGradleVersion=7.0.1 korkVersion=7.16.7 +spinnakerGradleVersion=7.5.1 org.gradle.parallel=true diff --git a/igor-core/igor-core.gradle b/igor-core/igor-core.gradle index 1f5822110..5827208a8 100644 --- a/igor-core/igor-core.gradle +++ b/igor-core/igor-core.gradle @@ -1,4 +1,7 @@ dependencies { + implementation "org.springframework.boot:spring-boot-starter-web" + + implementation "com.netflix.spinnaker.fiat:fiat-core:$fiatVersion" implementation "com.netflix.spinnaker.kork:kork-artifacts" implementation "com.netflix.spinnaker.kork:kork-core" diff --git a/igor-core/src/main/java/com/netflix/spinnaker/igor/IgorConfigurationProperties.java b/igor-core/src/main/java/com/netflix/spinnaker/igor/IgorConfigurationProperties.java index 2afad5203..a0cc0a0ff 100644 --- a/igor-core/src/main/java/com/netflix/spinnaker/igor/IgorConfigurationProperties.java +++ b/igor-core/src/main/java/com/netflix/spinnaker/igor/IgorConfigurationProperties.java @@ -67,7 +67,7 @@ public static class BuildProperties { */ private int pollInterval = 60; - /** TODO(jc): Please document */ + /** TODO(jc): Please document TODO(rz): Duration */ private int lookBackWindowMins = 10 * 60 * 60; /** TODO(jc): Please document */ diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/build/model/GenericBuild.java b/igor-core/src/main/java/com/netflix/spinnaker/igor/build/model/GenericBuild.java similarity index 90% rename from igor-web/src/main/groovy/com/netflix/spinnaker/igor/build/model/GenericBuild.java rename to igor-core/src/main/java/com/netflix/spinnaker/igor/build/model/GenericBuild.java index 0d62a6514..aa1a76dbb 100644 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/build/model/GenericBuild.java +++ b/igor-core/src/main/java/com/netflix/spinnaker/igor/build/model/GenericBuild.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 Netflix, Inc. + * Copyright 2020 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,12 +18,13 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import com.netflix.spinnaker.igor.jenkins.client.model.TestResults; import java.util.List; import java.util.Map; +import lombok.Builder; import lombok.Data; @Data +@Builder @JsonInclude(JsonInclude.Include.NON_NULL) public class GenericBuild { private boolean building; @@ -36,7 +37,7 @@ public class GenericBuild { private Result result; private List artifacts; - private List testResults; + private List testResults; private String url; private String id; diff --git a/igor-core/src/main/java/com/netflix/spinnaker/igor/build/model/GenericGitRevision.java b/igor-core/src/main/java/com/netflix/spinnaker/igor/build/model/GenericGitRevision.java index 76bbf8455..84dd0d0a2 100644 --- a/igor-core/src/main/java/com/netflix/spinnaker/igor/build/model/GenericGitRevision.java +++ b/igor-core/src/main/java/com/netflix/spinnaker/igor/build/model/GenericGitRevision.java @@ -22,6 +22,7 @@ import lombok.Getter; import lombok.experimental.Wither; +/** TODO(rz): Rename to GitRevision. */ @Getter @EqualsAndHashCode(of = "sha1") @Builder diff --git a/igor-core/src/main/java/com/netflix/spinnaker/igor/history/model/Event.groovy b/igor-core/src/main/java/com/netflix/spinnaker/igor/build/model/TestResult.java similarity index 83% rename from igor-core/src/main/java/com/netflix/spinnaker/igor/history/model/Event.groovy rename to igor-core/src/main/java/com/netflix/spinnaker/igor/build/model/TestResult.java index f086dc250..be35328db 100644 --- a/igor-core/src/main/java/com/netflix/spinnaker/igor/history/model/Event.groovy +++ b/igor-core/src/main/java/com/netflix/spinnaker/igor/build/model/TestResult.java @@ -1,7 +1,7 @@ /* - * Copyright 2016 Google, Inc. + * Copyright 2020 Netflix, Inc. * - * Licensed under the Apache License, Version 2.0 (the "License") + * 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 * @@ -13,8 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package com.netflix.spinnaker.igor.build.model; -package com.netflix.spinnaker.igor.history.model - -abstract class Event { -} +public interface TestResult {} diff --git a/igor-web/src/main/java/com/netflix/spinnaker/igor/config/BuildServerProperties.java b/igor-core/src/main/java/com/netflix/spinnaker/igor/config/BuildServerProperties.java similarity index 97% rename from igor-web/src/main/java/com/netflix/spinnaker/igor/config/BuildServerProperties.java rename to igor-core/src/main/java/com/netflix/spinnaker/igor/config/BuildServerProperties.java index 21cedeb1a..2d2519ccd 100644 --- a/igor-web/src/main/java/com/netflix/spinnaker/igor/config/BuildServerProperties.java +++ b/igor-core/src/main/java/com/netflix/spinnaker/igor/config/BuildServerProperties.java @@ -1,12 +1,11 @@ /* - * Copyright 2019 Schibsted ASA. + * Copyright 2020 Netflix, 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 + * 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, diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/exceptions/ArtifactNotFoundException.java b/igor-core/src/main/java/com/netflix/spinnaker/igor/exceptions/ArtifactNotFoundException.java similarity index 90% rename from igor-web/src/main/groovy/com/netflix/spinnaker/igor/exceptions/ArtifactNotFoundException.java rename to igor-core/src/main/java/com/netflix/spinnaker/igor/exceptions/ArtifactNotFoundException.java index 747e4f467..62d4687b5 100644 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/exceptions/ArtifactNotFoundException.java +++ b/igor-core/src/main/java/com/netflix/spinnaker/igor/exceptions/ArtifactNotFoundException.java @@ -1,7 +1,7 @@ /* - * Copyright 2019 Google, Inc. + * Copyright 2020 Netflix, Inc. * - * Licensed under the Apache License, Version 2.0 (the "License") + * 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 * @@ -16,12 +16,11 @@ package com.netflix.spinnaker.igor.exceptions; -import static org.springframework.http.HttpStatus.NOT_FOUND; - import com.netflix.spinnaker.kork.web.exceptions.NotFoundException; +import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; -@ResponseStatus(NOT_FOUND) +@ResponseStatus(HttpStatus.NOT_FOUND) public class ArtifactNotFoundException extends NotFoundException { public ArtifactNotFoundException( String master, String job, Integer buildNumber, String fileName) { diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/exceptions/BuildJobError.java b/igor-core/src/main/java/com/netflix/spinnaker/igor/exceptions/BuildJobError.java similarity index 89% rename from igor-web/src/main/groovy/com/netflix/spinnaker/igor/exceptions/BuildJobError.java rename to igor-core/src/main/java/com/netflix/spinnaker/igor/exceptions/BuildJobError.java index 8820b01e0..75c426a8f 100644 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/exceptions/BuildJobError.java +++ b/igor-core/src/main/java/com/netflix/spinnaker/igor/exceptions/BuildJobError.java @@ -1,7 +1,7 @@ /* - * Copyright 2019 Google, Inc. + * Copyright 2020 Netflix, Inc. * - * Licensed under the Apache License, Version 2.0 (the "License") + * 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 * @@ -18,6 +18,7 @@ import com.netflix.spinnaker.kork.web.exceptions.InvalidRequestException; +/** TODO(rz): Sparsely used, but primarily just Jenkins. */ public class BuildJobError extends InvalidRequestException { public BuildJobError(String message) { super(message); diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/exceptions/QueuedJobDeterminationError.java b/igor-core/src/main/java/com/netflix/spinnaker/igor/exceptions/QueuedJobDeterminationError.java similarity index 90% rename from igor-web/src/main/groovy/com/netflix/spinnaker/igor/exceptions/QueuedJobDeterminationError.java rename to igor-core/src/main/java/com/netflix/spinnaker/igor/exceptions/QueuedJobDeterminationError.java index 377eb507f..23c60244f 100644 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/exceptions/QueuedJobDeterminationError.java +++ b/igor-core/src/main/java/com/netflix/spinnaker/igor/exceptions/QueuedJobDeterminationError.java @@ -1,7 +1,7 @@ /* - * Copyright 2019 Google, Inc. + * Copyright 2020 Netflix, Inc. * - * Licensed under the Apache License, Version 2.0 (the "License") + * 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 * @@ -16,6 +16,7 @@ package com.netflix.spinnaker.igor.exceptions; +/** TODO(rz): Document. SpinnakerException. */ public class QueuedJobDeterminationError extends RuntimeException { public QueuedJobDeterminationError(String msg) { super(msg); diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/exceptions/UnhandledDownstreamServiceErrorException.java b/igor-core/src/main/java/com/netflix/spinnaker/igor/exceptions/UnhandledDownstreamServiceErrorException.java similarity index 100% rename from igor-web/src/main/groovy/com/netflix/spinnaker/igor/exceptions/UnhandledDownstreamServiceErrorException.java rename to igor-core/src/main/java/com/netflix/spinnaker/igor/exceptions/UnhandledDownstreamServiceErrorException.java diff --git a/igor-core/src/main/java/com/netflix/spinnaker/igor/history/model/ArtifactoryEvent.java b/igor-core/src/main/java/com/netflix/spinnaker/igor/history/model/ArtifactoryEvent.java index 1e60aff79..aaf2856dc 100644 --- a/igor-core/src/main/java/com/netflix/spinnaker/igor/history/model/ArtifactoryEvent.java +++ b/igor-core/src/main/java/com/netflix/spinnaker/igor/history/model/ArtifactoryEvent.java @@ -21,13 +21,11 @@ import java.util.Map; import lombok.AllArgsConstructor; import lombok.Data; -import lombok.EqualsAndHashCode; import lombok.RequiredArgsConstructor; -@EqualsAndHashCode(callSuper = true) @RequiredArgsConstructor @Data -public class ArtifactoryEvent extends Event { +public class ArtifactoryEvent implements Event { private final Content content; private final Map details = ImmutableMap.builder() diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/history/model/GenericBuildEvent.groovy b/igor-core/src/main/java/com/netflix/spinnaker/igor/history/model/BuildContent.java similarity index 65% rename from igor-web/src/main/groovy/com/netflix/spinnaker/igor/history/model/GenericBuildEvent.groovy rename to igor-core/src/main/java/com/netflix/spinnaker/igor/history/model/BuildContent.java index 9f3e0a0f4..2899df70c 100644 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/history/model/GenericBuildEvent.groovy +++ b/igor-core/src/main/java/com/netflix/spinnaker/igor/history/model/BuildContent.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 Schibsted ASA. + * Copyright 2020 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,16 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package com.netflix.spinnaker.igor.history.model; -package com.netflix.spinnaker.igor.history.model +/** TODO(rz): Documnt. */ +public interface BuildContent { -/** - * TODO(rz): Cannot move to kork-core due to Jenkins dependency - */ -class GenericBuildEvent extends Event{ - GenericBuildContent content - Map details = [ - type : 'build', - source: 'igor' - ] + String UNDEFINED_TYPE = "undefined"; + + default String getType() { + return UNDEFINED_TYPE; + } } diff --git a/igor-core/src/main/java/com/netflix/spinnaker/igor/history/model/BuildEvent.java b/igor-core/src/main/java/com/netflix/spinnaker/igor/history/model/BuildEvent.java new file mode 100644 index 000000000..d55e1b20d --- /dev/null +++ b/igor-core/src/main/java/com/netflix/spinnaker/igor/history/model/BuildEvent.java @@ -0,0 +1,32 @@ +/* + * Copyright 2020 Netflix, 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.netflix.spinnaker.igor.history.model; + +import java.util.HashMap; +import java.util.Map; + +/** TODO(rz): Documnt. */ +public interface BuildEvent extends Event { + + T getContent(); + + default Map getDetails() { + Map d = new HashMap<>(); + d.put("type", "build"); + d.put("source", "igor"); + return d; + } +} diff --git a/igor-core/src/main/java/com/netflix/spinnaker/igor/history/model/DockerEvent.groovy b/igor-core/src/main/java/com/netflix/spinnaker/igor/history/model/DockerEvent.groovy index 72cc91819..7b45dc96a 100644 --- a/igor-core/src/main/java/com/netflix/spinnaker/igor/history/model/DockerEvent.groovy +++ b/igor-core/src/main/java/com/netflix/spinnaker/igor/history/model/DockerEvent.groovy @@ -18,7 +18,7 @@ package com.netflix.spinnaker.igor.history.model import com.netflix.spinnaker.igor.build.model.GenericArtifact -class DockerEvent extends Event { +class DockerEvent implements Event { Content content GenericArtifact artifact diff --git a/igor-core/src/main/java/com/netflix/spinnaker/igor/history/model/EmptyBuildContent.java b/igor-core/src/main/java/com/netflix/spinnaker/igor/history/model/EmptyBuildContent.java new file mode 100644 index 000000000..2fa632d8f --- /dev/null +++ b/igor-core/src/main/java/com/netflix/spinnaker/igor/history/model/EmptyBuildContent.java @@ -0,0 +1,18 @@ +package com.netflix.spinnaker.igor.history.model; + +import lombok.AllArgsConstructor; +import lombok.Data; + +/** Move any invocation of this class to a monitor-specific BuildContent implementation. */ +@Deprecated +@Data +@AllArgsConstructor +public class EmptyBuildContent implements BuildContent { + + public static String TYPE = "empty"; + + @Override + public String getType() { + return TYPE; + } +} diff --git a/igor-core/src/main/java/com/netflix/spinnaker/igor/history/model/Event.java b/igor-core/src/main/java/com/netflix/spinnaker/igor/history/model/Event.java new file mode 100644 index 000000000..17c11edab --- /dev/null +++ b/igor-core/src/main/java/com/netflix/spinnaker/igor/history/model/Event.java @@ -0,0 +1,4 @@ +package com.netflix.spinnaker.igor.history.model; + +/** TODO(rz): Document. */ +public interface Event {} diff --git a/igor-core/src/main/java/com/netflix/spinnaker/igor/history/model/GenericBuildEvent.java b/igor-core/src/main/java/com/netflix/spinnaker/igor/history/model/GenericBuildEvent.java new file mode 100644 index 000000000..02db0126c --- /dev/null +++ b/igor-core/src/main/java/com/netflix/spinnaker/igor/history/model/GenericBuildEvent.java @@ -0,0 +1,34 @@ +/* + * Copyright 2020 Netflix, 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.netflix.spinnaker.igor.history.model; + +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Data; + +/** Move any invocation of this class to a monitor-specific BuildContent implementation. */ +@Deprecated +@Data +@AllArgsConstructor +public class GenericBuildEvent implements BuildEvent { + private EmptyBuildContent content; + private Map details; + + public GenericBuildEvent(EmptyBuildContent content) { + this.content = content; + } +} diff --git a/igor-core/src/main/java/com/netflix/spinnaker/igor/polling/PollingMonitor.java b/igor-core/src/main/java/com/netflix/spinnaker/igor/polling/PollingMonitor.java index 1a0dfd16f..7aaa84319 100644 --- a/igor-core/src/main/java/com/netflix/spinnaker/igor/polling/PollingMonitor.java +++ b/igor-core/src/main/java/com/netflix/spinnaker/igor/polling/PollingMonitor.java @@ -27,6 +27,7 @@ public interface PollingMonitor extends ApplicationListener getBuilds(String job); JobConfiguration getJobConfig(String jobName); + + default void stopRunningBuild(String jobName, int buildNumber) { + throw new UnsupportedOperationException("build service has not implemented build stopping yet"); + } } diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/service/BuildProperties.java b/igor-core/src/main/java/com/netflix/spinnaker/igor/service/BuildProperties.java similarity index 100% rename from igor-web/src/main/groovy/com/netflix/spinnaker/igor/service/BuildProperties.java rename to igor-core/src/main/java/com/netflix/spinnaker/igor/service/BuildProperties.java diff --git a/igor-core/src/main/java/com/netflix/spinnaker/igor/service/BuildQueueOperations.java b/igor-core/src/main/java/com/netflix/spinnaker/igor/service/BuildQueueOperations.java new file mode 100644 index 000000000..10124b332 --- /dev/null +++ b/igor-core/src/main/java/com/netflix/spinnaker/igor/service/BuildQueueOperations.java @@ -0,0 +1,31 @@ +/* + * Copyright 2020 Netflix, 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.netflix.spinnaker.igor.service; + +/** Additional operations for {@link BuildService}s that support build queuing. */ +public interface BuildQueueOperations extends BuildOperations { + /** Get the queued build at {@code queueId}. */ + T getQueuedBuild(String queueId); + + /** + * Stop a queued build at {@code queueId}. + * + *

Will not wait for a result. Must be idempotent. + */ + default void stopQueuedBuild(String jobName, String queueId, int buildNumber) { + // Do nothing. + } +} diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/service/BuildService.java b/igor-core/src/main/java/com/netflix/spinnaker/igor/service/BuildService.java similarity index 94% rename from igor-web/src/main/groovy/com/netflix/spinnaker/igor/service/BuildService.java rename to igor-core/src/main/java/com/netflix/spinnaker/igor/service/BuildService.java index c0d95e105..2dc43fd85 100644 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/service/BuildService.java +++ b/igor-core/src/main/java/com/netflix/spinnaker/igor/service/BuildService.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Schibsted ASA. + * Copyright 2020 Netflix, 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 + * 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, @@ -17,7 +17,6 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.netflix.spinnaker.fiat.model.resources.Permissions; -import com.netflix.spinnaker.igor.model.BuildServiceProvider; /** * Interface representing a Build Service host (CI) and the permissions needed to access it. Most diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/model/BuildServiceProvider.groovy b/igor-core/src/main/java/com/netflix/spinnaker/igor/service/BuildServiceProvider.java similarity index 67% rename from igor-web/src/main/groovy/com/netflix/spinnaker/igor/model/BuildServiceProvider.groovy rename to igor-core/src/main/java/com/netflix/spinnaker/igor/service/BuildServiceProvider.java index e56f578b6..b6dd193b5 100644 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/model/BuildServiceProvider.groovy +++ b/igor-core/src/main/java/com/netflix/spinnaker/igor/service/BuildServiceProvider.java @@ -1,12 +1,11 @@ /* - * Copyright 2016 Schibsted ASA. - * Copyright (c) 2017, 2018, Oracle Corporation and/or its affiliates. All rights reserved. + * Copyright 2020 Netflix, 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 + * 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, @@ -15,13 +14,17 @@ * limitations under the License. */ -package com.netflix.spinnaker.igor.model +package com.netflix.spinnaker.igor.service; -enum BuildServiceProvider { +/** + * TODO(rz): Delete this enum entirely. Replace with a registry or something if it's actually + * needed. + */ +public enum BuildServiceProvider { JENKINS, TRAVIS, CONCOURSE, GITLAB_CI, WERCKER, - GCB + GCB; } diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/service/BuildServices.java b/igor-core/src/main/java/com/netflix/spinnaker/igor/service/BuildServices.java similarity index 91% rename from igor-web/src/main/groovy/com/netflix/spinnaker/igor/service/BuildServices.java rename to igor-core/src/main/java/com/netflix/spinnaker/igor/service/BuildServices.java index c88196820..8684a007d 100644 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/service/BuildServices.java +++ b/igor-core/src/main/java/com/netflix/spinnaker/igor/service/BuildServices.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 Schibsted ASA. + * Copyright 2020 Netflix, 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 + * 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, @@ -16,7 +16,6 @@ package com.netflix.spinnaker.igor.service; -import com.netflix.spinnaker.igor.model.BuildServiceProvider; import java.util.HashMap; import java.util.List; import java.util.Map; diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/UpstreamProject.groovy b/igor-core/src/main/java/com/netflix/spinnaker/igor/service/JobNamesProvider.java similarity index 68% rename from igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/UpstreamProject.groovy rename to igor-core/src/main/java/com/netflix/spinnaker/igor/service/JobNamesProvider.java index 92fa63722..4f188ca36 100644 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/UpstreamProject.groovy +++ b/igor-core/src/main/java/com/netflix/spinnaker/igor/service/JobNamesProvider.java @@ -1,11 +1,11 @@ /* - * Copyright 2014 Netflix, Inc. + * Copyright 2020 Netflix, 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 + * 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, @@ -13,11 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package com.netflix.spinnaker.igor.service; -package com.netflix.spinnaker.igor.jenkins.client.model +import java.util.List; -/** - * Represents a Jenkins job upstream project - */ -class UpstreamProject extends RelatedProject { +public interface JobNamesProvider { + List getJobNames(); } diff --git a/igor-monitor-jenkins/igor-monitor-jenkins.gradle b/igor-monitor-jenkins/igor-monitor-jenkins.gradle new file mode 100644 index 000000000..313bbb5d0 --- /dev/null +++ b/igor-monitor-jenkins/igor-monitor-jenkins.gradle @@ -0,0 +1,56 @@ +/* + * Copyright 2020 Netflix, 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. + */ + +dependencies { + implementation project(":igor-core") + + compileOnly "org.projectlombok:lombok" + annotationProcessor platform("com.netflix.spinnaker.kork:kork-bom:$korkVersion") + annotationProcessor "org.projectlombok:lombok" + testAnnotationProcessor platform("com.netflix.spinnaker.kork:kork-bom:$korkVersion") + testAnnotationProcessor "org.projectlombok:lombok" + + implementation "org.springframework.boot:spring-boot-starter-web" + + implementation "com.netflix.spinnaker.fiat:fiat-api:$fiatVersion" + implementation "com.netflix.spinnaker.fiat:fiat-core:$fiatVersion" + implementation "com.netflix.spinnaker.kork:kork-security" + implementation "com.netflix.spinnaker.kork:kork-telemetry" + + implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-xml" + implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml" + + implementation "com.squareup.okhttp:okhttp" + implementation "com.squareup.retrofit:converter-jackson" + implementation "com.google.guava:guava" + + testImplementation "com.squareup.okhttp:mockwebserver" + + // TODO(rz): Get rid of this dependency! + implementation "com.netflix.spinnaker.kork:kork-jedis" + + // TODO(rz): Get rid of this dependency! + implementation "com.squareup.retrofit:retrofit" + + // TODO(rz): Get rid of this dependency! + implementation "com.squareup.retrofit:converter-simplexml" + + // TODO(rz): Get rid of this dependency! + implementation "com.netflix.spinnaker.kork:kork-hystrix" + + // TODO(rz): Get rid of this dependency! + implementation 'com.google.auth:google-auth-library-oauth2-http' +} diff --git a/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/config/JenkinsConfig.java b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/config/JenkinsConfig.java new file mode 100644 index 000000000..7d22f89f0 --- /dev/null +++ b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/config/JenkinsConfig.java @@ -0,0 +1,237 @@ +package com.netflix.spinnaker.igor.config; + +import static java.lang.String.format; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import com.fasterxml.jackson.module.jaxb.JaxbAnnotationModule; +import com.google.common.base.Strings; +import com.netflix.spectator.api.Registry; +import com.netflix.spinnaker.fiat.model.resources.Permissions; +import com.netflix.spinnaker.igor.IgorConfigurationProperties; +import com.netflix.spinnaker.igor.config.client.DefaultJenkinsOkHttpClientProvider; +import com.netflix.spinnaker.igor.config.client.DefaultJenkinsRetrofitRequestInterceptorProvider; +import com.netflix.spinnaker.igor.config.client.JenkinsOkHttpClientProvider; +import com.netflix.spinnaker.igor.config.client.JenkinsRetrofitRequestInterceptorProvider; +import com.netflix.spinnaker.igor.jenkins.JenkinsService; +import com.netflix.spinnaker.igor.jenkins.client.JenkinsClient; +import com.netflix.spinnaker.igor.service.BuildServices; +import com.netflix.spinnaker.kork.telemetry.InstrumentedProxy; +import com.squareup.okhttp.OkHttpClient; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.*; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import javax.net.ssl.*; +import javax.validation.Valid; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import retrofit.Endpoints; +import retrofit.RequestInterceptor; +import retrofit.RestAdapter; +import retrofit.client.OkClient; +import retrofit.converter.JacksonConverter; + +/** + * Converts the list of Jenkins Configuration properties a collection of clients to access the + * Jenkins hosts + */ +@Configuration +@Slf4j +@ConditionalOnProperty("jenkins.enabled") +@EnableConfigurationProperties(JenkinsProperties.class) +public class JenkinsConfig { + public static JenkinsService jenkinsService( + String jenkinsHostId, JenkinsClient jenkinsClient, Boolean csrf, Permissions permissions) { + return new JenkinsService(jenkinsHostId, jenkinsClient, csrf, permissions); + } + + public static ObjectMapper getObjectMapper() { + return new XmlMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .registerModule(new JaxbAnnotationModule()); + } + + public static JenkinsClient jenkinsClient( + JenkinsProperties.JenkinsHost host, + OkHttpClient client, + RequestInterceptor requestInterceptor, + int timeout) { + try { + return checkedJenkinsClient(host, client, requestInterceptor, timeout); + } catch (GeneralSecurityException | IOException e) { + throw new RuntimeException( + format("Cannot configure jenkins client for host '%s'", host.getName()), e); + } + } + + public static JenkinsClient checkedJenkinsClient( + JenkinsProperties.JenkinsHost host, + OkHttpClient client, + RequestInterceptor requestInterceptor, + int timeout) + throws KeyStoreException, NoSuchAlgorithmException, KeyManagementException, IOException, + CertificateException, UnrecoverableKeyException { + client.setReadTimeout(timeout, TimeUnit.MILLISECONDS); + + if (host.getSkipHostnameVerification()) { + client.setHostnameVerifier((hostname, session) -> true); + } + + TrustManager[] trustManagers = null; + KeyManager[] keyManagers = null; + + if (!Strings.isNullOrEmpty(host.getTrustStore())) { + if (host.getTrustStore().equals("*")) { + trustManagers = + new ArrayList<>(Collections.singletonList(new TrustAllTrustManager())) + .toArray(new TrustManager[0]); + } else { + String trustStorePassword = host.getTrustStorePassword(); + + KeyStore trustStore = KeyStore.getInstance(host.getTrustStoreType()); + trustStore.load( + new ByteArrayInputStream(Files.readAllBytes(Paths.get(host.getTrustStore()))), + trustStorePassword.toCharArray()); + + TrustManagerFactory trustManagerFactory = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(trustStore); + + trustManagers = trustManagerFactory.getTrustManagers(); + } + } + + if (!Strings.isNullOrEmpty(host.getKeyStore())) { + KeyStore keyStore = KeyStore.getInstance(host.getKeyStoreType()); + + keyStore.load( + new ByteArrayInputStream(Files.readAllBytes(Paths.get(host.getKeyStore()))), + host.getKeyStorePassword().toCharArray()); + + KeyManagerFactory keyManagerFactory = + KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init(keyStore, host.getKeyStorePassword().toCharArray()); + + keyManagers = keyManagerFactory.getKeyManagers(); + } + + if (trustManagers != null || keyManagers != null) { + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(keyManagers, trustManagers, null); + + client.setSslSocketFactory(sslContext.getSocketFactory()); + } + + return new RestAdapter.Builder() + .setEndpoint(Endpoints.newFixedEndpoint(host.getAddress())) + .setRequestInterceptor( + request -> { + request.addHeader("User-Agent", "Spinnaker-igor"); + requestInterceptor.intercept(request); + }) + .setClient(new OkClient(client)) + .setConverter(new JacksonConverter(getObjectMapper())) + .build() + .create(JenkinsClient.class); + } + + public static JenkinsClient jenkinsClient( + JenkinsProperties.JenkinsHost host, + OkHttpClient client, + RequestInterceptor requestInterceptor) { + return JenkinsConfig.jenkinsClient(host, client, requestInterceptor, 30000); + } + + public static JenkinsClient jenkinsClient(JenkinsProperties.JenkinsHost host, int timeout) { + OkHttpClient client = new OkHttpClient(); + return jenkinsClient(host, client, RequestInterceptor.NONE, timeout); + } + + public static JenkinsClient jenkinsClient(JenkinsProperties.JenkinsHost host) { + return JenkinsConfig.jenkinsClient(host, 30000); + } + + @Bean + @ConditionalOnMissingBean + public JenkinsOkHttpClientProvider jenkinsOkHttpClientProvider() { + return new DefaultJenkinsOkHttpClientProvider(); + } + + @Bean + @ConditionalOnMissingBean + public JenkinsRetrofitRequestInterceptorProvider jenkinsRetrofitRequestInterceptorProvider() { + return new DefaultJenkinsRetrofitRequestInterceptorProvider(); + } + + @Bean + public Map jenkinsMasters( + BuildServices buildServices, + final IgorConfigurationProperties igorConfigurationProperties, + @Valid JenkinsProperties jenkinsProperties, + final JenkinsOkHttpClientProvider jenkinsOkHttpClientProvider, + final JenkinsRetrofitRequestInterceptorProvider jenkinsRetrofitRequestInterceptorProvider, + final Registry registry) { + log.info("creating jenkinsMasters"); + + Map jenkinsMasters = + jenkinsProperties.getMasters().stream() + .filter(Objects::nonNull) + .collect( + Collectors.toMap( + (JenkinsProperties.JenkinsHost host) -> { + log.info("bootstrapping {} as {}", host.getAddress(), host.getName()); + return host.getName(); + }, + (JenkinsProperties.JenkinsHost host) -> { + JenkinsClient client = + InstrumentedProxy.proxy( + registry, + jenkinsClient( + host, + jenkinsOkHttpClientProvider.provide(host), + jenkinsRetrofitRequestInterceptorProvider.provide(host), + igorConfigurationProperties.getClient().getTimeout()), + "jenkinsClient", + Collections.singletonMap("master", host.getName())); + return jenkinsService( + host.getName(), client, host.getCsrf(), host.getPermissions().build()); + })); + + buildServices.addServices(jenkinsMasters); + return jenkinsMasters; + } + + public static class TrustAllTrustManager implements X509TrustManager { + @Override + public void checkClientTrusted(X509Certificate[] x509Certificates, String s) + throws CertificateException { + // do nothing + } + + @Override + public void checkServerTrusted(X509Certificate[] x509Certificates, String s) + throws CertificateException { + // do nothing + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + } +} diff --git a/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/config/JenkinsProperties.java b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/config/JenkinsProperties.java new file mode 100644 index 000000000..d6643b564 --- /dev/null +++ b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/config/JenkinsProperties.java @@ -0,0 +1,187 @@ +package com.netflix.spinnaker.igor.config; + +import static com.netflix.spinnaker.igor.config.JenkinsProperties.*; + +import com.netflix.spinnaker.fiat.model.resources.Permissions; +import java.security.KeyStore; +import java.util.ArrayList; +import java.util.List; +import javax.validation.Valid; +import javax.validation.constraints.NotEmpty; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +/** Helper class to map masters in properties file into a validated property map */ +@ConfigurationProperties(prefix = "jenkins") +@Validated +public class JenkinsProperties implements BuildServerProperties { + @Valid private List masters; + + @Override + public List getMasters() { + return masters; + } + + public void setMasters(List masters) { + this.masters = masters; + } + + public static class JenkinsHost implements BuildServerProperties.Host { + @NotEmpty private String name; + @NotEmpty private String address; + private String username; + private String password; + private Boolean csrf = false; + private String jsonPath; + private List oauthScopes = new ArrayList(); + private String token; + private Integer itemUpperThreshold; + private String trustStore; + private String trustStoreType = KeyStore.getDefaultType(); + private String trustStorePassword; + private String keyStore; + private String keyStoreType = KeyStore.getDefaultType(); + private String keyStorePassword; + private Boolean skipHostnameVerification = false; + private Permissions.Builder permissions = new Permissions.Builder(); + + @Override + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public Boolean getCsrf() { + return csrf; + } + + public void setCsrf(Boolean csrf) { + this.csrf = csrf; + } + + public String getJsonPath() { + return jsonPath; + } + + public void setJsonPath(String jsonPath) { + this.jsonPath = jsonPath; + } + + public List getOauthScopes() { + return oauthScopes; + } + + public void setOauthScopes(List oauthScopes) { + this.oauthScopes = oauthScopes; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public Integer getItemUpperThreshold() { + return itemUpperThreshold; + } + + public void setItemUpperThreshold(Integer itemUpperThreshold) { + this.itemUpperThreshold = itemUpperThreshold; + } + + public String getTrustStore() { + return trustStore; + } + + public void setTrustStore(String trustStore) { + this.trustStore = trustStore; + } + + public String getTrustStoreType() { + return trustStoreType; + } + + public void setTrustStoreType(String trustStoreType) { + this.trustStoreType = trustStoreType; + } + + public String getTrustStorePassword() { + return trustStorePassword; + } + + public void setTrustStorePassword(String trustStorePassword) { + this.trustStorePassword = trustStorePassword; + } + + public String getKeyStore() { + return keyStore; + } + + public void setKeyStore(String keyStore) { + this.keyStore = keyStore; + } + + public String getKeyStoreType() { + return keyStoreType; + } + + public void setKeyStoreType(String keyStoreType) { + this.keyStoreType = keyStoreType; + } + + public String getKeyStorePassword() { + return keyStorePassword; + } + + public void setKeyStorePassword(String keyStorePassword) { + this.keyStorePassword = keyStorePassword; + } + + public Boolean getSkipHostnameVerification() { + return skipHostnameVerification; + } + + public void setSkipHostnameVerification(Boolean skipHostnameVerification) { + this.skipHostnameVerification = skipHostnameVerification; + } + + @Override + public Permissions.Builder getPermissions() { + return permissions; + } + + public void setPermissions(Permissions.Builder permissions) { + this.permissions = permissions; + } + } +} diff --git a/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/config/auth/AuthRequestInterceptor.java b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/config/auth/AuthRequestInterceptor.java new file mode 100644 index 000000000..063fd294a --- /dev/null +++ b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/config/auth/AuthRequestInterceptor.java @@ -0,0 +1,105 @@ +package com.netflix.spinnaker.igor.config.auth; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.common.base.Joiner; +import com.google.common.base.Strings; +import com.netflix.spinnaker.igor.config.JenkinsProperties; +import com.squareup.okhttp.Credentials; +import java.io.*; +import java.util.ArrayList; +import java.util.List; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import retrofit.RequestInterceptor; + +@Slf4j +public class AuthRequestInterceptor implements RequestInterceptor { + + private static final Joiner SUPPLIERS_JOINER = Joiner.on(", "); + + public AuthRequestInterceptor(JenkinsProperties.JenkinsHost host) { + // Order may be significant here. + if (!Strings.isNullOrEmpty(host.getUsername()) && !Strings.isNullOrEmpty(host.getPassword())) { + suppliers.add(new BasicAuthHeaderSupplier(host.getUsername(), host.getPassword())); + } + + if (!Strings.isNullOrEmpty(host.getJsonPath()) && !host.getOauthScopes().isEmpty()) { + suppliers.add(new GoogleBearerTokenHeaderSupplier(host.getJsonPath(), host.getOauthScopes())); + } else if (!Strings.isNullOrEmpty(host.getToken())) { + BearerTokenHeaderSupplier supplier = new BearerTokenHeaderSupplier(); + supplier.token = host.getToken(); + suppliers.add(supplier); + } + } + + @Override + public void intercept(RequestFacade request) { + if (suppliers != null && !suppliers.isEmpty()) { + request.addHeader("Authorization", SUPPLIERS_JOINER.join(suppliers)); + } + } + + public List getSuppliers() { + return suppliers; + } + + public void setSuppliers(List suppliers) { + this.suppliers = suppliers; + } + + private List suppliers = + new ArrayList(); + + /** TODO(rz): Good candidate for plugins. */ + public interface AuthorizationHeaderSupplier { + /** + * Returns the value to be added as the value in the "Authorization" HTTP header. + * + * @return + */ + String toString(); + } + + public static class BasicAuthHeaderSupplier implements AuthorizationHeaderSupplier { + public BasicAuthHeaderSupplier(String username, String password) { + this.username = username; + this.password = password; + } + + public String toString() { + return Credentials.basic(username, password); + } + + private final String username; + private final String password; + } + + @Slf4j + public static class GoogleBearerTokenHeaderSupplier implements AuthorizationHeaderSupplier { + @SneakyThrows + public GoogleBearerTokenHeaderSupplier(String jsonPath, List scopes) { + credentials = + GoogleCredentials.fromStream(new FileInputStream(new File(jsonPath))) + .createScoped(scopes); + } + + @SneakyThrows + public String toString() { + log.debug("Including Google Bearer token in Authorization header"); + credentials.refreshIfExpired(); + return credentials.getAccessToken().getTokenValue(); + } + + private GoogleCredentials credentials; + } + + @Slf4j + public static class BearerTokenHeaderSupplier implements AuthorizationHeaderSupplier { + public String toString() { + log.debug("Including raw bearer token in Authorization header"); + return "Bearer " + token; + } + + private Object token; + } +} diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/config/client/DefaultJenkinsOkHttpClientProvider.java b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/config/client/DefaultJenkinsOkHttpClientProvider.java similarity index 100% rename from igor-web/src/main/groovy/com/netflix/spinnaker/igor/config/client/DefaultJenkinsOkHttpClientProvider.java rename to igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/config/client/DefaultJenkinsOkHttpClientProvider.java diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/config/client/DefaultJenkinsRetrofitRequestInterceptorProvider.java b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/config/client/DefaultJenkinsRetrofitRequestInterceptorProvider.java similarity index 100% rename from igor-web/src/main/groovy/com/netflix/spinnaker/igor/config/client/DefaultJenkinsRetrofitRequestInterceptorProvider.java rename to igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/config/client/DefaultJenkinsRetrofitRequestInterceptorProvider.java diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/config/client/JenkinsOkHttpClientProvider.java b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/config/client/JenkinsOkHttpClientProvider.java similarity index 100% rename from igor-web/src/main/groovy/com/netflix/spinnaker/igor/config/client/JenkinsOkHttpClientProvider.java rename to igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/config/client/JenkinsOkHttpClientProvider.java diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/config/client/JenkinsRetrofitRequestInterceptorProvider.java b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/config/client/JenkinsRetrofitRequestInterceptorProvider.java similarity index 100% rename from igor-web/src/main/groovy/com/netflix/spinnaker/igor/config/client/JenkinsRetrofitRequestInterceptorProvider.java rename to igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/config/client/JenkinsRetrofitRequestInterceptorProvider.java diff --git a/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/history/model/JenkinsBuildContent.java b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/history/model/JenkinsBuildContent.java new file mode 100644 index 000000000..ea0873d42 --- /dev/null +++ b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/history/model/JenkinsBuildContent.java @@ -0,0 +1,17 @@ +package com.netflix.spinnaker.igor.history.model; + +import com.netflix.spinnaker.igor.jenkins.client.model.Project; +import lombok.AllArgsConstructor; +import lombok.Data; + +/** + * Encapsulates a build content block + * + *

+ */ +@Data +@AllArgsConstructor +public class JenkinsBuildContent implements BuildContent { + private Project project; + private String master; +} diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/history/model/GenericBuildContent.groovy b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/history/model/JenkinsBuildEvent.java similarity index 65% rename from igor-web/src/main/groovy/com/netflix/spinnaker/igor/history/model/GenericBuildContent.groovy rename to igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/history/model/JenkinsBuildEvent.java index 7fa34b95c..c7e644d92 100644 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/history/model/GenericBuildContent.groovy +++ b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/history/model/JenkinsBuildEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 Schibsted ASA. + * Copyright 2020 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,16 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package com.netflix.spinnaker.igor.history.model; -package com.netflix.spinnaker.igor.history.model +import lombok.AllArgsConstructor; +import lombok.Data; -import com.netflix.spinnaker.igor.build.model.GenericProject - -/** - * TODO(rz): Cannot move to kork-core due to Jenkins dependency - */ -class GenericBuildContent { - GenericProject project - String master - String type +@Data +@AllArgsConstructor +public class JenkinsBuildEvent implements BuildEvent { + private JenkinsBuildContent content; } diff --git a/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/JenkinsBuildMonitor.java b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/JenkinsBuildMonitor.java new file mode 100644 index 000000000..9927e43da --- /dev/null +++ b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/JenkinsBuildMonitor.java @@ -0,0 +1,314 @@ +package com.netflix.spinnaker.igor.jenkins; + +import static net.logstash.logback.argument.StructuredArguments.kv; + +import com.netflix.discovery.DiscoveryClient; +import com.netflix.spectator.api.Registry; +import com.netflix.spinnaker.igor.IgorConfigurationProperties; +import com.netflix.spinnaker.igor.config.JenkinsProperties; +import com.netflix.spinnaker.igor.history.EchoService; +import com.netflix.spinnaker.igor.history.model.JenkinsBuildContent; +import com.netflix.spinnaker.igor.history.model.JenkinsBuildEvent; +import com.netflix.spinnaker.igor.jenkins.client.model.Build; +import com.netflix.spinnaker.igor.jenkins.client.model.Project; +import com.netflix.spinnaker.igor.jenkins.client.model.ProjectsList; +import com.netflix.spinnaker.igor.polling.*; +import com.netflix.spinnaker.igor.service.BuildServiceProvider; +import com.netflix.spinnaker.igor.service.BuildServices; +import com.netflix.spinnaker.security.AuthenticatedRequest; +import java.time.Duration; +import java.time.Instant; +import java.util.*; +import java.util.stream.Collectors; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; +import retrofit.RetrofitError; + +/** Monitors new jenkins builds */ +@Slf4j +@Service +@SuppressWarnings("CatchException") +@ConditionalOnProperty("jenkins.enabled") +public class JenkinsBuildMonitor + extends CommonPollingMonitor< + JenkinsBuildMonitor.JobDelta, JenkinsBuildMonitor.JobPollingDelta> { + + private final JenkinsCache cache; + private final BuildServices buildServices; + private final boolean pollingEnabled; + private final Optional echoService; + private final JenkinsProperties jenkinsProperties; + + @Autowired + public JenkinsBuildMonitor( + IgorConfigurationProperties properties, + Registry registry, + Optional discoveryClient, + Optional lockService, + JenkinsCache cache, + BuildServices buildServices, + @Value("${jenkins.polling.enabled:true}") boolean pollingEnabled, + Optional echoService, + JenkinsProperties jenkinsProperties) { + super(properties, registry, discoveryClient, lockService); + this.cache = cache; + this.buildServices = buildServices; + this.pollingEnabled = pollingEnabled; + this.echoService = echoService; + this.jenkinsProperties = jenkinsProperties; + } + + @Override + public String getName() { + return "jenkinsBuildMonitor"; + } + + @Override + public boolean isInService() { + return pollingEnabled && super.isInService(); + } + + @Override + public void poll(final boolean sendEvents) { + buildServices + .getServiceNames(BuildServiceProvider.JENKINS) + .forEach(master -> pollSingle(new PollContext(master, !sendEvents))); + } + + /** + * Gets a list of jobs for this master & processes builds between last poll stamp and a sliding + * upper bound stamp, the cursor will be used to advanced to the upper bound when all builds are + * completed in the commit phase. + */ + @Override + protected JobPollingDelta generateDelta(PollContext ctx) { + final String master = ctx.partitionName; + log.trace("Checking for new builds in '{}'", master); + + final List delta = new ArrayList<>(); + registry + .timer("pollingMonitor.jenkins.retrieveProjects", "partition", master) + .record( + () -> { + JenkinsService jenkinsService = (JenkinsService) buildServices.getService(master); + ProjectsList projects = jenkinsService.getProjects(); + if (projects == null) { + return; + } + projects + .getList() + .forEach( + project -> { + processBuildsOfProject(jenkinsService, master, project, delta); + }); + }); + + return new JobPollingDelta(master, delta); + } + + private void processBuildsOfProject( + JenkinsService jenkinsService, + final String master, + final Project job, + List deltas) { + if (job.getLastBuild() == null) { + log.trace( + "[{}:{}] has no builds skipping...", kv("master", master), kv("job", job.getName())); + return; + } + + try { + Long cursor = cache.getLastPollCycleTimestamp(master, job.getName()); + Long lastBuildStamp = Long.valueOf(job.getLastBuild().getTimestamp()); + Date upperBound = new Date(lastBuildStamp); + if (Objects.equals(cursor, lastBuildStamp)) { + log.trace("[{}:{}] is up to date. skipping", master, job); + return; + } + + if (cursorIsUnset(cursor) + && !igorProperties.getSpinnaker().getBuild().isHandleFirstBuilds()) { + cache.setLastPollCycleTimestamp(master, job.getName(), lastBuildStamp); + return; + } + + List allBuilds = getBuilds(jenkinsService, master, job, cursor, lastBuildStamp); + List currentlyBuilding = + allBuilds.stream().filter(Build::isBuilding).collect(Collectors.toList()); + List completedBuilds = + allBuilds.stream().filter(b -> !b.isBuilding()).collect(Collectors.toList()); + + cursor = cursorIsUnset(cursor) ? lastBuildStamp : cursor; + Date lowerBound = new Date(cursor); + + // TODO(jc): Document. + if (!igorProperties.getSpinnaker().getBuild().isProcessBuildsOlderThanLookBackWindow()) { + completedBuilds = onlyInLookBackWindow(completedBuilds); + } + + JobDelta delta = new JobDelta(); + delta.setCursor(cursor); + delta.setName(job.getName()); + delta.setLastBuildStamp(lastBuildStamp); + delta.setUpperBound(upperBound); + delta.setLowerBound(lowerBound); + delta.setCompletedBuilds(completedBuilds); + delta.setRunningBuilds(currentlyBuilding); + + deltas.add(delta); + } catch (Exception e) { + log.error( + "Error processing builds for [{}:{}]", kv("master", master), kv("job", job.getName()), e); + if (e.getCause() instanceof RetrofitError) { + RetrofitError re = (RetrofitError) e.getCause(); + log.error( + "Error communicating with jenkins for [{}:{}]: {}", + kv("master", master), + kv("job", job.getName()), + kv("url", re.getUrl()), + re); + } + } + } + + private List getBuilds( + JenkinsService jenkinsService, + final String master, + final Project job, + final Long cursor, + final Long lastBuildStamp) { + if (cursorIsUnset(cursor)) { + log.debug("[{}:{}] setting new cursor to {}", master, job.getName(), lastBuildStamp); + final List builds = jenkinsService.getBuilds(job.getName()); + return Optional.ofNullable(builds).orElse(Collections.emptyList()); + } + + // filter between last poll and jenkins last build included + final List builds = + Optional.ofNullable(jenkinsService.getBuilds(job.getName())) + .orElse(Collections.emptyList()); + + return builds.stream() + .filter( + build -> { + String buildTimestamp = build.getTimestamp(); + long buildStamp = Long.parseLong(buildTimestamp == null ? "0" : buildTimestamp); + return buildStamp <= lastBuildStamp && buildStamp > cursor; + }) + .collect(Collectors.toList()); + } + + private List onlyInLookBackWindow(final List builds) { + Duration offsetSeconds = Duration.ofSeconds(getPollInterval()); + Duration lookBackWindowMinutes = + Duration.ofMinutes(igorProperties.getSpinnaker().getBuild().getLookBackWindowMins()); + + Instant lookBackDate = Instant.now().minus(offsetSeconds.plus(lookBackWindowMinutes)); + + return builds.stream() + .filter( + build -> { + Instant buildEndDate = + Instant.ofEpochMilli(Long.parseLong(build.getTimestamp())) + .plusMillis(build.getDuration()); + return buildEndDate.isAfter(lookBackDate); + }) + .collect(Collectors.toList()); + } + + @Override + protected void commitDelta(JobPollingDelta delta, final boolean sendEvents) { + final String master = delta.getMaster(); + + delta + .getItems() + .forEach( + job -> { + // post events for finished builds + job.completedBuilds.forEach( + build -> { + boolean eventPosted = + cache.getEventPosted(master, job.name, job.cursor, build.getNumber()); + if (!eventPosted && sendEvents) { + Project project = new Project(); + project.setName(job.getName()); + project.setLastBuild(build); + postEvent(project, master); + log.debug( + "[{}:{}]:{} event posted", master, job.getName(), build.getNumber()); + cache.setEventPosted( + master, job.getName(), job.getCursor(), build.getNumber()); + } + }); + + // advance cursor when all builds have completed in the interval + if (job.getRunningBuilds().isEmpty()) { + log.info( + "[{}:{}] has no other builds between [{} - {}], advancing cursor to {}", + kv("master", master), + kv("job", job.name), + job.getLowerBound(), + job.getUpperBound(), + job.getLastBuildStamp()); + cache.pruneOldMarkers(master, job.getName(), job.getCursor()); + cache.setLastPollCycleTimestamp(master, job.getName(), job.getLastBuildStamp()); + } + }); + } + + @Override + protected Integer getPartitionUpperThreshold(final String partition) { + return jenkinsProperties.getMasters().stream() + .filter(it -> it.getName().equals(partition)) + .findFirst() + .map(JenkinsProperties.JenkinsHost::getItemUpperThreshold) + .orElse(null); + } + + private void postEvent(final Project project, final String master) { + if (!echoService.isPresent()) { + log.warn("Cannot send build notification: Echo is not configured"); + registry + .counter(missedNotificationId.withTag("monitor", getClass().getSimpleName())) + .increment(); + return; + } + + AuthenticatedRequest.allowAnonymous( + () -> { + echoService.ifPresent( + echo -> + echo.postEvent(new JenkinsBuildEvent(new JenkinsBuildContent(project, master)))); + // TODO(rz): Add allowAnonymous(Runnable) + return null; + }); + } + + /** TODO(jc): First pass, not sure what this cursor actually is. Document. */ + private boolean cursorIsUnset(Long cursor) { + return (cursor == null || cursor == 0); + } + + @Data + @AllArgsConstructor + static class JobPollingDelta implements PollingDelta { + private String master; + private List items; + } + + @Data + static class JobDelta implements DeltaItem { + private Long cursor; + private String name; + private Long lastBuildStamp; + private Date lowerBound; + private Date upperBound; + private List completedBuilds; + private List runningBuilds; + } +} diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/JenkinsCache.java b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/JenkinsCache.java similarity index 96% rename from igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/JenkinsCache.java rename to igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/JenkinsCache.java index f323b0510..1510ab6c6 100644 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/JenkinsCache.java +++ b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/JenkinsCache.java @@ -1,7 +1,7 @@ /* - * Copyright 2017 Netflix, Inc. + * Copyright 2020 Netflix, Inc. * - * Licensed under the Apache License, Version 2.0 (the "License") + * 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 * @@ -106,6 +106,7 @@ public void setLastPollCycleTimestamp(String master, String job, Long timestamp) }); } + /** TODO(rz): Use primitive type. Return -1 if there's no prior timestamp. */ public Long getLastPollCycleTimestamp(String master, String job) { return redisClientDelegate.withCommandsClient( c -> { @@ -114,7 +115,7 @@ public Long getLastPollCycleTimestamp(String master, String job) { }); } - public Boolean getEventPosted(String master, String job, Long cursor, Integer buildNumber) { + public boolean getEventPosted(String master, String job, Long cursor, Integer buildNumber) { String key = makeKey(master, job) + ":" + POLL_STAMP + ":" + cursor; return redisClientDelegate.withCommandsClient( c -> c.hget(key, Integer.toString(buildNumber)) != null); diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/service/JenkinsService.java b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/JenkinsService.java similarity index 60% rename from igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/service/JenkinsService.java rename to igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/JenkinsService.java index d8f6a6e64..6f37536df 100644 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/service/JenkinsService.java +++ b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/JenkinsService.java @@ -1,11 +1,11 @@ /* - * Copyright 2015 Netflix, Inc. + * Copyright 2020 Netflix, 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 + * 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, @@ -14,13 +14,17 @@ * limitations under the License. */ -package com.netflix.spinnaker.igor.jenkins.service; +package com.netflix.spinnaker.igor.jenkins; +import static java.lang.String.format; import static net.logstash.logback.argument.StructuredArguments.kv; import static org.springframework.http.HttpStatus.NOT_FOUND; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Joiner; +import com.google.common.base.Splitter; +import com.google.common.base.Strings; import com.netflix.spinnaker.fiat.model.resources.Permissions; import com.netflix.spinnaker.hystrix.SimpleJava8HystrixCommand; import com.netflix.spinnaker.igor.build.model.GenericBuild; @@ -29,30 +33,18 @@ import com.netflix.spinnaker.igor.exceptions.BuildJobError; import com.netflix.spinnaker.igor.exceptions.QueuedJobDeterminationError; import com.netflix.spinnaker.igor.jenkins.client.JenkinsClient; -import com.netflix.spinnaker.igor.jenkins.client.model.Build; -import com.netflix.spinnaker.igor.jenkins.client.model.BuildArtifact; -import com.netflix.spinnaker.igor.jenkins.client.model.BuildDependencies; -import com.netflix.spinnaker.igor.jenkins.client.model.JobConfig; -import com.netflix.spinnaker.igor.jenkins.client.model.JobList; -import com.netflix.spinnaker.igor.jenkins.client.model.Project; -import com.netflix.spinnaker.igor.jenkins.client.model.ProjectsList; -import com.netflix.spinnaker.igor.jenkins.client.model.QueuedJob; -import com.netflix.spinnaker.igor.jenkins.client.model.ScmDetails; -import com.netflix.spinnaker.igor.model.BuildServiceProvider; -import com.netflix.spinnaker.igor.model.Crumb; -import com.netflix.spinnaker.igor.service.BuildOperations; -import com.netflix.spinnaker.igor.service.BuildProperties; +import com.netflix.spinnaker.igor.jenkins.client.model.*; +import com.netflix.spinnaker.igor.jenkins.exceptions.InvalidJobParameterException; +import com.netflix.spinnaker.igor.service.*; import com.netflix.spinnaker.kork.core.RetrySupport; import com.netflix.spinnaker.kork.exceptions.SpinnakerException; import com.netflix.spinnaker.kork.web.exceptions.NotFoundException; import java.io.InputStream; import java.time.Duration; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Properties; +import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; +import java.util.stream.StreamSupport; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.web.util.UriUtils; @@ -63,7 +55,11 @@ import retrofit.client.Response; @Slf4j -public class JenkinsService implements BuildOperations, BuildProperties { +public class JenkinsService + implements BuildProperties, BuildQueueOperations, JobNamesProvider { + + private static final Splitter QUEUED_BUILD_SPLITTER = Splitter.on("/"); + private final ObjectMapper objectMapper = new ObjectMapper(); private final String groupKey; private final String serviceName; @@ -121,7 +117,35 @@ private Stream recursiveGetProjects(Project project, String prefix) { return project.getList().stream().flatMap(p -> recursiveGetProjects(p, projectName + "/job/")); } - public JobList getJobs() { + @Override + public List getJobNames() { + return recursiveJobNames(getJobs().getList(), null); + } + + private static List recursiveJobNames(List jobs, String prefix) { + List jobNames = new ArrayList<>(); + + final String pre = (Strings.isNullOrEmpty(prefix)) ? null : prefix + "/job/"; + jobs.forEach( + job -> { + String qualifiedName; + if (pre == null) { + qualifiedName = job.getName(); + } else { + qualifiedName = pre + job.getName(); + } + + if (job.getList() == null || job.getList().isEmpty()) { + jobNames.add(qualifiedName); + } else { + jobNames.addAll(recursiveJobNames(job.getList(), qualifiedName)); + } + }); + + return jobNames; + } + + private JobList getJobs() { return new SimpleJava8HystrixCommand<>( groupKey, buildCommandKey("getJobs"), jenkinsClient::getJobs) .execute(); @@ -168,25 +192,88 @@ public GenericBuild getGenericBuild(String jobName, int buildNumber) { @Override public int triggerBuildWithParameters(String job, Map queryParameters) { - Response response = buildWithParameters(job, queryParameters); + JobConfig jobConfig = getJobConfig(job); + if (!jobConfig.isBuildable()) { + throw new BuildJobError(format("Job '%s' is not buildable. It may be disabled.", job)); + } + + if (jobConfig.getParameterDefinitionList() != null + && !jobConfig.getParameterDefinitionList().isEmpty()) { + validateJobParameters(jobConfig, queryParameters); + } + + Response response; + if (!queryParameters.isEmpty() + && jobConfig.getParameterDefinitionList() != null + && !jobConfig.getParameterDefinitionList().isEmpty()) { + response = buildWithParameters(job, queryParameters); + } else if (queryParameters.isEmpty() + && jobConfig.getParameterDefinitionList() != null + && !jobConfig.getParameterDefinitionList().isEmpty()) { + // account for when you just want to fire a job with the default parameter values by adding a + // dummy param + response = buildWithParameters(job, Collections.singletonMap("startedBy", "igor")); + } else if (queryParameters.isEmpty() + && (jobConfig.getParameterDefinitionList() == null + || jobConfig.getParameterDefinitionList().isEmpty())) { + response = build(job); + } else { + // Jenkins will reject the build, so don't even try + // we should throw a BuildJobError, but I get a bytecode error : java.lang.VerifyError: Bad + // method call from inside of a branch + throw new RuntimeException( + format("job : %s, passing params to a job which doesn't need them", job)); + } + + return getBuildNumberFromResponse(job, response); + } + + static int getBuildNumberFromResponse(String job, Response response) { if (response.getStatus() != 201) { - throw new BuildJobError("Received a non-201 status when submitting job '" + job + "'"); + // TODO(rz): How to determine master? + throw new BuildJobError( + format("Received a non-201 status when submitting job '%s' to master 'unknown'", job)); } log.info("Submitted build job '{}'", kv("job", job)); - String queuedLocation = + Header locationHeader = response.getHeaders().stream() - .filter(h -> h.getName() != null) - .filter(h -> h.getName().toLowerCase().equals("location")) - .map(Header::getValue) + .filter(it -> it.getName().toLowerCase().equals("location")) .findFirst() .orElseThrow( () -> new QueuedJobDeterminationError( - "Could not find Location header for job '" + job + "'")); + format("Could not find Location header for job '%s'", job))); + String queuedLocation = locationHeader.getValue(); - int lastSlash = queuedLocation.lastIndexOf('/'); - return Integer.parseInt(queuedLocation.substring(lastSlash + 1)); + String[] parts = + StreamSupport.stream(QUEUED_BUILD_SPLITTER.split(queuedLocation).spliterator(), false) + .toArray(String[]::new); + return Integer.parseInt(parts[parts.length - 1]); + } + + static void validateJobParameters(JobConfig jobConfig, Map requestParams) { + if (jobConfig.getParameterDefinitionList() == null) { + return; + } + + jobConfig + .getParameterDefinitionList() + .forEach( + (parameterDefinition) -> { + String matchingParam = requestParams.get(parameterDefinition.getName()); + if (matchingParam != null + && "ChoiceParameterDefinition".equals(parameterDefinition.type) + && parameterDefinition.choices != null + && !parameterDefinition.choices.contains(matchingParam)) { + throw new InvalidJobParameterException( + format( + "'%s' is not a valid choice for '%s'. Valid choices are: %s", + matchingParam, + parameterDefinition.name, + Joiner.on(", ").join(parameterDefinition.choices))); + } + }); } @Override @@ -224,6 +311,7 @@ private ScmDetails getGitDetails(String jobName, Integer buildNumber) { false); } + // TODO(rz): Unused? public Build getLatestBuild(String jobName) { return new SimpleJava8HystrixCommand<>( groupKey, @@ -232,22 +320,54 @@ public Build getLatestBuild(String jobName) { .execute(); } - public QueuedJob queuedBuild(Integer item) { + @Override + public QueuedJob getQueuedBuild(String queueId) { try { - return jenkinsClient.getQueuedItem(item); + return jenkinsClient.getQueuedItem(Integer.valueOf(queueId)); } catch (RetrofitError e) { - if (e.getResponse() != null && e.getResponse().getStatus() == NOT_FOUND.value()) { - throw new NotFoundException("Queued job '${item}' not found for master '${master}'."); + if (e.getResponse() != null && e.getResponse().getStatus() == 404) { + throw new NotFoundException( + format("Queued job '%s' not found for master '%s'.", queueId, groupKey)); } throw e; } } - public Response build(String jobName) { + @Override + public void stopQueuedBuild(String jobName, String queueId, int buildNumber) { + String crumb = getCrumb(); + + // Jobs that haven't been started yet won't have a buildNumber + // (They're still in the queue). We use 0 to denote that case + if (buildNumber != 0) { + jenkinsClient.stopRunningBuild(encode(jobName), buildNumber, "", crumb); + } + + // The jenkins api for removing a job from the queue + // (http:///queue/cancelItem?id=) + // always returns a 404. This try catch block insures that the exception is eaten instead + // of being handled by the handleOtherException handler and returning a 500 to orca + try { + jenkinsClient.stopQueuedBuild(queueId, "", crumb); + return; + } catch (RetrofitError e) { + if (e.getResponse() != null && e.getResponse().getStatus() != NOT_FOUND.value()) { + throw e; + } + } + jenkinsClient.stopQueuedBuild(queueId, "", crumb); + } + + @Override + public void stopRunningBuild(String jobName, int buildNumber) { + jenkinsClient.stopRunningBuild(encode(jobName), buildNumber, "", getCrumb()); + } + + private Response build(String jobName) { return jenkinsClient.build(encode(jobName), "", getCrumb()); } - public Response buildWithParameters(String jobName, Map queryParams) { + private Response buildWithParameters(String jobName, Map queryParams) { return jenkinsClient.buildWithParameters(encode(jobName), queryParams, "", getCrumb()); } @@ -261,22 +381,22 @@ public Map getBuildProperties(String job, GenericBuild build, St if (StringUtils.isEmpty(fileName)) { return new HashMap<>(); } - Map map = new HashMap<>(); + try { String path = getArtifactPathFromBuild(job, build.getNumber(), fileName); try (InputStream propertyStream = this.getPropertyFile(job, build.getNumber(), path).getBody().in()) { if (fileName.endsWith(".yml") || fileName.endsWith(".yaml")) { Yaml yml = new Yaml(new SafeConstructor()); - map = yml.load(propertyStream); + return yml.load(propertyStream); } else if (fileName.endsWith(".json")) { - map = objectMapper.readValue(propertyStream, new TypeReference>() {}); + return objectMapper.readValue( + propertyStream, new TypeReference>() {}); } else { Properties properties = new Properties(); properties.load(propertyStream); - map = - properties.entrySet().stream() - .collect(Collectors.toMap(e -> e.getKey().toString(), Map.Entry::getValue)); + return properties.entrySet().stream() + .collect(Collectors.toMap(e -> e.getKey().toString(), Map.Entry::getValue)); } } } catch (NotFoundException e) { @@ -284,7 +404,8 @@ public Map getBuildProperties(String job, GenericBuild build, St } catch (Exception e) { log.error("Unable to get igorProperties '{}'", kv("job", job), e); } - return map; + + return new HashMap<>(); } private String getArtifactPathFromBuild(String job, int buildNumber, String fileName) { @@ -329,14 +450,6 @@ private Response getPropertyFile(String jobName, Integer buildNumber, String fil false); } - public Response stopRunningBuild(String jobName, Integer buildNumber) { - return jenkinsClient.stopRunningBuild(encode(jobName), buildNumber, "", getCrumb()); - } - - public Response stopQueuedBuild(String queuedBuild) { - return jenkinsClient.stopQueuedBuild(queuedBuild, "", getCrumb()); - } - /** * A CommandKey should be unique per group (to ensure broken circuits do not span Jenkins masters) */ diff --git a/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/JenkinsClient.java b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/JenkinsClient.java new file mode 100644 index 000000000..de8a8395b --- /dev/null +++ b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/JenkinsClient.java @@ -0,0 +1,98 @@ +/* + * Copyright 2020 Netflix, 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.netflix.spinnaker.igor.jenkins.client; + +import com.netflix.spinnaker.igor.jenkins.client.model.*; +import java.util.Map; +import retrofit.client.Response; +import retrofit.http.*; + +/** Interface for interacting with a Jenkins Service via Xml */ +@SuppressWarnings("LineLength") +public interface JenkinsClient { + @GET( + "/api/xml?tree=jobs[name,lastBuild[actions[failCount,skipCount,totalCount,urlName],duration,number,timestamp,result,building,url],jobs[name,lastBuild[actions[failCount,skipCount,totalCount,urlName],duration,number,timestamp,result,building,url],jobs[name,lastBuild[actions[failCount,skipCount,totalCount,urlName],duration,number,timestamp,result,building,url],jobs[name,lastBuild[actions[failCount,skipCount,totalCount,urlName],duration,number,timestamp,result,building,url],jobs[name,lastBuild[actions[failCount,skipCount,totalCount,urlName],duration,number,timestamp,result,building,url],jobs[name,lastBuild[actions[failCount,skipCount,totalCount,urlName],duration,number,timestamp,result,building,url],jobs[name,lastBuild[actions[failCount,skipCount,totalCount,urlName],duration,number,timestamp,result,building,url],jobs[name,lastBuild[actions[failCount,skipCount,totalCount,urlName],duration,number,timestamp,result,building,url],jobs[name,lastBuild[actions[failCount,skipCount,totalCount,urlName],duration,number,timestamp,result,building,url],jobs[name,lastBuild[actions[failCount,skipCount,totalCount,urlName],duration,number,timestamp,result,building,url]]]]]]]]]]]&exclude=/*/*/*/action[not(totalCount)]") + public abstract ProjectsList getProjects(); + + @GET( + "/api/xml?tree=jobs[name,jobs[name,jobs[name,jobs[name,jobs[name,jobs[name,jobs[name,jobs[name,jobs[name,jobs[name]]]]]]]]]]") + public abstract JobList getJobs(); + + @GET( + "/job/{jobName}/api/xml?exclude=/*/build/action[not(totalCount)]&tree=builds[number,url,duration,timestamp,result,building,url,fullDisplayName,actions[failCount,skipCount,totalCount]]") + public abstract BuildsList getBuilds(@EncodedPath("jobName") String jobName); + + @GET( + "/job/{jobName}/api/xml?tree=name,url,actions[processes[name]],downstreamProjects[name,url],upstreamProjects[name,url]") + public abstract BuildDependencies getDependencies(@EncodedPath("jobName") String jobName); + + @GET( + "/job/{jobName}/{buildNumber}/api/xml?exclude=/*/action[not(totalCount)]&tree=actions[failCount,skipCount,totalCount,urlName],duration,number,timestamp,result,building,url,fullDisplayName,artifacts[displayPath,fileName,relativePath]") + public abstract Build getBuild( + @EncodedPath("jobName") String jobName, @Path("buildNumber") Integer buildNumber); + + @GET( + "/job/{jobName}/{buildNumber}/api/xml?exclude=/*/action[not(build|lastBuiltRevision)]&tree=actions[remoteUrls,lastBuiltRevision[branch[name,SHA1]],build[revision[branch[name,SHA1]]]]") + public abstract ScmDetails getGitDetails( + @EncodedPath("jobName") String jobName, @Path("buildNumber") Integer buildNumber); + + @GET("/job/{jobName}/lastCompletedBuild/api/xml") + public abstract Build getLatestBuild(@EncodedPath("jobName") String jobName); + + @GET("/queue/item/{itemNumber}/api/xml") + public abstract QueuedJob getQueuedItem(@Path("itemNumber") Integer item); + + @POST("/job/{jobName}/build") + public abstract Response build( + @EncodedPath("jobName") String jobName, + @Body String emptyRequest, + @Header("Jenkins-Crumb") String crumb); + + @POST("/job/{jobName}/buildWithParameters") + public abstract Response buildWithParameters( + @EncodedPath("jobName") String jobName, + @QueryMap Map queryParams, + @Body String EmptyRequest, + @Header("Jenkins-Crumb") String crumb); + + @POST("/job/{jobName}/{buildNumber}/stop") + public abstract Response stopRunningBuild( + @EncodedPath("jobName") String jobName, + @Path("buildNumber") Integer buildNumber, + @Body String EmptyRequest, + @Header("Jenkins-Crumb") String crumb); + + @POST("/queue/cancelItem") + public abstract Response stopQueuedBuild( + @Query("id") String queuedBuild, + @Body String emptyRequest, + @Header("Jenkins-Crumb") String crumb); + + @GET( + "/job/{jobName}/api/xml?exclude=/*/action&exclude=/*/build&exclude=/*/property[not(parameterDefinition)]") + public abstract JobConfig getJobConfig(@EncodedPath("jobName") String jobName); + + @Streaming + @GET("/job/{jobName}/{buildNumber}/artifact/{fileName}") + public abstract Response getPropertyFile( + @EncodedPath("jobName") String jobName, + @Path("buildNumber") Integer buildNumber, + @Path(value = "fileName", encode = false) String fileName); + + @GET("/crumbIssuer/api/xml") + public abstract Crumb getCrumb(); +} diff --git a/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/JenkinsMasters.java b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/JenkinsMasters.java new file mode 100644 index 000000000..9d2b6adc6 --- /dev/null +++ b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/JenkinsMasters.java @@ -0,0 +1,17 @@ +package com.netflix.spinnaker.igor.jenkins.client; + +import com.netflix.spinnaker.igor.jenkins.JenkinsService; +import java.util.Map; + +/** Wrapper class for a collection of jenkins clients */ +public class JenkinsMasters { + public Map getMap() { + return map; + } + + public void setMap(Map map) { + this.map = map; + } + + private Map map; +} diff --git a/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/Action.java b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/Action.java new file mode 100644 index 000000000..a303b18dc --- /dev/null +++ b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/Action.java @@ -0,0 +1,53 @@ +/* + * Copyright 2020 Netflix, 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.netflix.spinnaker.igor.jenkins.client.model; + +import javax.xml.bind.annotation.XmlElement; + +public class Action { + public Revision getLastBuiltRevision() { + return lastBuiltRevision; + } + + public void setLastBuiltRevision(Revision lastBuiltRevision) { + this.lastBuiltRevision = lastBuiltRevision; + } + + public ScmBuild getBuild() { + return build; + } + + public void setBuild(ScmBuild build) { + this.build = build; + } + + public String getRemoteUrl() { + return remoteUrl; + } + + public void setRemoteUrl(String remoteUrl) { + this.remoteUrl = remoteUrl; + } + + @XmlElement(required = false) + private Revision lastBuiltRevision; + + @XmlElement(required = false) + private ScmBuild build; + + @XmlElement(required = false) + private String remoteUrl; +} diff --git a/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/Branch.java b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/Branch.java new file mode 100644 index 000000000..8ad3725d4 --- /dev/null +++ b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/Branch.java @@ -0,0 +1,55 @@ +/* + * Copyright 2020 Netflix, 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.netflix.spinnaker.igor.jenkins.client.model; + +import javax.xml.bind.annotation.XmlElement; + +public class Branch { + + @XmlElement(required = false) + private String name; + + @XmlElement(required = false, name = "SHA1") + private String sha1; + + /** + * Given a full branch reference (e.g. {@code origin/master}), this method will return the branch + * name without the repository (e.g. {@code master}). + */ + public String getSimpleBranchName() { + if (name == null) { + return null; + } + String[] segments = name.split("/"); + return segments[segments.length - 1]; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getSha1() { + return sha1; + } + + public void setSha1(String sha1) { + this.sha1 = sha1; + } +} diff --git a/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/Build.java b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/Build.java new file mode 100644 index 000000000..d3358b787 --- /dev/null +++ b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/Build.java @@ -0,0 +1,184 @@ +package com.netflix.spinnaker.igor.jenkins.client.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.netflix.spinnaker.igor.build.model.GenericArtifact; +import com.netflix.spinnaker.igor.build.model.GenericBuild; +import com.netflix.spinnaker.igor.build.model.Result; +import java.util.List; +import java.util.stream.Collectors; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +/** Represents a build in Jenkins */ +@JsonInclude(JsonInclude.Include.NON_NULL) +@XmlRootElement +public class Build { + public GenericBuild genericBuild(final String jobName) { + GenericBuild.GenericBuildBuilder builder = + GenericBuild.builder() + .building(building) + .number(number) + .duration(duration.intValue()) + // TODO(rz): Groovyism. What does Groovy do when `result` is null and you cast it to an + // enum? WHO KNOWS, but + // all of the igor tests depend on _something_ being set from null. + .result((result == null) ? Result.NOT_BUILT : Result.valueOf(result)) + .name(jobName) + .url(url) + .timestamp(timestamp) + .fullDisplayName(fullDisplayName); + + if (artifacts != null && !artifacts.isEmpty()) { + builder.artifacts( + artifacts.stream() + .map( + buildArtifact -> { + GenericArtifact artifact = buildArtifact.getGenericArtifact(); + artifact.setName(jobName); + artifact.setVersion(getNumber().toString()); + return artifact; + }) + .collect(Collectors.toList())); + } + + if (testResults != null && !testResults.isEmpty()) { + builder.testResults(testResults); + } + + return builder.build(); + } + + public boolean getBuilding() { + return building; + } + + public boolean isBuilding() { + return building; + } + + public void setBuilding(boolean building) { + this.building = building; + } + + public Integer getNumber() { + return number; + } + + public void setNumber(Integer number) { + this.number = number; + } + + public String getResult() { + return result; + } + + public void setResult(String result) { + this.result = result; + } + + public String getTimestamp() { + return timestamp; + } + + public void setTimestamp(String timestamp) { + this.timestamp = timestamp; + } + + public Long getDuration() { + return duration; + } + + public void setDuration(Long duration) { + this.duration = duration; + } + + public Integer getEstimatedDuration() { + return estimatedDuration; + } + + public void setEstimatedDuration(Integer estimatedDuration) { + this.estimatedDuration = estimatedDuration; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getBuiltOn() { + return builtOn; + } + + public void setBuiltOn(String builtOn) { + this.builtOn = builtOn; + } + + public String getFullDisplayName() { + return fullDisplayName; + } + + public void setFullDisplayName(String fullDisplayName) { + this.fullDisplayName = fullDisplayName; + } + + public List getArtifacts() { + return artifacts; + } + + public void setArtifacts(List artifacts) { + this.artifacts = artifacts; + } + + public List getTestResults() { + return testResults; + } + + public void setTestResults(List testResults) { + this.testResults = testResults; + } + + private boolean building; + private Integer number; + + @XmlElement(required = false) + private String result; + + private String timestamp; + + @XmlElement(required = false) + private Long duration; + + @XmlElement(required = false) + private Integer estimatedDuration; + + @XmlElement(required = false) + private String id; + + private String url; + + @XmlElement(required = false) + private String builtOn; + + @XmlElement(required = false) + private String fullDisplayName; + + @JacksonXmlElementWrapper(useWrapping = false) + @XmlElement(name = "artifact", required = false) + private List artifacts; + + @JacksonXmlElementWrapper(useWrapping = false) + @XmlElement(name = "action", required = false) + private List testResults; +} diff --git a/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/BuildArtifact.java b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/BuildArtifact.java new file mode 100644 index 000000000..f19926c24 --- /dev/null +++ b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/BuildArtifact.java @@ -0,0 +1,49 @@ +package com.netflix.spinnaker.igor.jenkins.client.model; + +import com.netflix.spinnaker.igor.build.model.GenericArtifact; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +/** Represents a build artifact */ +@XmlRootElement(name = "artifact") +public class BuildArtifact { + public GenericArtifact getGenericArtifact() { + GenericArtifact artifact = new GenericArtifact(fileName, displayPath, relativePath); + artifact.setType("jenkins/file"); + artifact.setReference(relativePath); + return artifact; + } + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public String getDisplayPath() { + return displayPath; + } + + public void setDisplayPath(String displayPath) { + this.displayPath = displayPath; + } + + public String getRelativePath() { + return relativePath; + } + + public void setRelativePath(String relativePath) { + this.relativePath = relativePath; + } + + @XmlElement(required = false) + private String fileName; + + @XmlElement(required = false) + private String displayPath; + + @XmlElement(required = false) + private String relativePath; +} diff --git a/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/BuildDependencies.java b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/BuildDependencies.java new file mode 100644 index 000000000..921b5a564 --- /dev/null +++ b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/BuildDependencies.java @@ -0,0 +1,34 @@ +package com.netflix.spinnaker.igor.jenkins.client.model; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import java.util.List; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +/** Captures build dependencies for a jenkins job */ +@XmlRootElement +public class BuildDependencies { + public List getDownstreamProjects() { + return downstreamProjects; + } + + public void setDownstreamProjects(List downstreamProjects) { + this.downstreamProjects = downstreamProjects; + } + + public List getUpstreamProjects() { + return upstreamProjects; + } + + public void setUpstreamProjects(List upstreamProjects) { + this.upstreamProjects = upstreamProjects; + } + + @JacksonXmlElementWrapper(useWrapping = false) + @XmlElement(name = "downstreamProject", required = false) + private List downstreamProjects; + + @JacksonXmlElementWrapper(useWrapping = false) + @XmlElement(name = "upstreamProject", required = false) + private List upstreamProjects; +} diff --git a/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/BuildDependency.java b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/BuildDependency.java new file mode 100644 index 000000000..57db4a4f2 --- /dev/null +++ b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/BuildDependency.java @@ -0,0 +1,26 @@ +package com.netflix.spinnaker.igor.jenkins.client.model; + +import org.simpleframework.xml.Default; + +/** Represents either an upstream or downstream dependency in Jenkins */ +@Default +public class BuildDependency { + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + private String name; + private String url; +} diff --git a/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/BuildsList.java b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/BuildsList.java new file mode 100644 index 000000000..2a7119b3a --- /dev/null +++ b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/BuildsList.java @@ -0,0 +1,22 @@ +package com.netflix.spinnaker.igor.jenkins.client.model; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import java.util.List; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +/** Represents a list of builds */ +@XmlRootElement +public class BuildsList { + public List getList() { + return list; + } + + public void setList(List list) { + this.list = list; + } + + @JacksonXmlElementWrapper(useWrapping = false) + @XmlElement(name = "build") + private List list; +} diff --git a/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/Crumb.java b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/Crumb.java new file mode 100644 index 000000000..f9d1e3da9 --- /dev/null +++ b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/Crumb.java @@ -0,0 +1,27 @@ +package com.netflix.spinnaker.igor.jenkins.client.model; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +/** Represents a Jenkins CSRF Crumb. */ +@XmlRootElement +public class Crumb { + public String getCrumbRequestField() { + return crumbRequestField; + } + + public void setCrumbRequestField(String crumbRequestField) { + this.crumbRequestField = crumbRequestField; + } + + public String getCrumb() { + return crumb; + } + + public void setCrumb(String crumb) { + this.crumb = crumb; + } + + @XmlElement private String crumbRequestField; + @XmlElement private String crumb; +} diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/DefaultParameterValue.java b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/DefaultParameterValue.java similarity index 100% rename from igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/DefaultParameterValue.java rename to igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/DefaultParameterValue.java diff --git a/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/DownstreamProject.java b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/DownstreamProject.java new file mode 100644 index 000000000..80b52f913 --- /dev/null +++ b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/DownstreamProject.java @@ -0,0 +1,7 @@ +package com.netflix.spinnaker.igor.jenkins.client.model; + +import org.simpleframework.xml.Root; + +/** Represents a Jenkins job downstream project */ +@Root(name = "downstreamProject", strict = false) +public class DownstreamProject extends RelatedProject {} diff --git a/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/Job.java b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/Job.java new file mode 100644 index 000000000..dec6fe205 --- /dev/null +++ b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/Job.java @@ -0,0 +1,45 @@ +/* + * Copyright 2020 Netflix, 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.netflix.spinnaker.igor.jenkins.client.model; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import java.util.List; +import javax.xml.bind.annotation.XmlElement; + +public class Job { + public List getList() { + return list; + } + + public void setList(List list) { + this.list = list; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @JacksonXmlElementWrapper(useWrapping = false) + @XmlElement(name = "job") + private List list; + + @XmlElement(required = false) + private String name; +} diff --git a/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/JobConfig.java b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/JobConfig.java new file mode 100644 index 000000000..f266a9f73 --- /dev/null +++ b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/JobConfig.java @@ -0,0 +1,123 @@ +package com.netflix.spinnaker.igor.jenkins.client.model; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.netflix.spinnaker.igor.build.model.JobConfiguration; +import java.util.List; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; +import javax.xml.bind.annotation.XmlRootElement; + +/** Represents the basic Jenkins job configuration information */ +@XmlRootElement +public class JobConfig implements JobConfiguration { + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean getBuildable() { + return buildable; + } + + public boolean isBuildable() { + return buildable; + } + + public void setBuildable(boolean buildable) { + this.buildable = buildable; + } + + public String getColor() { + return color; + } + + public void setColor(String color) { + this.color = color; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public List getParameterDefinitionList() { + return parameterDefinitionList; + } + + public void setParameterDefinitionList(List parameterDefinitionList) { + this.parameterDefinitionList = parameterDefinitionList; + } + + public List getUpstreamProjectList() { + return upstreamProjectList; + } + + public void setUpstreamProjectList(List upstreamProjectList) { + this.upstreamProjectList = upstreamProjectList; + } + + public List getDownstreamProjectList() { + return downstreamProjectList; + } + + public void setDownstreamProjectList(List downstreamProjectList) { + this.downstreamProjectList = downstreamProjectList; + } + + public boolean getConcurrentBuild() { + return concurrentBuild; + } + + public boolean isConcurrentBuild() { + return concurrentBuild; + } + + public void setConcurrentBuild(boolean concurrentBuild) { + this.concurrentBuild = concurrentBuild; + } + + @XmlElement(required = false) + private String description; + + @XmlElement private String displayName; + @XmlElement private String name; + @XmlElement private boolean buildable; + @XmlElement private String color; + @XmlElement private String url; + + @XmlElementWrapper(name = "property") + @XmlElement(name = "parameterDefinition", required = false) + private List parameterDefinitionList; + + @JacksonXmlElementWrapper(useWrapping = false) + @XmlElement(name = "upstreamProject", required = false) + private List upstreamProjectList; + + @JacksonXmlElementWrapper(useWrapping = false) + @XmlElement(name = "downstreamProject", required = false) + private List downstreamProjectList; + + @XmlElement private boolean concurrentBuild; +} diff --git a/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/JobList.java b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/JobList.java new file mode 100644 index 000000000..3f48473a3 --- /dev/null +++ b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/JobList.java @@ -0,0 +1,22 @@ +package com.netflix.spinnaker.igor.jenkins.client.model; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import java.util.List; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +/** Represents a list of projects */ +@XmlRootElement(name = "hudson") +public class JobList { + public List getList() { + return list; + } + + public void setList(List list) { + this.list = list; + } + + @JacksonXmlElementWrapper(useWrapping = false) + @XmlElement(name = "job") + private List list; +} diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/ParameterDefinition.java b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/ParameterDefinition.java similarity index 100% rename from igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/ParameterDefinition.java rename to igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/ParameterDefinition.java diff --git a/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/Project.java b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/Project.java new file mode 100644 index 000000000..b9d755a92 --- /dev/null +++ b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/Project.java @@ -0,0 +1,39 @@ +package com.netflix.spinnaker.igor.jenkins.client.model; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import java.util.List; +import javax.xml.bind.annotation.XmlElement; + +/** Represents a Project returned by the Jenkins service in the project list */ +public class Project { + public List getList() { + return list; + } + + public void setList(List list) { + this.list = list; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Build getLastBuild() { + return lastBuild; + } + + public void setLastBuild(Build lastBuild) { + this.lastBuild = lastBuild; + } + + @JacksonXmlElementWrapper(useWrapping = false) + @XmlElement(name = "job", required = false) + private List list; + + @XmlElement private String name; + @XmlElement private Build lastBuild; +} diff --git a/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/ProjectsList.java b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/ProjectsList.java new file mode 100644 index 000000000..0e4845a28 --- /dev/null +++ b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/ProjectsList.java @@ -0,0 +1,22 @@ +package com.netflix.spinnaker.igor.jenkins.client.model; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import java.util.List; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +/** Represents a list of projects */ +@XmlRootElement(name = "hudson") +public class ProjectsList { + public List getList() { + return list; + } + + public void setList(List list) { + this.list = list; + } + + @JacksonXmlElementWrapper(useWrapping = false) + @XmlElement(name = "job") + private List list; +} diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/history/model/BuildEvent.groovy b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/QueuedExecutable.java similarity index 62% rename from igor-web/src/main/groovy/com/netflix/spinnaker/igor/history/model/BuildEvent.groovy rename to igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/QueuedExecutable.java index b612497f7..6ffd716de 100644 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/history/model/BuildEvent.groovy +++ b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/QueuedExecutable.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 Netflix, Inc. + * Copyright 2020 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,20 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package com.netflix.spinnaker.igor.jenkins.client.model; -package com.netflix.spinnaker.igor.history.model +import javax.xml.bind.annotation.XmlElement; -/** - * A history entry that contains a build detail - * - * TODO(rz): Cannot move to kork-core due to Jenkins dependency - */ -class BuildEvent extends Event { +public class QueuedExecutable { + public Integer getNumber() { + return number; + } - BuildContent content - Map details = [ - type : 'build', - source: 'igor' - ] + public void setNumber(Integer number) { + this.number = number; + } + @XmlElement private Integer number; } diff --git a/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/QueuedJob.java b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/QueuedJob.java new file mode 100644 index 000000000..0d7bbb7de --- /dev/null +++ b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/QueuedJob.java @@ -0,0 +1,23 @@ +package com.netflix.spinnaker.igor.jenkins.client.model; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +@XmlRootElement +public class QueuedJob { + @XmlElement(name = "number") + public Integer getNumber() { + final QueuedExecutable executable1 = executable; + return (executable1 == null ? null : executable1.getNumber()); + } + + public QueuedExecutable getExecutable() { + return executable; + } + + public void setExecutable(QueuedExecutable executable) { + this.executable = executable; + } + + @XmlElement private QueuedExecutable executable; +} diff --git a/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/RelatedProject.java b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/RelatedProject.java new file mode 100644 index 000000000..c99edd5f9 --- /dev/null +++ b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/RelatedProject.java @@ -0,0 +1,36 @@ +package com.netflix.spinnaker.igor.jenkins.client.model; + +import org.simpleframework.xml.Element; +import org.simpleframework.xml.Root; + +/** Represents a upstream/downstream project for a Jenkins job */ +@Root(strict = false) +public class RelatedProject { + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getColor() { + return color; + } + + public void setColor(String color) { + this.color = color; + } + + @Element private String name; + @Element private String url; + @Element private String color; +} diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/BuildsList.groovy b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/Revision.java similarity index 54% rename from igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/BuildsList.groovy rename to igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/Revision.java index 5671fa504..95480661c 100644 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/BuildsList.groovy +++ b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/Revision.java @@ -1,11 +1,11 @@ /* - * Copyright 2014 Netflix, Inc. + * Copyright 2020 Netflix, 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 + * 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, @@ -13,22 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package com.netflix.spinnaker.igor.jenkins.client.model; -package com.netflix.spinnaker.igor.jenkins.client.model +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import java.util.List; +import javax.xml.bind.annotation.XmlElement; -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper -import groovy.transform.CompileStatic +public class Revision { + public List getBranch() { + return branch; + } -import javax.xml.bind.annotation.XmlElement -import javax.xml.bind.annotation.XmlRootElement + public void setBranch(List branch) { + this.branch = branch; + } -/** - * Represents a list of builds - */ -@XmlRootElement -@CompileStatic -class BuildsList { - @JacksonXmlElementWrapper(useWrapping=false) - @XmlElement(name = 'build') - List list + @JacksonXmlElementWrapper(useWrapping = false) + @XmlElement(name = "branch") + private List branch; } diff --git a/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/ScmBuild.java b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/ScmBuild.java new file mode 100644 index 000000000..89bb1abf6 --- /dev/null +++ b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/ScmBuild.java @@ -0,0 +1,31 @@ +/* + * Copyright 2020 Netflix, 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.netflix.spinnaker.igor.jenkins.client.model; + +import javax.xml.bind.annotation.XmlElement; + +public class ScmBuild { + public Revision getRevision() { + return revision; + } + + public void setRevision(Revision revision) { + this.revision = revision; + } + + @XmlElement(required = false) + private Revision revision; +} diff --git a/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/ScmDetails.java b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/ScmDetails.java new file mode 100644 index 000000000..8fd9c50ae --- /dev/null +++ b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/ScmDetails.java @@ -0,0 +1,78 @@ +package com.netflix.spinnaker.igor.jenkins.client.model; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.netflix.spinnaker.igor.build.model.GenericGitRevision; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import javax.xml.bind.annotation.XmlElement; + +/** + * Represents git details + * + *

The serialization of these details in the Jenkins build XML changed in version 4.0.0 of the + * jenkins-git plugin. + * + *

Prior to 4.0.0, the format was: + * 943a702d06f34599aee1f8da8ef9f7296031d699 + * refs/remotes/origin/master + * some-url + * + *

As of version 4.0.0, the format is: + * 943a702d06f34599aee1f8da8ef9f7296031d699 + * refs/remotes/origin/master some-url + * + * + *

The code in this module should remain compatible with both formats to ensure that SCM info is + * populated in Spinnaker regardless of which version of the jenkins-git plugin is being used. + */ +public class ScmDetails { + /** TODO(rz): Rename to gitRevisions */ + public List genericGitRevisions() { + List genericGitRevisions = new ArrayList<>(); + + if (actions == null) { + return null; + } + + for (Action action : actions) { + final Revision lastBuiltRevision = (action == null ? null : action.getLastBuiltRevision()); + final ScmBuild build = (action == null ? null : action.getBuild()); + final Revision revision = + (lastBuiltRevision == null) + ? (build == null ? null : build.getRevision()) + : lastBuiltRevision; + final List branch = (revision == null ? null : revision.getBranch()); + + if (branch != null && !branch.isEmpty()) { + genericGitRevisions.addAll( + branch.stream() + .map( + b -> + GenericGitRevision.builder() + .name(b.getName()) + .branch(b.getSimpleBranchName()) + .sha1(b.getSha1()) + .remoteUrl(action.getRemoteUrl()) + .build()) + .collect(Collectors.toList())); + } + } + + // If the same revision appears in both the old and new locations in the XML, we only want to + // return it once. + return genericGitRevisions.stream().distinct().collect(Collectors.toList()); + } + + public ArrayList getActions() { + return actions; + } + + public void setActions(ArrayList actions) { + this.actions = actions; + } + + @JacksonXmlElementWrapper(useWrapping = false) + @XmlElement(name = "action") + private ArrayList actions; +} diff --git a/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/TestResults.java b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/TestResults.java new file mode 100644 index 000000000..390629606 --- /dev/null +++ b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/TestResults.java @@ -0,0 +1,53 @@ +package com.netflix.spinnaker.igor.jenkins.client.model; + +import com.netflix.spinnaker.igor.build.model.TestResult; +import org.simpleframework.xml.Element; +import org.simpleframework.xml.Root; + +/** Represents a build artifact */ +@Root(name = "action", strict = false) +public class TestResults implements TestResult { + public int getFailCount() { + return failCount; + } + + public void setFailCount(int failCount) { + this.failCount = failCount; + } + + public int getSkipCount() { + return skipCount; + } + + public void setSkipCount(int skipCount) { + this.skipCount = skipCount; + } + + public int getTotalCount() { + return totalCount; + } + + public void setTotalCount(int totalCount) { + this.totalCount = totalCount; + } + + public String getUrlName() { + return urlName; + } + + public void setUrlName(String urlName) { + this.urlName = urlName; + } + + @Element(required = false) + private int failCount; + + @Element(required = false) + private int skipCount; + + @Element(required = false) + private int totalCount; + + @Element(required = false) + private String urlName; +} diff --git a/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/UpstreamProject.java b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/UpstreamProject.java new file mode 100644 index 000000000..e8bf377a6 --- /dev/null +++ b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/client/model/UpstreamProject.java @@ -0,0 +1,4 @@ +package com.netflix.spinnaker.igor.jenkins.client.model; + +/** Represents a Jenkins job upstream project */ +public class UpstreamProject extends RelatedProject {} diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/history/model/BuildContent.groovy b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/exceptions/InvalidJobParameterException.java similarity index 64% rename from igor-web/src/main/groovy/com/netflix/spinnaker/igor/history/model/BuildContent.groovy rename to igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/exceptions/InvalidJobParameterException.java index ebcf22096..04797182a 100644 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/history/model/BuildContent.groovy +++ b/igor-monitor-jenkins/src/main/java/com/netflix/spinnaker/igor/jenkins/exceptions/InvalidJobParameterException.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 Netflix, Inc. + * Copyright 2020 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,17 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package com.netflix.spinnaker.igor.jenkins.exceptions; -package com.netflix.spinnaker.igor.history.model +import com.netflix.spinnaker.kork.web.exceptions.InvalidRequestException; -import com.netflix.spinnaker.igor.jenkins.client.model.Project - -/** - * Encapsulates a build content block - * - * TODO(rz): Cannot move to kork-core due to Jenkins dependency - */ -class BuildContent { - Project project - String master +public class InvalidJobParameterException extends InvalidRequestException { + public InvalidJobParameterException(String message) { + super(message); + } } diff --git a/igor-web/src/test/groovy/com/netflix/spinnaker/igor/jenkins/service/JenkinsServiceSpec.groovy b/igor-monitor-jenkins/src/test/groovy/com/netflix/spinnaker/igor/jenkins/JenkinsServiceSpec.groovy similarity index 89% rename from igor-web/src/test/groovy/com/netflix/spinnaker/igor/jenkins/service/JenkinsServiceSpec.groovy rename to igor-monitor-jenkins/src/test/groovy/com/netflix/spinnaker/igor/jenkins/JenkinsServiceSpec.groovy index ecac2591b..fb2f10862 100644 --- a/igor-web/src/test/groovy/com/netflix/spinnaker/igor/jenkins/service/JenkinsServiceSpec.groovy +++ b/igor-monitor-jenkins/src/test/groovy/com/netflix/spinnaker/igor/jenkins/JenkinsServiceSpec.groovy @@ -5,7 +5,7 @@ * 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 + * 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, @@ -14,30 +14,45 @@ * limitations under the License. */ -package com.netflix.spinnaker.igor.jenkins.service +package com.netflix.spinnaker.igor.jenkins import com.netflix.spinnaker.fiat.model.resources.Permissions import com.netflix.spinnaker.igor.build.model.GenericBuild import com.netflix.spinnaker.igor.build.model.GenericGitRevision import com.netflix.spinnaker.igor.config.JenkinsConfig import com.netflix.spinnaker.igor.config.JenkinsProperties +import com.netflix.spinnaker.igor.jenkins.JenkinsService import com.netflix.spinnaker.igor.jenkins.client.JenkinsClient import com.netflix.spinnaker.igor.jenkins.client.model.Build import com.netflix.spinnaker.igor.jenkins.client.model.BuildArtifact import com.netflix.spinnaker.igor.jenkins.client.model.BuildsList +import com.netflix.spinnaker.igor.jenkins.client.model.Job +import com.netflix.spinnaker.igor.jenkins.client.model.JobConfig +import com.netflix.spinnaker.igor.jenkins.client.model.JobList +import com.netflix.spinnaker.igor.jenkins.client.model.ParameterDefinition import com.netflix.spinnaker.igor.jenkins.client.model.Project -import com.netflix.spinnaker.kork.exceptions.SpinnakerException +import com.netflix.spinnaker.igor.jenkins.exceptions.InvalidJobParameterException import com.squareup.okhttp.mockwebserver.MockResponse import com.squareup.okhttp.mockwebserver.MockWebServer import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.mock.web.MockHttpServletResponse import retrofit.RetrofitError +import retrofit.client.Header import retrofit.client.Response -import retrofit.mime.TypedInput import retrofit.mime.TypedString +import spock.lang.Ignore import spock.lang.Shared import spock.lang.Specification import spock.lang.Unroll +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put + @SuppressWarnings(['LineLength', 'DuplicateNumberLiteral']) class JenkinsServiceSpec extends Specification { static { @@ -146,7 +161,7 @@ class JenkinsServiceSpec extends Specification { 'build' | [] 'buildWithParameters' | [['key': 'value']] 'stopRunningBuild' | [1] - 'stopQueuedBuild' | [] + 'stopQueuedBuild' | ["hello darkness", 1] } void 'we can read crumbs'() { @@ -288,8 +303,7 @@ class JenkinsServiceSpec extends Specification { password: 'password') client = new JenkinsConfig().jenkinsClient(host) service = new JenkinsService('http://my.jenkins.net', client, false, Permissions.EMPTY) - def genericBuild = new GenericBuild() - genericBuild.number = 1 + def genericBuild = GenericBuild.builder().number(1).build() when: List genericGitRevision = service.getGenericGitRevisions('test', genericBuild) @@ -343,8 +357,7 @@ class JenkinsServiceSpec extends Specification { password: 'password') client = new JenkinsConfig().jenkinsClient(host) service = new JenkinsService('http://my.jenkins.net', client, false, Permissions.EMPTY) - def genericBuild = new GenericBuild() - genericBuild.number = 1 + def genericBuild = GenericBuild.builder().number(1).build() when: List genericGitRevision = service.getGenericGitRevisions('test', genericBuild) @@ -398,8 +411,7 @@ class JenkinsServiceSpec extends Specification { password: 'password') client = new JenkinsConfig().jenkinsClient(host) service = new JenkinsService('http://my.jenkins.net', client, false, Permissions.EMPTY) - def genericBuild = new GenericBuild() - genericBuild.number = 1 + def genericBuild = GenericBuild.builder().number(1).build() when: List genericGitRevision = service.getGenericGitRevisions('test', genericBuild) @@ -459,8 +471,7 @@ class JenkinsServiceSpec extends Specification { password: 'password') client = new JenkinsConfig().jenkinsClient(host) service = new JenkinsService('http://my.jenkins.net', client, false, Permissions.EMPTY) - def genericBuild = new GenericBuild() - genericBuild.number = 1 + def genericBuild = GenericBuild.builder().number(1).build() when: List genericGitRevision = service.getGenericGitRevisions('test', genericBuild) @@ -523,8 +534,7 @@ class JenkinsServiceSpec extends Specification { password: 'password') client = new JenkinsConfig().jenkinsClient(host) service = new JenkinsService('http://my.jenkins.net', client, false, Permissions.EMPTY) - def genericBuild = new GenericBuild() - genericBuild.number = 1 + def genericBuild = GenericBuild.builder().number(1).build() when: List genericGitRevision = service.getGenericGitRevisions('test', genericBuild) @@ -587,8 +597,7 @@ class JenkinsServiceSpec extends Specification { password: 'password') client = new JenkinsConfig().jenkinsClient(host) service = new JenkinsService('http://my.jenkins.net', client, false, Permissions.EMPTY) - def genericBuild = new GenericBuild() - genericBuild.number = 1 + def genericBuild = GenericBuild.builder().number(1).build() when: List genericGitRevision = service.getGenericGitRevisions('test', genericBuild) @@ -641,8 +650,7 @@ class JenkinsServiceSpec extends Specification { password: 'password') client = new JenkinsConfig().jenkinsClient(host) service = new JenkinsService('http://my.jenkins.net', client, false, Permissions.EMPTY) - def genericBuild = new GenericBuild() - genericBuild.number = 1 + def genericBuild = GenericBuild.builder().number(1).build() expect: service.getBuildProperties("PropertiesTest", genericBuild, "props$extension") == testCase.result @@ -751,4 +759,54 @@ class JenkinsServiceSpec extends Specification { properties == [:] } + + void 'get jobs for a jenkins master with the folders plugin'() { + when: + def result = service.getJobNames() + + then: + 1 * client.getJobs() >> new JobList(list: [ + new Job(name: 'folder', list: [ + new Job(name: 'job1'), + new Job(name: 'job2') + ]), + new Job(name: 'job3') + ]) + result == ["folder/job/job1", "folder/job/job2", "job3"] + } + + void 'validation successful for null list of choices'() { + given: + Map requestParams = ["hey" : "you"] + ParameterDefinition parameterDefinition = new ParameterDefinition() + parameterDefinition.choices = null + parameterDefinition.type = "ChoiceParameterDefinition" + parameterDefinition.name = "hey" + JobConfig jobConfig = new JobConfig() + jobConfig.parameterDefinitionList = [parameterDefinition] + + when: + JenkinsService.validateJobParameters(jobConfig, requestParams) + + then: + noExceptionThrown() + } + + void 'validation failed for option not in list of choices'() { + given: + Map requestParams = ["hey" : "you"] + ParameterDefinition parameterDefinition = new ParameterDefinition() + parameterDefinition.choices = ["why", "not"] + parameterDefinition.type = "ChoiceParameterDefinition" + parameterDefinition.name = "hey" + JobConfig jobConfig = new JobConfig() + jobConfig.parameterDefinitionList = [parameterDefinition] + + when: + JenkinsService.validateJobParameters(jobConfig, requestParams) + + then: + thrown(InvalidJobParameterException) + } + } diff --git a/igor-web/src/test/groovy/com/netflix/spinnaker/igor/jenkins/client/JenkinsClientSpec.groovy b/igor-monitor-jenkins/src/test/groovy/com/netflix/spinnaker/igor/jenkins/client/JenkinsClientSpec.groovy similarity index 99% rename from igor-web/src/test/groovy/com/netflix/spinnaker/igor/jenkins/client/JenkinsClientSpec.groovy rename to igor-monitor-jenkins/src/test/groovy/com/netflix/spinnaker/igor/jenkins/client/JenkinsClientSpec.groovy index b377c7bf3..ffd3cb7f2 100644 --- a/igor-web/src/test/groovy/com/netflix/spinnaker/igor/jenkins/client/JenkinsClientSpec.groovy +++ b/igor-monitor-jenkins/src/test/groovy/com/netflix/spinnaker/igor/jenkins/client/JenkinsClientSpec.groovy @@ -20,10 +20,10 @@ import com.netflix.spinnaker.igor.config.JenkinsConfig import com.netflix.spinnaker.igor.config.JenkinsProperties import com.netflix.spinnaker.igor.jenkins.client.model.Build import com.netflix.spinnaker.igor.jenkins.client.model.BuildArtifact +import com.netflix.spinnaker.igor.jenkins.client.model.Crumb import com.netflix.spinnaker.igor.jenkins.client.model.JobConfig import com.netflix.spinnaker.igor.jenkins.client.model.Project import com.netflix.spinnaker.igor.jenkins.client.model.ProjectsList -import com.netflix.spinnaker.igor.model.Crumb import com.squareup.okhttp.mockwebserver.MockResponse import com.squareup.okhttp.mockwebserver.MockWebServer import spock.lang.Shared diff --git a/igor-web/igor-web.gradle b/igor-web/igor-web.gradle index 25e0371b3..b95f6a4b9 100644 --- a/igor-web/igor-web.gradle +++ b/igor-web/igor-web.gradle @@ -18,8 +18,8 @@ test { dependencies { implementation project(":igor-core") implementation project(":igor-monitor-artifactory") + implementation project(":igor-monitor-jenkins") - implementation platform("com.netflix.spinnaker.kork:kork-bom:$korkVersion") compileOnly "org.projectlombok:lombok" annotationProcessor platform("com.netflix.spinnaker.kork:kork-bom:$korkVersion") annotationProcessor "org.projectlombok:lombok" diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/artifacts/ArtifactExtractor.java b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/artifacts/ArtifactExtractor.java index f11d0155c..85c31e975 100644 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/artifacts/ArtifactExtractor.java +++ b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/artifacts/ArtifactExtractor.java @@ -31,7 +31,7 @@ import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; -/** TODO(rz): Cannot move to kork-core due to Jenkins dependency */ +/** TODO(rz): Cannot move to igor-core due to Jenkins dependency */ @Component @RequiredArgsConstructor @Slf4j diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/build/BuildController.groovy b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/build/BuildController.groovy index 38ddba247..01981ef48 100644 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/build/BuildController.groovy +++ b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/build/BuildController.groovy @@ -19,19 +19,13 @@ package com.netflix.spinnaker.igor.build import com.netflix.spinnaker.igor.artifacts.ArtifactExtractor import com.netflix.spinnaker.igor.build.model.GenericBuild -import com.netflix.spinnaker.igor.exceptions.BuildJobError -import com.netflix.spinnaker.igor.exceptions.QueuedJobDeterminationError -import com.netflix.spinnaker.igor.jenkins.client.model.JobConfig -import com.netflix.spinnaker.igor.jenkins.service.JenkinsService import com.netflix.spinnaker.igor.service.ArtifactDecorator import com.netflix.spinnaker.igor.service.BuildOperations import com.netflix.spinnaker.igor.service.BuildProperties import com.netflix.spinnaker.igor.service.BuildServices -import com.netflix.spinnaker.igor.travis.service.TravisService +import com.netflix.spinnaker.igor.service.BuildQueueOperations import com.netflix.spinnaker.kork.artifacts.model.Artifact -import com.netflix.spinnaker.kork.web.exceptions.InvalidRequestException import com.netflix.spinnaker.kork.web.exceptions.NotFoundException -import groovy.transform.InheritConstructors import groovy.util.logging.Slf4j import org.springframework.security.access.prepost.PreAuthorize import org.springframework.web.bind.annotation.PathVariable @@ -118,12 +112,8 @@ class BuildController { @PreAuthorize("hasPermission(#master, 'BUILD_SERVICE', 'READ')") Object getQueueLocation(@PathVariable String master, @PathVariable int item) { def buildService = getBuildService(master) - if (buildService instanceof JenkinsService) { - JenkinsService jenkinsService = (JenkinsService) buildService - return jenkinsService.queuedBuild(item) - } else if (buildService instanceof TravisService) { - TravisService travisService = (TravisService) buildService - return travisService.queuedBuild(item) + if (buildService instanceof BuildQueueOperations) { + return buildService.getQueuedBuild(String.valueOf(item)); } throw new UnsupportedOperationException(String.format("Queued builds are not supported for build service %s", master)) } @@ -146,28 +136,11 @@ class BuildController { @PathVariable Integer buildNumber) { def buildService = getBuildService(master) - if (buildService instanceof JenkinsService) { - // Jobs that haven't been started yet won't have a buildNumber - // (They're still in the queue). We use 0 to denote that case - if (buildNumber != 0 && - buildService.metaClass.respondsTo(buildService, 'stopRunningBuild')) { - buildService.stopRunningBuild(jobName, buildNumber) - } - - // The jenkins api for removing a job from the queue (http:///queue/cancelItem?id=) - // always returns a 404. This try catch block insures that the exception is eaten instead - // of being handled by the handleOtherException handler and returning a 500 to orca - try { - if (buildService.metaClass.respondsTo(buildService, 'stopQueuedBuild')) { - buildService.stopQueuedBuild(queuedBuild) - } - } catch (RetrofitError e) { - if (e.response?.status != NOT_FOUND.value()) { - throw e - } - } + if (buildService instanceof BuildQueueOperations) { + buildService.stopQueuedBuild(jobName, queuedBuild, buildNumber) } + // TODO(rz): lol, for real? "true" } @@ -178,58 +151,7 @@ class BuildController { @RequestParam Map requestParams, HttpServletRequest request) { def job = ((String) request.getAttribute( HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE)).split('/').drop(4).join('/') - def buildService = getBuildService(master) - if (buildService instanceof JenkinsService) { - def response - JenkinsService jenkinsService = (JenkinsService) buildService - JobConfig jobConfig = jenkinsService.getJobConfig(job) - if (!jobConfig.buildable) { - throw new BuildJobError("Job '${job}' is not buildable. It may be disabled.") - } - - if (jobConfig.parameterDefinitionList?.size() > 0) { - validateJobParameters(jobConfig, requestParams) - } - if (requestParams && jobConfig.parameterDefinitionList?.size() > 0) { - response = jenkinsService.buildWithParameters(job, requestParams) - } else if (!requestParams && jobConfig.parameterDefinitionList?.size() > 0) { - // account for when you just want to fire a job with the default parameter values by adding a dummy param - response = jenkinsService.buildWithParameters(job, ['startedBy': "igor"]) - } else if (!requestParams && (!jobConfig.parameterDefinitionList || jobConfig.parameterDefinitionList.size() == 0)) { - response = jenkinsService.build(job) - } else { // Jenkins will reject the build, so don't even try - // we should throw a BuildJobError, but I get a bytecode error : java.lang.VerifyError: Bad method call from inside of a branch - throw new RuntimeException("job : ${job}, passing params to a job which doesn't need them") - } - - if (response.status != 201) { - throw new BuildJobError("Received a non-201 status when submitting job '${job}' to master '${master}'") - } - - log.info("Submitted build job '{}'", kv("job", job)) - def locationHeader = response.headers.find { it.name.toLowerCase() == "location" } - if (!locationHeader) { - throw new QueuedJobDeterminationError("Could not find Location header for job '${job}'") - } - def queuedLocation = locationHeader.value - - queuedLocation.split('/')[-1] - } else { - return buildService.triggerBuildWithParameters(job, requestParams) - } - } - - static void validateJobParameters(JobConfig jobConfig, Map requestParams) { - jobConfig.parameterDefinitionList.each { parameterDefinition -> - String matchingParam = requestParams[parameterDefinition.name] - if (matchingParam != null && - parameterDefinition.type == 'ChoiceParameterDefinition' && - parameterDefinition.choices != null && - !parameterDefinition.choices.contains(matchingParam)) { - throw new InvalidJobParameterException("`${matchingParam}` is not a valid choice " + - "for `${parameterDefinition.name}`. Valid choices are: ${parameterDefinition.choices.join(', ')}") - } - } + return getBuildService(master).triggerBuildWithParameters(job, requestParams) } @RequestMapping(value = '/builds/properties/{buildNumber}/{fileName}/{master:.+}/**') @@ -256,8 +178,4 @@ class BuildController { } return buildService } - - @InheritConstructors - static class InvalidJobParameterException extends InvalidRequestException {} - } diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/build/InfoController.groovy b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/build/InfoController.groovy index 9e9f2ddd7..9bab48938 100644 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/build/InfoController.groovy +++ b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/build/InfoController.groovy @@ -18,10 +18,10 @@ package com.netflix.spinnaker.igor.build import com.netflix.spinnaker.igor.config.GoogleCloudBuildProperties -import com.netflix.spinnaker.igor.jenkins.service.JenkinsService -import com.netflix.spinnaker.igor.model.BuildServiceProvider +import com.netflix.spinnaker.igor.service.BuildServiceProvider import com.netflix.spinnaker.igor.service.BuildService import com.netflix.spinnaker.igor.service.BuildServices +import com.netflix.spinnaker.igor.service.JobNamesProvider import com.netflix.spinnaker.igor.wercker.WerckerService import com.netflix.spinnaker.kork.web.exceptions.NotFoundException import groovy.util.logging.Slf4j @@ -38,7 +38,7 @@ import org.springframework.web.servlet.HandlerMapping import javax.servlet.http.HttpServletRequest /** - * A controller that provides jenkins information + * A controller that provides build service information */ @RestController @Slf4j @@ -85,31 +85,10 @@ class InfoController { throw new NotFoundException("Master '${master}' does not exist") } - if (buildService instanceof JenkinsService) { - JenkinsService jenkinsService = (JenkinsService) buildService - def jobList = [] - def recursiveGetJobs - - recursiveGetJobs = { list, prefix = "" -> - if (prefix) { - prefix = prefix + "/job/" - } - list.each { - if (it.list == null || it.list.empty) { - jobList << prefix + it.name - } else { - recursiveGetJobs(it.list, prefix + it.name) - } - } - } - recursiveGetJobs(jenkinsService.jobs.list) - - return jobList - } else if (buildService instanceof WerckerService) { - WerckerService werckerService = (WerckerService) buildService - return werckerService.getJobs() + if (buildService instanceof JobNamesProvider) { + return buildService.getJobNames() } else { - return buildCache.getJobNames(master) + return buildCache.getJobNames(master) } } diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/build/model/GenericProject.java b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/build/model/GenericProject.java index ab3852c0a..654f5d483 100644 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/build/model/GenericProject.java +++ b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/build/model/GenericProject.java @@ -19,6 +19,7 @@ import lombok.AllArgsConstructor; import lombok.Data; +/** TODO(rz): Jenkins in wrong module. */ @Data @AllArgsConstructor public class GenericProject { diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/config/JenkinsConfig.groovy b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/config/JenkinsConfig.groovy deleted file mode 100644 index 39d485fb0..000000000 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/config/JenkinsConfig.groovy +++ /dev/null @@ -1,210 +0,0 @@ -/* - * Copyright 2014 Netflix, 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.netflix.spinnaker.igor.config - -import com.fasterxml.jackson.databind.DeserializationFeature -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.dataformat.xml.XmlMapper -import com.fasterxml.jackson.module.jaxb.JaxbAnnotationModule -import com.netflix.spectator.api.Registry -import com.netflix.spinnaker.fiat.model.resources.Permissions -import com.netflix.spinnaker.igor.IgorConfigurationProperties -import com.netflix.spinnaker.igor.config.client.DefaultJenkinsOkHttpClientProvider -import com.netflix.spinnaker.igor.config.client.DefaultJenkinsRetrofitRequestInterceptorProvider -import com.netflix.spinnaker.igor.config.client.JenkinsOkHttpClientProvider -import com.netflix.spinnaker.igor.config.client.JenkinsRetrofitRequestInterceptorProvider -import com.netflix.spinnaker.igor.jenkins.client.JenkinsClient -import com.netflix.spinnaker.igor.jenkins.service.JenkinsService -import com.netflix.spinnaker.igor.service.BuildServices -import com.netflix.spinnaker.kork.telemetry.InstrumentedProxy -import com.squareup.okhttp.OkHttpClient -import groovy.transform.CompileStatic -import groovy.util.logging.Slf4j -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty -import org.springframework.boot.context.properties.EnableConfigurationProperties -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import retrofit.Endpoints -import retrofit.RequestInterceptor -import retrofit.RestAdapter -import retrofit.client.OkClient -import retrofit.converter.JacksonConverter - -import javax.net.ssl.KeyManager -import javax.net.ssl.KeyManagerFactory -import javax.net.ssl.SSLContext -import javax.net.ssl.TrustManager -import javax.net.ssl.TrustManagerFactory -import javax.net.ssl.X509TrustManager -import javax.validation.Valid -import java.security.KeyStore -import java.security.cert.CertificateException -import java.security.cert.X509Certificate -import java.util.concurrent.TimeUnit - -/** - * Converts the list of Jenkins Configuration properties a collection of clients to access the Jenkins hosts - */ -@Configuration -@Slf4j -@CompileStatic -@ConditionalOnProperty("jenkins.enabled") -@EnableConfigurationProperties(JenkinsProperties) -class JenkinsConfig { - - @Bean - @ConditionalOnMissingBean - JenkinsOkHttpClientProvider jenkinsOkHttpClientProvider() { - return new DefaultJenkinsOkHttpClientProvider() - } - - @Bean - @ConditionalOnMissingBean - JenkinsRetrofitRequestInterceptorProvider jenkinsRetrofitRequestInterceptorProvider() { - return new DefaultJenkinsRetrofitRequestInterceptorProvider() - } - - @Bean - Map jenkinsMasters(BuildServices buildServices, - IgorConfigurationProperties igorConfigurationProperties, - @Valid JenkinsProperties jenkinsProperties, - JenkinsOkHttpClientProvider jenkinsOkHttpClientProvider, - JenkinsRetrofitRequestInterceptorProvider jenkinsRetrofitRequestInterceptorProvider, - Registry registry) { - log.info "creating jenkinsMasters" - Map jenkinsMasters = jenkinsProperties?.masters?.collectEntries { JenkinsProperties.JenkinsHost host -> - log.info "bootstrapping ${host.address} as ${host.name}" - [(host.name): jenkinsService( - host.name, - (JenkinsClient) InstrumentedProxy.proxy( - registry, - jenkinsClient( - host, - jenkinsOkHttpClientProvider.provide(host), - jenkinsRetrofitRequestInterceptorProvider.provide(host), - igorConfigurationProperties.client.timeout - ), - "jenkinsClient", - [master: host.name]), - host.csrf, - host.permissions.build() - )] - } - - buildServices.addServices(jenkinsMasters) - jenkinsMasters - } - - static JenkinsService jenkinsService(String jenkinsHostId, JenkinsClient jenkinsClient, Boolean csrf, Permissions permissions) { - return new JenkinsService(jenkinsHostId, jenkinsClient, csrf, permissions) - } - - static ObjectMapper getObjectMapper() { - return new XmlMapper() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - .registerModule(new JaxbAnnotationModule()) - } - - static JenkinsClient jenkinsClient(JenkinsProperties.JenkinsHost host, - OkHttpClient client, - RequestInterceptor requestInterceptor, - int timeout = 30000) { - client.setReadTimeout(timeout, TimeUnit.MILLISECONDS) - - if (host.skipHostnameVerification) { - client.setHostnameVerifier({ hostname, _ -> - true - }) - } - - TrustManager[] trustManagers = null - KeyManager[] keyManagers = null - - if (host.trustStore) { - if (host.trustStore.equals("*")) { - trustManagers = [new TrustAllTrustManager()] - } else { - def trustStorePassword = host.trustStorePassword - def trustStore = KeyStore.getInstance(host.trustStoreType) - new File(host.trustStore).withInputStream { - trustStore.load(it, trustStorePassword.toCharArray()) - } - def trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) - trustManagerFactory.init(trustStore) - - trustManagers = trustManagerFactory.trustManagers - } - } - - if (host.keyStore) { - def keyStorePassword = host.keyStorePassword - def keyStore = KeyStore.getInstance(host.keyStoreType) - new File(host.keyStore).withInputStream { - keyStore.load(it, keyStorePassword.toCharArray()) - } - def keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()) - keyManagerFactory.init(keyStore, keyStorePassword.toCharArray()) - - keyManagers = keyManagerFactory.keyManagers - } - - if (trustManagers || keyManagers) { - def sslContext = SSLContext.getInstance("TLS") - sslContext.init(keyManagers, trustManagers, null) - - client.setSslSocketFactory(sslContext.socketFactory) - } - - new RestAdapter.Builder() - .setEndpoint(Endpoints.newFixedEndpoint(host.address)) - .setRequestInterceptor(new RequestInterceptor() { - @Override - void intercept(RequestInterceptor.RequestFacade request) { - request.addHeader("User-Agent", "Spinnaker-igor") - requestInterceptor.intercept(request) - } - }) - .setClient(new OkClient(client)) - .setConverter(new JacksonConverter(getObjectMapper())) - .build() - .create(JenkinsClient) - } - - static JenkinsClient jenkinsClient(JenkinsProperties.JenkinsHost host, int timeout = 30000) { - OkHttpClient client = new OkHttpClient() - jenkinsClient(host, client, RequestInterceptor.NONE, timeout) - } - - static class TrustAllTrustManager implements X509TrustManager { - @Override - void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { - // do nothing - } - - @Override - void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { - // do nothing - } - - @Override - X509Certificate[] getAcceptedIssuers() { - return new X509Certificate[0] - } - } - -} diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/config/JenkinsProperties.groovy b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/config/JenkinsProperties.groovy deleted file mode 100644 index 091e18823..000000000 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/config/JenkinsProperties.groovy +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2014 Netflix, 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.netflix.spinnaker.igor.config - -import com.netflix.spinnaker.fiat.model.resources.Permissions -import groovy.transform.CompileStatic -import org.hibernate.validator.constraints.NotEmpty -import org.springframework.boot.context.properties.ConfigurationProperties -import org.springframework.validation.annotation.Validated - -import javax.validation.Valid -import java.security.KeyStore - -/** - * Helper class to map masters in properties file into a validated property map - */ -@CompileStatic -@ConfigurationProperties(prefix = 'jenkins') -@Validated -class JenkinsProperties implements BuildServerProperties { - @Valid - List masters - - static class JenkinsHost implements BuildServerProperties.Host { - @NotEmpty - String name - - @NotEmpty - String address - - String username - - String password - - Boolean csrf = false - - // These are needed for Google-based OAuth with a service account credential - String jsonPath - List oauthScopes = [] - - // Can be used directly, if available. - String token - - Integer itemUpperThreshold; - - String trustStore - String trustStoreType = KeyStore.getDefaultType() - String trustStorePassword - - String keyStore - String keyStoreType = KeyStore.getDefaultType() - String keyStorePassword - - Boolean skipHostnameVerification = false - - Permissions.Builder permissions = new Permissions.Builder() - } -} diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/config/auth/AuthRequestInterceptor.groovy b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/config/auth/AuthRequestInterceptor.groovy deleted file mode 100644 index f1ca16b2a..000000000 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/config/auth/AuthRequestInterceptor.groovy +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright 2017 Google, 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.netflix.spinnaker.igor.config.auth - -import com.google.auth.oauth2.GoogleCredentials -import com.netflix.spinnaker.igor.config.JenkinsProperties -import com.squareup.okhttp.Credentials -import groovy.util.logging.Slf4j -import retrofit.RequestInterceptor - -@Slf4j -class AuthRequestInterceptor implements RequestInterceptor { - List suppliers = [] - - AuthRequestInterceptor(JenkinsProperties.JenkinsHost host) { - // Order may be significant here. - if (host.username && host.password) { - suppliers.add(new BasicAuthHeaderSupplier(host.username, host.password)) - } - if (host.jsonPath && host.oauthScopes) { - suppliers.add(new GoogleBearerTokenHeaderSupplier(host.jsonPath, host.oauthScopes)) - } else if (host.token) { - suppliers.add(new BearerTokenHeaderSupplier(token: host.token)) - } - } - - @Override - void intercept(RequestInterceptor.RequestFacade request) { - if (suppliers) { - def values = suppliers.join(", ") - request.addHeader("Authorization", values) - } - } - - static interface AuthorizationHeaderSupplier { - /** - * Returns the value to be added as the value in the "Authorization" HTTP header. - * @return - */ - String toString() - } - - static class BasicAuthHeaderSupplier implements AuthorizationHeaderSupplier { - - private final String username - private final String password - - BasicAuthHeaderSupplier(String username, String password) { - this.username = username - this.password = password - } - - String toString() { - return Credentials.basic(username, password) - } - } - - static class GoogleBearerTokenHeaderSupplier implements AuthorizationHeaderSupplier { - - private GoogleCredentials credentials - - GoogleBearerTokenHeaderSupplier(String jsonPath, List scopes) { - InputStream is = new File(jsonPath).newInputStream() - credentials = GoogleCredentials.fromStream(is).createScoped(scopes) - } - - String toString() { - log.debug("Including Google Bearer token in Authorization header") - credentials.refreshIfExpired() - return credentials.accessToken.tokenValue - } - } - - static class BearerTokenHeaderSupplier implements AuthorizationHeaderSupplier { - private token - - String toString() { - log.debug("Including raw bearer token in Authorization header") - return "Bearer ${token}".toString() - } - } -} diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/gitlabci/GitlabCiBuildContent.java b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/gitlabci/GitlabCiBuildContent.java new file mode 100644 index 000000000..582b86260 --- /dev/null +++ b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/gitlabci/GitlabCiBuildContent.java @@ -0,0 +1,34 @@ +/* + * Copyright 2020 Netflix, 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.netflix.spinnaker.igor.gitlabci; + +import com.netflix.spinnaker.igor.build.model.GenericProject; +import com.netflix.spinnaker.igor.history.model.BuildContent; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class GitlabCiBuildContent implements BuildContent { + private static final String TYPE = "gitlab-ci"; + private String master; + private GenericProject project; + + @Override + public String getType() { + return TYPE; + } +} diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/gitlabci/GitlabCiBuildEvent.java b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/gitlabci/GitlabCiBuildEvent.java new file mode 100644 index 000000000..40ebb436c --- /dev/null +++ b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/gitlabci/GitlabCiBuildEvent.java @@ -0,0 +1,28 @@ +/* + * Copyright 2020 Netflix, 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.netflix.spinnaker.igor.gitlabci; + +import com.netflix.spinnaker.igor.history.model.BuildEvent; +import lombok.Data; + +@Data +public class GitlabCiBuildEvent implements BuildEvent { + private GitlabCiBuildContent content; + + public GitlabCiBuildEvent(GitlabCiBuildContent content) { + this.content = content; + } +} diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/gitlabci/GitlabCiBuildMonitor.java b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/gitlabci/GitlabCiBuildMonitor.java index 8b6530519..a726c6129 100644 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/gitlabci/GitlabCiBuildMonitor.java +++ b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/gitlabci/GitlabCiBuildMonitor.java @@ -29,14 +29,12 @@ import com.netflix.spinnaker.igor.gitlabci.service.GitlabCiResultConverter; import com.netflix.spinnaker.igor.gitlabci.service.GitlabCiService; import com.netflix.spinnaker.igor.history.EchoService; -import com.netflix.spinnaker.igor.history.model.GenericBuildContent; -import com.netflix.spinnaker.igor.history.model.GenericBuildEvent; -import com.netflix.spinnaker.igor.model.BuildServiceProvider; import com.netflix.spinnaker.igor.polling.CommonPollingMonitor; import com.netflix.spinnaker.igor.polling.DeltaItem; import com.netflix.spinnaker.igor.polling.LockService; import com.netflix.spinnaker.igor.polling.PollContext; import com.netflix.spinnaker.igor.polling.PollingDelta; +import com.netflix.spinnaker.igor.service.BuildServiceProvider; import com.netflix.spinnaker.igor.service.BuildServices; import com.netflix.spinnaker.security.AuthenticatedRequest; import java.util.ArrayList; @@ -227,14 +225,10 @@ private void sendEvent( slug, GitlabCiPipelineUtis.genericBuild(pipeline, project.getPathWithNamespace(), address)); - GenericBuildContent content = new GenericBuildContent(); - content.setMaster(master); - content.setType("gitlab-ci"); - content.setProject(genericProject); + GitlabCiBuildContent content = new GitlabCiBuildContent(master, genericProject); - GenericBuildEvent event = new GenericBuildEvent(); - event.setContent(content); - AuthenticatedRequest.allowAnonymous(() -> echoService.get().postEvent(event)); + AuthenticatedRequest.allowAnonymous( + () -> echoService.get().postEvent(new GitlabCiBuildEvent(content))); } @Override diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/gitlabci/service/GitlabCiPipelineUtis.java b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/gitlabci/service/GitlabCiPipelineUtis.java index 634654250..b17a05655 100644 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/gitlabci/service/GitlabCiPipelineUtis.java +++ b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/gitlabci/service/GitlabCiPipelineUtis.java @@ -27,23 +27,23 @@ public static String getBranchedPipelineSlug(final Project project, final Pipeli } public static GenericBuild genericBuild(Pipeline pipeline, String repoSlug, String baseUrl) { - GenericBuild genericBuild = new GenericBuild(); - genericBuild.setBuilding(GitlabCiResultConverter.running(pipeline.getStatus())); - genericBuild.setNumber(pipeline.getId()); - genericBuild.setDuration(pipeline.getDuration()); - genericBuild.setResult( - GitlabCiResultConverter.getResultFromGitlabCiState(pipeline.getStatus())); - genericBuild.setName(repoSlug); - genericBuild.setUrl(url(repoSlug, baseUrl, pipeline.getId())); + GenericBuild.GenericBuildBuilder builder = + GenericBuild.builder() + .building(GitlabCiResultConverter.running(pipeline.getStatus())) + .number(pipeline.getId()) + .duration(pipeline.getDuration()) + .result(GitlabCiResultConverter.getResultFromGitlabCiState(pipeline.getStatus())) + .name(repoSlug) + .url(url(repoSlug, baseUrl, pipeline.getId())); if (pipeline.getFinishedAt() != null) { - genericBuild.setTimestamp(Long.toString(pipeline.getFinishedAt().getTime())); + builder.timestamp(Long.toString(pipeline.getFinishedAt().getTime())); } - return genericBuild; + return builder.build(); } private static String url(final String repoSlug, final String baseUrl, final int id) { - return baseUrl + "/" + repoSlug + "/pipelines/" + String.valueOf(id); + return baseUrl + "/" + repoSlug + "/pipelines/" + id; } } diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/gitlabci/service/GitlabCiService.java b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/gitlabci/service/GitlabCiService.java index e2c1a32c3..a9bf2338a 100644 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/gitlabci/service/GitlabCiService.java +++ b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/gitlabci/service/GitlabCiService.java @@ -23,8 +23,8 @@ import com.netflix.spinnaker.igor.gitlabci.client.model.Pipeline; import com.netflix.spinnaker.igor.gitlabci.client.model.PipelineSummary; import com.netflix.spinnaker.igor.gitlabci.client.model.Project; -import com.netflix.spinnaker.igor.model.BuildServiceProvider; import com.netflix.spinnaker.igor.service.BuildOperations; +import com.netflix.spinnaker.igor.service.BuildServiceProvider; import java.util.ArrayList; import java.util.List; import java.util.Map; diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/JenkinsBuildMonitor.groovy b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/JenkinsBuildMonitor.groovy deleted file mode 100644 index efcf5f1d0..000000000 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/JenkinsBuildMonitor.groovy +++ /dev/null @@ -1,246 +0,0 @@ -/* - * Copyright 2014 Netflix, 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.netflix.spinnaker.igor.jenkins - -import com.netflix.discovery.DiscoveryClient -import com.netflix.spectator.api.BasicTag -import com.netflix.spectator.api.Registry -import com.netflix.spinnaker.igor.IgorConfigurationProperties -import com.netflix.spinnaker.igor.config.JenkinsProperties -import com.netflix.spinnaker.igor.history.EchoService -import com.netflix.spinnaker.igor.history.model.BuildContent -import com.netflix.spinnaker.igor.history.model.BuildEvent -import com.netflix.spinnaker.igor.jenkins.client.model.Build -import com.netflix.spinnaker.igor.jenkins.client.model.Project -import com.netflix.spinnaker.igor.jenkins.service.JenkinsService -import com.netflix.spinnaker.igor.model.BuildServiceProvider -import com.netflix.spinnaker.igor.polling.CommonPollingMonitor -import com.netflix.spinnaker.igor.polling.DeltaItem -import com.netflix.spinnaker.igor.polling.LockService -import com.netflix.spinnaker.igor.polling.PollContext -import com.netflix.spinnaker.igor.polling.PollingDelta -import com.netflix.spinnaker.igor.service.BuildServices -import com.netflix.spinnaker.security.AuthenticatedRequest -import groovy.time.TimeCategory -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.beans.factory.annotation.Value -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty -import org.springframework.stereotype.Service -import retrofit.RetrofitError -import java.util.stream.Collectors - -import static net.logstash.logback.argument.StructuredArguments.kv -/** - * Monitors new jenkins builds - */ -@Service -@SuppressWarnings('CatchException') -@ConditionalOnProperty('jenkins.enabled') -class JenkinsBuildMonitor extends CommonPollingMonitor { - - private final JenkinsCache cache - private final BuildServices buildServices - private final boolean pollingEnabled - private final Optional echoService - private final JenkinsProperties jenkinsProperties - - @Autowired - JenkinsBuildMonitor(IgorConfigurationProperties properties, - Registry registry, - Optional discoveryClient, - Optional lockService, - JenkinsCache cache, - BuildServices buildServices, - @Value('${jenkins.polling.enabled:true}') boolean pollingEnabled, - Optional echoService, - JenkinsProperties jenkinsProperties) { - super(properties, registry, discoveryClient, lockService) - this.cache = cache - this.buildServices = buildServices - this.pollingEnabled = pollingEnabled - this.echoService = echoService - this.jenkinsProperties = jenkinsProperties - } - - @Override - String getName() { - "jenkinsBuildMonitor" - } - - @Override - boolean isInService() { - pollingEnabled && super.isInService() - } - - @Override - void poll(boolean sendEvents) { - buildServices.getServiceNames(BuildServiceProvider.JENKINS).stream().forEach( - { master -> pollSingle(new PollContext(master, !sendEvents)) } - ) - } - - /** - * Gets a list of jobs for this master & processes builds between last poll stamp and a sliding upper bound stamp, - * the cursor will be used to advanced to the upper bound when all builds are completed in the commit phase. - */ - @Override - protected JobPollingDelta generateDelta(PollContext ctx) { - String master = ctx.partitionName - log.trace("Checking for new builds for $master") - - final List delta = [] - registry.timer("pollingMonitor.jenkins.retrieveProjects", [new BasicTag("partition", master)]).record { - JenkinsService jenkinsService = buildServices.getService(master) as JenkinsService - List jobs = jenkinsService.getProjects()?.getList() ?:[] - jobs.forEach( { job -> processBuildsOfProject(jenkinsService, master, job, delta)}) - } - return new JobPollingDelta(master: master, items: delta) - } - - private void processBuildsOfProject(JenkinsService jenkinsService, String master, Project job, List delta) { - if (!job.lastBuild) { - log.trace("[{}:{}] has no builds skipping...", kv("master", master), kv("job", job.name)) - return - } - - try { - Long cursor = cache.getLastPollCycleTimestamp(master, job.name) - Long lastBuildStamp = job.lastBuild.timestamp as Long - Date upperBound = new Date(lastBuildStamp) - if (cursor == lastBuildStamp) { - log.trace("[${master}:${job.name}] is up to date. skipping") - return - } - - if (!cursor && !igorProperties.spinnaker.build.handleFirstBuilds) { - cache.setLastPollCycleTimestamp(master, job.name, lastBuildStamp) - return - } - - List allBuilds = getBuilds(jenkinsService, master, job, cursor, lastBuildStamp) - List currentlyBuilding = allBuilds.findAll { it.building } - List completedBuilds = allBuilds.findAll { !it.building } - cursor = !cursor ? lastBuildStamp : cursor - Date lowerBound = new Date(cursor) - - if (!igorProperties.spinnaker.build.processBuildsOlderThanLookBackWindow) { - completedBuilds = onlyInLookBackWindow(completedBuilds) - } - - delta.add(new JobDelta( - cursor: cursor, - name: job.name, - lastBuildStamp: lastBuildStamp, - upperBound: upperBound, - lowerBound: lowerBound, - completedBuilds: completedBuilds, - runningBuilds: currentlyBuilding - )) - - } catch (e) { - log.error("Error processing builds for [{}:{}]", kv("master", master), kv("job", job.name), e) - if (e.cause instanceof RetrofitError) { - def re = (RetrofitError) e.cause - log.error("Error communicating with jenkins for [{}:{}]: {}", kv("master", master), kv("job", job.name), kv("url", re.url), re) - } - } - } - - private List getBuilds(JenkinsService jenkinsService, String master, Project job, Long cursor, Long lastBuildStamp) { - if (!cursor) { - log.debug("[${master}:${job.name}] setting new cursor to ${lastBuildStamp}") - return jenkinsService.getBuilds(job.name) ?: [] - } - - // filter between last poll and jenkins last build included - return (jenkinsService.getBuilds(job.name) ?: []).findAll { build -> - Long buildStamp = build.timestamp as Long - return buildStamp <= lastBuildStamp && buildStamp > cursor - } - } - - private List onlyInLookBackWindow(List builds) { - use(TimeCategory) { - def offsetSeconds = pollInterval.seconds - def lookBackWindowMins = igorProperties.spinnaker.build.lookBackWindowMins.minutes - Date lookBackDate = (offsetSeconds + lookBackWindowMins).ago - - return builds.stream().filter({ - Date buildEndDate = new Date((it.timestamp as Long) + it.duration) - return buildEndDate.after(lookBackDate) - }).collect(Collectors.toList()) - } - } - - @Override - protected void commitDelta(JobPollingDelta delta, boolean sendEvents) { - String master = delta.master - - delta.items.stream().forEach { job -> - // post events for finished builds - job.completedBuilds.forEach { build -> - Boolean eventPosted = cache.getEventPosted(master, job.name, job.cursor, build.number) - if (!eventPosted) { - if (sendEvents) { - postEvent(new Project(name: job.name, lastBuild: build), master) - log.debug("[${master}:${job.name}]:${build.number} event posted") - cache.setEventPosted(master, job.name, job.cursor, build.number) - } - } - } - - // advance cursor when all builds have completed in the interval - if (job.runningBuilds.isEmpty()) { - log.info("[{}:{}] has no other builds between [${job.lowerBound} - ${job.upperBound}], " + - "advancing cursor to ${job.lastBuildStamp}", kv("master", master), kv("job", job.name)) - cache.pruneOldMarkers(master, job.name, job.cursor) - cache.setLastPollCycleTimestamp(master, job.name, job.lastBuildStamp) - } - } - } - - @Override - protected Integer getPartitionUpperThreshold(String partition) { - return jenkinsProperties.masters.find { partition == it.name }?.itemUpperThreshold - } - - private void postEvent(Project project, String master) { - if (!echoService.isPresent()) { - log.warn("Cannot send build notification: Echo is not configured") - registry.counter(missedNotificationId.withTag("monitor", getClass().simpleName)).increment() - return - } - AuthenticatedRequest.allowAnonymous { - echoService.get().postEvent(new BuildEvent(content: new BuildContent(project: project, master: master))) - } - } - - private static class JobPollingDelta implements PollingDelta { - String master - List items - } - - private static class JobDelta implements DeltaItem { - Long cursor - String name - Long lastBuildStamp - Date lowerBound - Date upperBound - List completedBuilds - List runningBuilds - } -} diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/JenkinsClient.groovy b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/JenkinsClient.groovy deleted file mode 100644 index a474f7f02..000000000 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/JenkinsClient.groovy +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2014 Netflix, 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.netflix.spinnaker.igor.jenkins.client - -import com.netflix.spinnaker.igor.jenkins.client.model.* -import com.netflix.spinnaker.igor.jenkins.client.model.BuildsList -import com.netflix.spinnaker.igor.jenkins.client.model.JobConfig -import com.netflix.spinnaker.igor.jenkins.client.model.JobList -import com.netflix.spinnaker.igor.jenkins.client.model.ScmDetails -import com.netflix.spinnaker.igor.model.Crumb -import retrofit.client.Response -import retrofit.http.* - -/** - * Interface for interacting with a Jenkins Service via Xml - */ -@SuppressWarnings('LineLength') -interface JenkinsClient { - /* - * Jobs created with the Jenkins Folders plugin are nested. - * Some queries look for jobs within folders with a depth of 10. - */ - - @GET('/api/xml?tree=jobs[name,lastBuild[actions[failCount,skipCount,totalCount,urlName],duration,number,timestamp,result,building,url],jobs[name,lastBuild[actions[failCount,skipCount,totalCount,urlName],duration,number,timestamp,result,building,url],jobs[name,lastBuild[actions[failCount,skipCount,totalCount,urlName],duration,number,timestamp,result,building,url],jobs[name,lastBuild[actions[failCount,skipCount,totalCount,urlName],duration,number,timestamp,result,building,url],jobs[name,lastBuild[actions[failCount,skipCount,totalCount,urlName],duration,number,timestamp,result,building,url],jobs[name,lastBuild[actions[failCount,skipCount,totalCount,urlName],duration,number,timestamp,result,building,url],jobs[name,lastBuild[actions[failCount,skipCount,totalCount,urlName],duration,number,timestamp,result,building,url],jobs[name,lastBuild[actions[failCount,skipCount,totalCount,urlName],duration,number,timestamp,result,building,url],jobs[name,lastBuild[actions[failCount,skipCount,totalCount,urlName],duration,number,timestamp,result,building,url],jobs[name,lastBuild[actions[failCount,skipCount,totalCount,urlName],duration,number,timestamp,result,building,url]]]]]]]]]]]&exclude=/*/*/*/action[not(totalCount)]') - ProjectsList getProjects() - - @GET('/api/xml?tree=jobs[name,jobs[name,jobs[name,jobs[name,jobs[name,jobs[name,jobs[name,jobs[name,jobs[name,jobs[name]]]]]]]]]]') - JobList getJobs() - - @GET('/job/{jobName}/api/xml?exclude=/*/build/action[not(totalCount)]&tree=builds[number,url,duration,timestamp,result,building,url,fullDisplayName,actions[failCount,skipCount,totalCount]]') - BuildsList getBuilds(@EncodedPath('jobName') String jobName) - - @GET('/job/{jobName}/api/xml?tree=name,url,actions[processes[name]],downstreamProjects[name,url],upstreamProjects[name,url]') - BuildDependencies getDependencies(@EncodedPath('jobName') String jobName) - - @GET('/job/{jobName}/{buildNumber}/api/xml?exclude=/*/action[not(totalCount)]&tree=actions[failCount,skipCount,totalCount,urlName],duration,number,timestamp,result,building,url,fullDisplayName,artifacts[displayPath,fileName,relativePath]') - Build getBuild(@EncodedPath('jobName') String jobName, @Path('buildNumber') Integer buildNumber) - - // The location of the SCM details in the build xml changed in version 4.0.0 of the jenkins-git plugin; see the - // header comment in com.netflix.spinnaker.igor.jenkins.client.model.ScmDetails for more information. - // The exclude and tree parameters to this call must continue to support both formats to remain compatible with - // all versions of the plugin. - @GET('/job/{jobName}/{buildNumber}/api/xml?exclude=/*/action[not(build|lastBuiltRevision)]&tree=actions[remoteUrls,lastBuiltRevision[branch[name,SHA1]],build[revision[branch[name,SHA1]]]]') - ScmDetails getGitDetails(@EncodedPath('jobName') String jobName, @Path('buildNumber') Integer buildNumber) - - @GET('/job/{jobName}/lastCompletedBuild/api/xml') - Build getLatestBuild(@EncodedPath('jobName') String jobName) - - @GET('/queue/item/{itemNumber}/api/xml') - QueuedJob getQueuedItem(@Path('itemNumber') Integer item) - - @POST('/job/{jobName}/build') - Response build(@EncodedPath('jobName') String jobName, @Body String emptyRequest, @Header("Jenkins-Crumb") String crumb) - - @POST('/job/{jobName}/buildWithParameters') - Response buildWithParameters(@EncodedPath('jobName') String jobName, @QueryMap Map queryParams, @Body String EmptyRequest, @Header("Jenkins-Crumb") String crumb) - - @POST('/job/{jobName}/{buildNumber}/stop') - Response stopRunningBuild(@EncodedPath('jobName') String jobName, @Path('buildNumber') Integer buildNumber, @Body String EmptyRequest, @Header("Jenkins-Crumb") String crumb) - - @POST('/queue/cancelItem') - Response stopQueuedBuild(@Query('id') String queuedBuild, @Body String emptyRequest, @Header("Jenkins-Crumb") String crumb) - - @GET('/job/{jobName}/api/xml?exclude=/*/action&exclude=/*/build&exclude=/*/property[not(parameterDefinition)]') - JobConfig getJobConfig(@EncodedPath('jobName') String jobName) - - @Streaming - @GET('/job/{jobName}/{buildNumber}/artifact/{fileName}') - Response getPropertyFile( - @EncodedPath('jobName') String jobName, - @Path('buildNumber') Integer buildNumber, @Path(value = 'fileName', encode = false) String fileName) - - @GET('/crumbIssuer/api/xml') - Crumb getCrumb() -} diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/JenkinsMasters.groovy b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/JenkinsMasters.groovy deleted file mode 100644 index e2056bce9..000000000 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/JenkinsMasters.groovy +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2014 Netflix, 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.netflix.spinnaker.igor.jenkins.client - -import com.netflix.spinnaker.igor.jenkins.service.JenkinsService - -/** - * Wrapper class for a collection of jenkins clients - */ -class JenkinsMasters { - - Map map - -} diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/Build.groovy b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/Build.groovy deleted file mode 100644 index d1f25385a..000000000 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/Build.groovy +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2014 Netflix, 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.netflix.spinnaker.igor.jenkins.client.model - -import com.fasterxml.jackson.annotation.JsonInclude -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper -import com.netflix.spinnaker.igor.build.model.GenericArtifact -import com.netflix.spinnaker.igor.build.model.GenericBuild -import com.netflix.spinnaker.igor.build.model.Result -import groovy.transform.CompileStatic - -import javax.xml.bind.annotation.XmlElement -import javax.xml.bind.annotation.XmlRootElement - -/** - * Represents a build in Jenkins - */ -@CompileStatic -@JsonInclude(JsonInclude.Include.NON_NULL) -@XmlRootElement -class Build { - boolean building - Integer number - @XmlElement(required = false) - String result - String timestamp - @XmlElement(required = false) - Long duration - @XmlElement(required = false) - Integer estimatedDuration - @XmlElement(required = false) - String id - String url - @XmlElement(required = false) - String builtOn - @XmlElement(required = false) - String fullDisplayName - - @JacksonXmlElementWrapper(useWrapping = false) - @XmlElement(name = "artifact", required = false) - List artifacts - - /* - We need to dump this into a list first since the Jenkins query returns - multiple action elements, with all but the test run one empty. We then filter it into a testResults var - */ - @JacksonXmlElementWrapper(useWrapping = false) - @XmlElement(name = "action", required = false) - List testResults - - GenericBuild genericBuild(String jobName) { - GenericBuild genericBuild = new GenericBuild(building: building, number: number.intValue(), duration: duration.intValue(), result: result as Result, name: jobName, url: url, timestamp: timestamp, fullDisplayName: fullDisplayName) - if (artifacts) { - genericBuild.artifacts = artifacts.collect { buildArtifact -> - GenericArtifact artifact = buildArtifact.getGenericArtifact() - artifact.name = jobName - artifact.version = number - artifact - } - } - if (testResults) { - genericBuild.testResults = testResults - } - return genericBuild - } -} diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/BuildArtifact.groovy b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/BuildArtifact.groovy deleted file mode 100644 index 7f1334f58..000000000 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/BuildArtifact.groovy +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2014 Netflix, 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.netflix.spinnaker.igor.jenkins.client.model - -import com.netflix.spinnaker.igor.build.model.GenericArtifact -import groovy.transform.CompileStatic -import org.simpleframework.xml.Default -import org.simpleframework.xml.Element -import org.simpleframework.xml.Root - -import javax.xml.bind.annotation.XmlElement -import javax.xml.bind.annotation.XmlRootElement - -/** - * Represents a build artifact - */ -@CompileStatic -@XmlRootElement(name = 'artifact') -class BuildArtifact { - @XmlElement(required = false) - String fileName - - @XmlElement(required = false) - String displayPath - - @XmlElement(required = false) - String relativePath - - GenericArtifact getGenericArtifact() { - GenericArtifact artifact = new GenericArtifact(fileName, displayPath, relativePath) - artifact.type = 'jenkins/file' - artifact.reference = relativePath - return artifact - } -} diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/BuildDependencies.groovy b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/BuildDependencies.groovy deleted file mode 100644 index ce6ff0701..000000000 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/BuildDependencies.groovy +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2014 Netflix, 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.netflix.spinnaker.igor.jenkins.client.model - -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper -import groovy.transform.CompileStatic - -import javax.xml.bind.annotation.XmlElement -import javax.xml.bind.annotation.XmlRootElement - -/** - * Captures build dependencies for a jenkins job - */ -@XmlRootElement -@CompileStatic -class BuildDependencies { - @JacksonXmlElementWrapper(useWrapping = false) - @XmlElement(name = "downstreamProject", required = false) - List downstreamProjects - - @JacksonXmlElementWrapper(useWrapping = false) - @XmlElement(name = "upstreamProject", required = false) - List upstreamProjects -} diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/BuildDependency.groovy b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/BuildDependency.groovy deleted file mode 100644 index 181267181..000000000 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/BuildDependency.groovy +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2014 Netflix, 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.netflix.spinnaker.igor.jenkins.client.model - -import groovy.transform.CompileStatic -import org.simpleframework.xml.Default - -/** - * Represents either an upstream or downstream dependency in Jenkins - */ -@Default -@CompileStatic -class BuildDependency { - String name - String url -} diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/DownstreamProject.groovy b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/DownstreamProject.groovy deleted file mode 100644 index 3591288cd..000000000 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/DownstreamProject.groovy +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2014 Netflix, 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.netflix.spinnaker.igor.jenkins.client.model - -import org.simpleframework.xml.Root - -/** - * Represents a Jenkins job downstream project - */ -@Root(name = 'downstreamProject', strict=false) -class DownstreamProject extends RelatedProject { -} diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/JobConfig.groovy b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/JobConfig.groovy deleted file mode 100644 index b9dc18041..000000000 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/JobConfig.groovy +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2014 Netflix, 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.netflix.spinnaker.igor.jenkins.client.model - -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper -import com.netflix.spinnaker.igor.build.model.JobConfiguration - -import javax.xml.bind.annotation.XmlElement -import javax.xml.bind.annotation.XmlElementWrapper -import javax.xml.bind.annotation.XmlRootElement - -/** - * Represents the basic Jenkins job configuration information - */ -@XmlRootElement -class JobConfig implements JobConfiguration { - @XmlElement(required = false) - String description - - @XmlElement - String displayName - - @XmlElement - String name - - @XmlElement - boolean buildable - - @XmlElement - String color - - @XmlElement - String url - - @XmlElementWrapper(name = "property") - @XmlElement(name = "parameterDefinition", required = false) - List parameterDefinitionList - - @JacksonXmlElementWrapper(useWrapping = false) - @XmlElement(name = "upstreamProject", required = false) - List upstreamProjectList - - @JacksonXmlElementWrapper(useWrapping = false) - @XmlElement(name = "downstreamProject", required = false) - List downstreamProjectList - - @XmlElement - boolean concurrentBuild -} diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/JobList.groovy b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/JobList.groovy deleted file mode 100644 index 496522d43..000000000 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/JobList.groovy +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2014 Netflix, 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.netflix.spinnaker.igor.jenkins.client.model - -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper -import groovy.transform.CompileStatic - -import javax.xml.bind.annotation.XmlElement -import javax.xml.bind.annotation.XmlRootElement - -/** - * Represents a list of projects - */ -@XmlRootElement(name = 'hudson') -@CompileStatic -class JobList { - @JacksonXmlElementWrapper(useWrapping = false) - @XmlElement(name = "job") - List list -} - -class Job { - @JacksonXmlElementWrapper(useWrapping = false) - @XmlElement(name = "job") - List list - - @XmlElement(required = false) - String name -} diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/Project.groovy b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/Project.groovy deleted file mode 100644 index a94bb927a..000000000 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/Project.groovy +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2014 Netflix, 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.netflix.spinnaker.igor.jenkins.client.model - - -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper -import groovy.transform.CompileStatic - -import javax.xml.bind.annotation.XmlElement - -/** - * Represents a Project returned by the Jenkins service in the project list - */ -@CompileStatic -class Project { - @JacksonXmlElementWrapper(useWrapping = false) - @XmlElement(name = "job", required = false) - List list - - @XmlElement - String name - - @XmlElement - Build lastBuild -} diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/ProjectsList.groovy b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/ProjectsList.groovy deleted file mode 100644 index 298863857..000000000 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/ProjectsList.groovy +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2014 Netflix, 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.netflix.spinnaker.igor.jenkins.client.model - - -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper -import groovy.transform.CompileStatic - -import javax.xml.bind.annotation.XmlElement -import javax.xml.bind.annotation.XmlRootElement - -/** - * Represents a list of projects - */ -@XmlRootElement(name = 'hudson') -@CompileStatic -class ProjectsList { - - // not sure why we need this, it seems that there shouldn't be a element wrapping the list - // since we don't have the JAXB @XmlElementWrapper annotation, but for some reason Jackson expects one - @JacksonXmlElementWrapper(useWrapping=false) - @XmlElement(name = 'job') - List list -} diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/QueuedJob.groovy b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/QueuedJob.groovy deleted file mode 100644 index 1cce114b2..000000000 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/QueuedJob.groovy +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2015 Netflix, 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.netflix.spinnaker.igor.jenkins.client.model - - -import javax.xml.bind.annotation.XmlElement -import javax.xml.bind.annotation.XmlRootElement - -@XmlRootElement -class QueuedJob { - @XmlElement - QueuedExecutable executable - - @XmlElement(name = 'number') - Integer getNumber() { - return executable?.number - } -} - - -class QueuedExecutable { - @XmlElement - Integer number -} diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/RelatedProject.groovy b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/RelatedProject.groovy deleted file mode 100644 index 74cb8d6d2..000000000 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/RelatedProject.groovy +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2014 Netflix, 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.netflix.spinnaker.igor.jenkins.client.model - -import org.simpleframework.xml.Element -import org.simpleframework.xml.Root - -/** - * Represents a upstream/downstream project for a Jenkins job - */ -@Root(strict=false) -class RelatedProject { - @Element - String name - - @Element - String url - - @Element - String color -} diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/ScmDetails.groovy b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/ScmDetails.groovy deleted file mode 100644 index 7d6b5116d..000000000 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/ScmDetails.groovy +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2014 Netflix, 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.netflix.spinnaker.igor.jenkins.client.model - -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper -import com.netflix.spinnaker.igor.build.model.GenericGitRevision -import groovy.transform.CompileStatic - -import javax.xml.bind.annotation.XmlElement -import java.util.stream.Collectors - -/** - * Represents git details - * - * The serialization of these details in the Jenkins build XML changed in version 4.0.0 of the jenkins-git plugin. - * - * Prior to 4.0.0, the format was: - * - * - * - * 943a702d06f34599aee1f8da8ef9f7296031d699 - * refs/remotes/origin/master - * - * - * some-url - * - * - * As of version 4.0.0, the format is: - * - * - * - * - * 943a702d06f34599aee1f8da8ef9f7296031d699 - * refs/remotes/origin/master - * - * - * some-url - * - * - * - * The code in this module should remain compatible with both formats to ensure that SCM info is populated in Spinnaker - * regardless of which version of the jenkins-git plugin is being used. - */ -@CompileStatic -class ScmDetails { - @JacksonXmlElementWrapper(useWrapping = false) - @XmlElement(name = "action") - ArrayList actions - - List genericGitRevisions() { - List genericGitRevisions = new ArrayList() - - if (actions == null) { - return null - } - - for (Action action : actions) { - Revision revision = action?.lastBuiltRevision ?: action?.build?.revision - if (revision?.branch?.name) { - genericGitRevisions.addAll(revision.branch.collect() { Branch branch -> - GenericGitRevision.builder() - .name(branch.getName()) - .branch(branch.name.split('/').last()) - .sha1(branch.sha1) - .remoteUrl(action.remoteUrl) - .build() - }) - } - } - - // If the same revision appears in both the old and the new location in the XML, we only want to return it once - return genericGitRevisions.stream().distinct().collect(Collectors.toList()) - } -} - -class Action { - @XmlElement(required = false) - Revision lastBuiltRevision - - @XmlElement(required = false) - ScmBuild build - - @XmlElement(required = false) - String remoteUrl -} - -class ScmBuild { - @XmlElement(required = false) - Revision revision -} - -class Revision { - @JacksonXmlElementWrapper(useWrapping = false) - @XmlElement(name = "branch") - List branch -} - -class Branch { - @XmlElement(required = false) - String name - - @XmlElement(required = false, name = "SHA1") - String sha1 -} diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/TestResults.groovy b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/TestResults.groovy deleted file mode 100644 index 65146f1fe..000000000 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/jenkins/client/model/TestResults.groovy +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2014 Netflix, 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.netflix.spinnaker.igor.jenkins.client.model - -import groovy.transform.CompileStatic -import org.simpleframework.xml.Element -import org.simpleframework.xml.Root - -/** - * Represents a build artifact - */ -@CompileStatic -@Root(name = 'action', strict = false) -class TestResults { - @Element(required = false) - int failCount - @Element(required = false) - int skipCount - @Element(required = false) - int totalCount - @Element(required = false) - String urlName -} diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/model/Crumb.groovy b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/model/Crumb.groovy deleted file mode 100644 index f874b837d..000000000 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/model/Crumb.groovy +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2018 Google, 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.netflix.spinnaker.igor.model - - -import javax.xml.bind.annotation.XmlElement -import javax.xml.bind.annotation.XmlRootElement - -/** - * Represents a Jenkins CSRF Crumb. - */ -@XmlRootElement -class Crumb { - @XmlElement - String crumbRequestField - - @XmlElement - String crumb -} diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/wercker/WerckerBuildContent.java b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/wercker/WerckerBuildContent.java new file mode 100644 index 000000000..892ce0263 --- /dev/null +++ b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/wercker/WerckerBuildContent.java @@ -0,0 +1,28 @@ +/* + * Copyright 2020 Netflix, 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.netflix.spinnaker.igor.wercker; + +import com.netflix.spinnaker.igor.build.model.GenericProject; +import com.netflix.spinnaker.igor.history.model.BuildContent; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class WerckerBuildContent implements BuildContent { + private String master; + private GenericProject project; +} diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/wercker/WerckerBuildEvent.java b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/wercker/WerckerBuildEvent.java new file mode 100644 index 000000000..9ddd3ecb3 --- /dev/null +++ b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/wercker/WerckerBuildEvent.java @@ -0,0 +1,26 @@ +/* + * Copyright 2020 Netflix, 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.netflix.spinnaker.igor.wercker; + +import com.netflix.spinnaker.igor.history.model.BuildEvent; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class WerckerBuildEvent implements BuildEvent { + private WerckerBuildContent content; +} diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/wercker/WerckerBuildMonitor.groovy b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/wercker/WerckerBuildMonitor.groovy index b09c2458b..be8f3b518 100644 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/wercker/WerckerBuildMonitor.groovy +++ b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/wercker/WerckerBuildMonitor.groovy @@ -22,9 +22,9 @@ import com.netflix.spinnaker.igor.build.model.GenericProject import com.netflix.spinnaker.igor.build.model.Result import com.netflix.spinnaker.igor.config.WerckerProperties import com.netflix.spinnaker.igor.history.EchoService -import com.netflix.spinnaker.igor.history.model.GenericBuildContent +import com.netflix.spinnaker.igor.history.model.EmptyBuildContent import com.netflix.spinnaker.igor.history.model.GenericBuildEvent -import com.netflix.spinnaker.igor.model.BuildServiceProvider +import com.netflix.spinnaker.igor.service.BuildServiceProvider import com.netflix.spinnaker.igor.polling.CommonPollingMonitor import com.netflix.spinnaker.igor.polling.DeltaItem import com.netflix.spinnaker.igor.polling.LockService @@ -212,15 +212,15 @@ class WerckerBuildMonitor extends CommonPollingMonitor { diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/wercker/WerckerService.groovy b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/wercker/WerckerService.groovy index 9266a397f..65e36048a 100644 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/wercker/WerckerService.groovy +++ b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/wercker/WerckerService.groovy @@ -10,7 +10,6 @@ package com.netflix.spinnaker.igor.wercker import com.netflix.spinnaker.fiat.model.resources.Permissions import com.netflix.spinnaker.hystrix.SimpleHystrixCommand -import com.netflix.spinnaker.igor.build.BuildController import com.netflix.spinnaker.igor.build.model.GenericBuild import com.netflix.spinnaker.igor.build.model.GenericGitRevision import com.netflix.spinnaker.igor.build.model.GenericJobConfiguration @@ -18,23 +17,25 @@ import com.netflix.spinnaker.igor.build.model.JobConfiguration import com.netflix.spinnaker.igor.build.model.Result import com.netflix.spinnaker.igor.config.WerckerProperties.WerckerHost import com.netflix.spinnaker.igor.exceptions.BuildJobError -import com.netflix.spinnaker.igor.model.BuildServiceProvider +import com.netflix.spinnaker.igor.service.BuildServiceProvider import com.netflix.spinnaker.igor.service.BuildOperations +import com.netflix.spinnaker.igor.service.JobNamesProvider import com.netflix.spinnaker.igor.wercker.model.Application import com.netflix.spinnaker.igor.wercker.model.Pipeline import com.netflix.spinnaker.igor.wercker.model.QualifiedPipelineName import com.netflix.spinnaker.igor.wercker.model.Run import com.netflix.spinnaker.igor.wercker.model.RunPayload +import com.netflix.spinnaker.kork.web.exceptions.InvalidRequestException import groovy.util.logging.Slf4j import retrofit.RetrofitError import retrofit.client.Response import retrofit.mime.TypedByteArray -import static com.netflix.spinnaker.igor.model.BuildServiceProvider.WERCKER +import static com.netflix.spinnaker.igor.service.BuildServiceProvider.WERCKER import static net.logstash.logback.argument.StructuredArguments.kv @Slf4j -class WerckerService implements BuildOperations { +class WerckerService implements BuildOperations, JobNamesProvider { String groupKey WerckerClient werckerClient @@ -96,7 +97,7 @@ class WerckerService implements BuildOperations { Run run = getRunById(runId) String addr = address.endsWith("/") ? address.substring(0, address.length()-1) : address - GenericBuild genericBuild = new GenericBuild() + GenericBuild genericBuild = GenericBuild.builder().build() genericBuild.name = job genericBuild.building = true genericBuild.fullDisplayName = "Wercker Job " + job + " [" + buildNumber + "]" @@ -185,7 +186,7 @@ class WerckerService implements BuildOperations { "Failed to trigger build for pipeline ${pipelineName}! Error from Wercker is: ${wkrMsg}") } } else { - throw new BuildController.InvalidJobParameterException( + throw new InvalidJobParameterException( "Could not retrieve pipeline ${pipelineName} for application ${appName} from Wercker!") } } @@ -354,4 +355,15 @@ class WerckerService implements BuildOperations { JobConfiguration getJobConfig(String jobName) { return new GenericJobConfiguration('WerckerPipeline ' + jobName, jobName) } + + @Override + List getJobNames() { + return getJobs(); + } + + class InvalidJobParameterException extends InvalidRequestException { + InvalidJobParameterException(String message) { + super(message) + } + } } diff --git a/igor-web/src/main/java/com/netflix/spinnaker/igor/concourse/ConcourseBuildContent.java b/igor-web/src/main/java/com/netflix/spinnaker/igor/concourse/ConcourseBuildContent.java new file mode 100644 index 000000000..0b465610b --- /dev/null +++ b/igor-web/src/main/java/com/netflix/spinnaker/igor/concourse/ConcourseBuildContent.java @@ -0,0 +1,40 @@ +/* + * Copyright 2020 Netflix, 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.netflix.spinnaker.igor.concourse; + +import com.netflix.spinnaker.igor.build.model.GenericProject; +import com.netflix.spinnaker.igor.history.model.BuildContent; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class ConcourseBuildContent implements BuildContent { + + public static final String TYPE = "concourse"; + + private GenericProject project; + private String master; + + public void setMaster(String master) { + this.master = "concourse-" + master; + } + + @Override + public String getType() { + return TYPE; + } +} diff --git a/igor-web/src/main/java/com/netflix/spinnaker/igor/concourse/ConcourseBuildEvent.java b/igor-web/src/main/java/com/netflix/spinnaker/igor/concourse/ConcourseBuildEvent.java new file mode 100644 index 000000000..b9febff53 --- /dev/null +++ b/igor-web/src/main/java/com/netflix/spinnaker/igor/concourse/ConcourseBuildEvent.java @@ -0,0 +1,28 @@ +/* + * Copyright 2020 Netflix, 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.netflix.spinnaker.igor.concourse; + +import com.netflix.spinnaker.igor.history.model.BuildEvent; +import lombok.Data; + +@Data +public class ConcourseBuildEvent implements BuildEvent { + private ConcourseBuildContent content; + + public ConcourseBuildEvent(ConcourseBuildContent content) { + this.content = content; + } +} diff --git a/igor-web/src/main/java/com/netflix/spinnaker/igor/concourse/ConcourseBuildMonitor.java b/igor-web/src/main/java/com/netflix/spinnaker/igor/concourse/ConcourseBuildMonitor.java index c8f1a9ebf..70f430fcf 100644 --- a/igor-web/src/main/java/com/netflix/spinnaker/igor/concourse/ConcourseBuildMonitor.java +++ b/igor-web/src/main/java/com/netflix/spinnaker/igor/concourse/ConcourseBuildMonitor.java @@ -26,8 +26,6 @@ import com.netflix.spinnaker.igor.concourse.service.ConcourseService; import com.netflix.spinnaker.igor.config.ConcourseProperties; import com.netflix.spinnaker.igor.history.EchoService; -import com.netflix.spinnaker.igor.history.model.GenericBuildContent; -import com.netflix.spinnaker.igor.history.model.GenericBuildEvent; import com.netflix.spinnaker.igor.polling.*; import com.netflix.spinnaker.igor.service.BuildServices; import com.netflix.spinnaker.security.AuthenticatedRequest; @@ -183,15 +181,10 @@ private void sendEventForBuild(ConcourseProperties.Host host, Job job, GenericBu new GenericProject( job.getTeamName() + "/" + job.getPipelineName() + "/" + job.getName(), build); - GenericBuildContent content = new GenericBuildContent(); - content.setProject(project); - content.setMaster("concourse-" + host.getName()); - content.setType("concourse"); + ConcourseBuildContent content = new ConcourseBuildContent(project, host.getName()); - GenericBuildEvent event = new GenericBuildEvent(); - event.setContent(content); - - AuthenticatedRequest.allowAnonymous(() -> echoService.get().postEvent(event)); + AuthenticatedRequest.allowAnonymous( + () -> echoService.get().postEvent(new ConcourseBuildEvent(content))); } else { log.warn("Cannot send build event notification: Echo is not configured"); log.info("({}) unable to push event for :" + build.getFullDisplayName()); diff --git a/igor-web/src/main/java/com/netflix/spinnaker/igor/concourse/service/ConcourseService.java b/igor-web/src/main/java/com/netflix/spinnaker/igor/concourse/service/ConcourseService.java index 9475f7947..74010cb17 100644 --- a/igor-web/src/main/java/com/netflix/spinnaker/igor/concourse/service/ConcourseService.java +++ b/igor-web/src/main/java/com/netflix/spinnaker/igor/concourse/service/ConcourseService.java @@ -36,10 +36,10 @@ import com.netflix.spinnaker.igor.concourse.client.model.Resource; import com.netflix.spinnaker.igor.concourse.client.model.Team; import com.netflix.spinnaker.igor.config.ConcourseProperties; -import com.netflix.spinnaker.igor.model.BuildServiceProvider; import com.netflix.spinnaker.igor.service.ArtifactDecorator; import com.netflix.spinnaker.igor.service.BuildOperations; import com.netflix.spinnaker.igor.service.BuildProperties; +import com.netflix.spinnaker.igor.service.BuildServiceProvider; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.Collection; @@ -144,7 +144,7 @@ public GenericBuild getGenericBuild(String jobPath, int buildNumber) { public GenericBuild getGenericBuild(String jobPath, Build b, boolean fetchResources) { Job job = toJob(jobPath); - GenericBuild build = new GenericBuild(); + GenericBuild build = GenericBuild.builder().build(); build.setId(b.getId()); build.setBuilding(false); build.setNumber(b.getNumber()); diff --git a/igor-web/src/main/java/com/netflix/spinnaker/igor/config/GoogleCloudBuildProperties.java b/igor-web/src/main/java/com/netflix/spinnaker/igor/config/GoogleCloudBuildProperties.java index a0075939f..4b363db6c 100644 --- a/igor-web/src/main/java/com/netflix/spinnaker/igor/config/GoogleCloudBuildProperties.java +++ b/igor-web/src/main/java/com/netflix/spinnaker/igor/config/GoogleCloudBuildProperties.java @@ -17,8 +17,8 @@ package com.netflix.spinnaker.igor.config; import com.netflix.spinnaker.fiat.model.resources.Permissions; -import com.netflix.spinnaker.igor.model.BuildServiceProvider; import com.netflix.spinnaker.igor.service.BuildService; +import com.netflix.spinnaker.igor.service.BuildServiceProvider; import java.util.List; import java.util.stream.Collectors; import lombok.Data; diff --git a/igor-web/src/main/java/com/netflix/spinnaker/igor/health/EchoServiceHealthIndicator.java b/igor-web/src/main/java/com/netflix/spinnaker/igor/health/EchoServiceHealthIndicator.java index a4c43f823..d0a462bd2 100644 --- a/igor-web/src/main/java/com/netflix/spinnaker/igor/health/EchoServiceHealthIndicator.java +++ b/igor-web/src/main/java/com/netflix/spinnaker/igor/health/EchoServiceHealthIndicator.java @@ -18,11 +18,8 @@ import com.netflix.spectator.api.Registry; import com.netflix.spectator.api.patterns.PolledMeter; -import com.netflix.spinnaker.igor.build.model.GenericBuild; -import com.netflix.spinnaker.igor.build.model.GenericProject; import com.netflix.spinnaker.igor.history.EchoService; -import com.netflix.spinnaker.igor.history.model.GenericBuildContent; -import com.netflix.spinnaker.igor.history.model.GenericBuildEvent; +import com.netflix.spinnaker.igor.history.model.Event; import com.netflix.spinnaker.security.AuthenticatedRequest; import java.util.Optional; import java.util.concurrent.atomic.AtomicBoolean; @@ -42,14 +39,15 @@ @Component @ConditionalOnBean(EchoService.class) public class EchoServiceHealthIndicator implements HealthIndicator { + + private static final Event EVENT = new HealthCheckEvent(); + private Logger log = LoggerFactory.getLogger(EchoServiceHealthIndicator.class); private final AtomicReference lastException = new AtomicReference<>(null); private final AtomicBoolean upOnce; private final Optional echoService; private final AtomicLong errors; - static final GenericBuildEvent event = buildGenericEvent(); - @Autowired EchoServiceHealthIndicator(Registry registry, Optional echoService) { this.echoService = echoService; @@ -78,7 +76,7 @@ void checkHealth() { echoService.ifPresent( s -> { try { - AuthenticatedRequest.allowAnonymous(() -> s.postEvent(event)); + AuthenticatedRequest.allowAnonymous(() -> s.postEvent(EVENT)); upOnce.set(true); errors.set(0); lastException.set(null); @@ -90,20 +88,13 @@ void checkHealth() { }); } - private static GenericBuildEvent buildGenericEvent() { - final GenericBuildEvent event = new GenericBuildEvent(); - final GenericBuildContent buildContent = new GenericBuildContent(); - final GenericProject project = new GenericProject("spinnaker", new GenericBuild()); - buildContent.setMaster("IgorHealthCheck"); - buildContent.setProject(project); - event.setContent(buildContent); - return event; - } - @ResponseStatus(value = HttpStatus.SERVICE_UNAVAILABLE, reason = "Could not reach Echo.") static class EchoUnreachableException extends RuntimeException { public EchoUnreachableException(Throwable cause) { super(cause); } } + + /** No need to send anything across the wire. */ + private static class HealthCheckEvent implements Event {} } diff --git a/igor-web/src/main/java/com/netflix/spinnaker/igor/nexus/model/NexusAssetEvent.java b/igor-web/src/main/java/com/netflix/spinnaker/igor/nexus/model/NexusAssetEvent.java index 778c8c68e..4f7ac58f1 100644 --- a/igor-web/src/main/java/com/netflix/spinnaker/igor/nexus/model/NexusAssetEvent.java +++ b/igor-web/src/main/java/com/netflix/spinnaker/igor/nexus/model/NexusAssetEvent.java @@ -28,7 +28,7 @@ @RequiredArgsConstructor @Data @EqualsAndHashCode(callSuper = false) -public class NexusAssetEvent extends Event { +public class NexusAssetEvent implements Event { private final Content content; private final Map details = ImmutableMap.builder().put("type", "nexus").put("source", "igor").build(); diff --git a/igor-web/src/main/java/com/netflix/spinnaker/igor/travis/TravisBuildContent.java b/igor-web/src/main/java/com/netflix/spinnaker/igor/travis/TravisBuildContent.java new file mode 100644 index 000000000..16332e167 --- /dev/null +++ b/igor-web/src/main/java/com/netflix/spinnaker/igor/travis/TravisBuildContent.java @@ -0,0 +1,35 @@ +/* + * Copyright 2020 Netflix, 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.netflix.spinnaker.igor.travis; + +import com.netflix.spinnaker.igor.build.model.GenericProject; +import com.netflix.spinnaker.igor.history.model.BuildContent; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class TravisBuildContent implements BuildContent { + public static final String TYPE = "travis"; + + private GenericProject project; + private String master; + + @Override + public String getType() { + return TYPE; + } +} diff --git a/igor-web/src/main/java/com/netflix/spinnaker/igor/travis/TravisBuildEvent.java b/igor-web/src/main/java/com/netflix/spinnaker/igor/travis/TravisBuildEvent.java new file mode 100644 index 000000000..1740ade3b --- /dev/null +++ b/igor-web/src/main/java/com/netflix/spinnaker/igor/travis/TravisBuildEvent.java @@ -0,0 +1,26 @@ +/* + * Copyright 2020 Netflix, 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.netflix.spinnaker.igor.travis; + +import com.netflix.spinnaker.igor.history.model.BuildEvent; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class TravisBuildEvent implements BuildEvent { + private TravisBuildContent content; +} diff --git a/igor-web/src/main/java/com/netflix/spinnaker/igor/travis/TravisBuildMonitor.java b/igor-web/src/main/java/com/netflix/spinnaker/igor/travis/TravisBuildMonitor.java index dcf913bb0..0f6becb1d 100644 --- a/igor-web/src/main/java/com/netflix/spinnaker/igor/travis/TravisBuildMonitor.java +++ b/igor-web/src/main/java/com/netflix/spinnaker/igor/travis/TravisBuildMonitor.java @@ -27,14 +27,12 @@ import com.netflix.spinnaker.igor.build.model.GenericProject; import com.netflix.spinnaker.igor.config.TravisProperties; import com.netflix.spinnaker.igor.history.EchoService; -import com.netflix.spinnaker.igor.history.model.GenericBuildContent; -import com.netflix.spinnaker.igor.history.model.GenericBuildEvent; -import com.netflix.spinnaker.igor.model.BuildServiceProvider; import com.netflix.spinnaker.igor.polling.CommonPollingMonitor; import com.netflix.spinnaker.igor.polling.DeltaItem; import com.netflix.spinnaker.igor.polling.LockService; import com.netflix.spinnaker.igor.polling.PollContext; import com.netflix.spinnaker.igor.polling.PollingDelta; +import com.netflix.spinnaker.igor.service.BuildServiceProvider; import com.netflix.spinnaker.igor.service.BuildServices; import com.netflix.spinnaker.igor.travis.client.model.v3.TravisBuildState; import com.netflix.spinnaker.igor.travis.client.model.v3.V3Build; @@ -249,16 +247,10 @@ private void sendEventForBuild( kv("master", master)); GenericProject project = new GenericProject(branchedSlug, buildDelta.getGenericBuild()); + TravisBuildContent content = new TravisBuildContent(project, master); - GenericBuildContent content = new GenericBuildContent(); - content.setProject(project); - content.setMaster(master); - content.setType("travis"); - - GenericBuildEvent event = new GenericBuildEvent(); - event.setContent(content); - - AuthenticatedRequest.allowAnonymous(() -> echoService.get().postEvent(event)); + AuthenticatedRequest.allowAnonymous( + () -> echoService.get().postEvent(new TravisBuildEvent(content))); } else { log.warn("Cannot send build event notification: Echo is not configured"); log.info( diff --git a/igor-web/src/main/java/com/netflix/spinnaker/igor/travis/service/TravisBuildConverter.java b/igor-web/src/main/java/com/netflix/spinnaker/igor/travis/service/TravisBuildConverter.java index 2d3196452..c50f5e724 100644 --- a/igor-web/src/main/java/com/netflix/spinnaker/igor/travis/service/TravisBuildConverter.java +++ b/igor-web/src/main/java/com/netflix/spinnaker/igor/travis/service/TravisBuildConverter.java @@ -24,14 +24,16 @@ public class TravisBuildConverter { public static GenericBuild genericBuild(Build build, String repoSlug, String baseUrl) { - GenericBuild genericBuild = new GenericBuild(); - genericBuild.setBuilding(build.getState() == TravisBuildState.started); - genericBuild.setNumber(build.getNumber()); - genericBuild.setDuration(build.getDuration()); - genericBuild.setResult(build.getState().getResult()); - genericBuild.setName(repoSlug); - genericBuild.setUrl(url(repoSlug, baseUrl, build.getId())); - genericBuild.setId(String.valueOf(build.getId())); + GenericBuild genericBuild = + GenericBuild.builder() + .building(build.getState() == TravisBuildState.started) + .number(build.getNumber()) + .duration(build.getDuration()) + .result(build.getState().getResult()) + .name(repoSlug) + .url(url(repoSlug, baseUrl, build.getId())) + .id(String.valueOf(build.getId())) + .build(); if (build.getFinishedAt() != null) { genericBuild.setTimestamp(String.valueOf(build.getTimestamp())); } @@ -39,14 +41,15 @@ public static GenericBuild genericBuild(Build build, String repoSlug, String bas } public static GenericBuild genericBuild(V3Build build, String baseUrl) { - GenericBuild genericBuild = new GenericBuild(); - genericBuild.setBuilding(build.getState() == TravisBuildState.started); - genericBuild.setNumber(build.getNumber()); - genericBuild.setDuration(build.getDuration()); - genericBuild.setResult(build.getState().getResult()); - genericBuild.setName(build.getRepository().getSlug()); - genericBuild.setUrl(url(build.getRepository().getSlug(), baseUrl, build.getId())); - genericBuild.setId(String.valueOf(build.getId())); + GenericBuild genericBuild = + GenericBuild.builder() + .building(build.getState() == TravisBuildState.started) + .number(build.getNumber()) + .result(build.getState().getResult()) + .name(build.getRepository().getSlug()) + .url(url(build.getRepository().getSlug(), baseUrl, build.getId())) + .id(String.valueOf(build.getId())) + .build(); if (build.getFinishedAt() != null) { genericBuild.setTimestamp(String.valueOf(build.getTimestamp())); } diff --git a/igor-web/src/main/java/com/netflix/spinnaker/igor/travis/service/TravisService.java b/igor-web/src/main/java/com/netflix/spinnaker/igor/travis/service/TravisService.java index ba3007641..182f2743f 100644 --- a/igor-web/src/main/java/com/netflix/spinnaker/igor/travis/service/TravisService.java +++ b/igor-web/src/main/java/com/netflix/spinnaker/igor/travis/service/TravisService.java @@ -26,10 +26,7 @@ import com.netflix.spinnaker.igor.build.model.GenericJobConfiguration; import com.netflix.spinnaker.igor.build.model.JobConfiguration; import com.netflix.spinnaker.igor.build.model.Result; -import com.netflix.spinnaker.igor.model.BuildServiceProvider; -import com.netflix.spinnaker.igor.service.ArtifactDecorator; -import com.netflix.spinnaker.igor.service.BuildOperations; -import com.netflix.spinnaker.igor.service.BuildProperties; +import com.netflix.spinnaker.igor.service.*; import com.netflix.spinnaker.igor.travis.TravisCache; import com.netflix.spinnaker.igor.travis.client.TravisClient; import com.netflix.spinnaker.igor.travis.client.logparser.ArtifactParser; @@ -69,7 +66,9 @@ import org.slf4j.LoggerFactory; import retrofit.RetrofitError; -public class TravisService implements BuildOperations, BuildProperties { +/** TODO(rz): The generic type of QueuingSupport isn't very good. */ +public class TravisService + implements BuildOperations, BuildProperties, BuildQueueOperations> { static final int TRAVIS_BUILD_RESULT_LIMIT = 100; @@ -494,8 +493,9 @@ public String getUrl(String repoSlug) { return baseUrl + "/" + repoSlug; } - public Map queuedBuild(int queueId) { - Map queuedJob = travisCache.getQueuedJob(groupKey, queueId); + @Override + public Map getQueuedBuild(String queueId) { + Map queuedJob = travisCache.getQueuedJob(groupKey, Integer.parseInt(queueId)); Request requestResponse = travisClient.request( getAccessToken(), queuedJob.get("repositoryId"), queuedJob.get("requestId")); @@ -507,7 +507,7 @@ public Map queuedBuild(int queueId) { requestResponse.getBuilds().get(0).getNumber(), queueId, groupKey); - travisCache.removeQuededJob(groupKey, queueId); + travisCache.removeQuededJob(groupKey, Integer.parseInt(queueId)); LinkedHashMap map = new LinkedHashMap<>(1); map.put("number", requestResponse.getBuilds().get(0).getNumber()); return map; diff --git a/igor-web/src/test/groovy/com/netflix/spinnaker/igor/artifacts/ArtifactExtractorSpec.groovy b/igor-web/src/test/groovy/com/netflix/spinnaker/igor/artifacts/ArtifactExtractorSpec.groovy index f32982451..8c5b3314a 100644 --- a/igor-web/src/test/groovy/com/netflix/spinnaker/igor/artifacts/ArtifactExtractorSpec.groovy +++ b/igor-web/src/test/groovy/com/netflix/spinnaker/igor/artifacts/ArtifactExtractorSpec.groovy @@ -40,8 +40,7 @@ class ArtifactExtractorSpec extends Specification { messageFormat: 'JAR', customFormat: 'false' ] - def build = new GenericBuild() - build.properties = properties + def build = GenericBuild.builder().properties(properties).build() when: def artifacts = artifactExtractor.extractArtifacts(build) diff --git a/igor-web/src/test/groovy/com/netflix/spinnaker/igor/build/BuildControllerSpec.groovy b/igor-web/src/test/groovy/com/netflix/spinnaker/igor/build/BuildControllerSpec.groovy index 1f22832bb..ff9081ca1 100644 --- a/igor-web/src/test/groovy/com/netflix/spinnaker/igor/build/BuildControllerSpec.groovy +++ b/igor-web/src/test/groovy/com/netflix/spinnaker/igor/build/BuildControllerSpec.groovy @@ -18,34 +18,27 @@ package com.netflix.spinnaker.igor.build import com.netflix.spinnaker.igor.build.model.GenericBuild import com.netflix.spinnaker.igor.config.JenkinsConfig +import com.netflix.spinnaker.igor.jenkins.JenkinsService import com.netflix.spinnaker.igor.jenkins.client.model.Build import com.netflix.spinnaker.igor.jenkins.client.model.BuildArtifact -import com.netflix.spinnaker.igor.jenkins.client.model.JobConfig -import com.netflix.spinnaker.igor.jenkins.client.model.ParameterDefinition import com.netflix.spinnaker.igor.jenkins.client.model.QueuedJob -import com.netflix.spinnaker.igor.jenkins.service.JenkinsService -import com.netflix.spinnaker.igor.model.BuildServiceProvider +import com.netflix.spinnaker.igor.service.BuildServiceProvider import com.netflix.spinnaker.igor.service.BuildOperations import com.netflix.spinnaker.igor.service.BuildServices import com.netflix.spinnaker.igor.travis.service.TravisService -import com.netflix.spinnaker.kork.core.RetrySupport import com.netflix.spinnaker.kork.web.exceptions.GenericExceptionHandlers import com.netflix.spinnaker.kork.web.exceptions.NotFoundException import com.squareup.okhttp.mockwebserver.MockWebServer -import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.mock.web.MockHttpServletResponse import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.setup.MockMvcBuilders -import retrofit.client.Header -import retrofit.client.Response import spock.lang.Shared import spock.lang.Specification -import static com.netflix.spinnaker.igor.build.BuildController.* import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + /** * Tests for BuildController */ @@ -58,10 +51,6 @@ class BuildControllerSpec extends Specification { JenkinsService jenkinsService BuildOperations service TravisService travisService - Map serviceList - def retrySupport = Spy(RetrySupport) { - _ * sleep(_) >> { /* do nothing */ } - } @Shared MockWebServer server @@ -69,12 +58,10 @@ class BuildControllerSpec extends Specification { final SERVICE = 'SERVICE' final JENKINS_SERVICE = 'JENKINS_SERVICE' final TRAVIS_SERVICE = 'TRAVIS_SERVICE' - final HTTP_201 = 201 final BUILD_NUMBER = 123 final BUILD_ID = 654321 final QUEUED_JOB_NUMBER = 123456 final JOB_NAME = "job/name/can/have/slashes" - final JOB_NAME_LEGACY = "job" final FILE_NAME = "test.yml" GenericBuild genericBuild @@ -95,7 +82,7 @@ class BuildControllerSpec extends Specification { (JENKINS_SERVICE): jenkinsService, (TRAVIS_SERVICE): travisService, ]) - genericBuild = new GenericBuild() + genericBuild = GenericBuild.builder().build() genericBuild.number = BUILD_NUMBER genericBuild.id = BUILD_ID @@ -110,7 +97,7 @@ class BuildControllerSpec extends Specification { void 'get the status of a build'() { given: - 1 * service.getGenericBuild(JOB_NAME, BUILD_NUMBER) >> new GenericBuild(building: false, number: BUILD_NUMBER) + 1 * service.getGenericBuild(JOB_NAME, BUILD_NUMBER) >> GenericBuild.builder().building(false).number(BUILD_NUMBER).build() when: MockHttpServletResponse response = mockMvc.perform(get("/builds/status/${BUILD_NUMBER}/${SERVICE}/${JOB_NAME}") @@ -122,7 +109,7 @@ class BuildControllerSpec extends Specification { void 'get an item from the queue'() { given: - 1 * jenkinsService.queuedBuild(QUEUED_JOB_NUMBER) >> new QueuedJob(executable: [number: QUEUED_JOB_NUMBER]) + 1 * jenkinsService.getQueuedBuild(QUEUED_JOB_NUMBER.toString()) >> new QueuedJob(executable: [number: QUEUED_JOB_NUMBER]) when: MockHttpServletResponse response = mockMvc.perform(get("/builds/queue/${JENKINS_SERVICE}/${QUEUED_JOB_NUMBER}") @@ -227,123 +214,4 @@ class BuildControllerSpec extends Specification { then: response.contentAsString == "{\"foo\":\"bar\"}" } - - void 'trigger a build without parameters'() { - given: - 1 * jenkinsService.getJobConfig(JOB_NAME) >> new JobConfig(buildable: true) - 1 * jenkinsService.build(JOB_NAME) >> new Response("http://test.com", HTTP_201, "", [new Header("Location", "foo/${BUILD_NUMBER}")], null) - - when: - MockHttpServletResponse response = mockMvc.perform(put("/masters/${JENKINS_SERVICE}/jobs/${JOB_NAME}") - .accept(MediaType.APPLICATION_JSON)).andReturn().response - - then: - response.contentAsString == BUILD_NUMBER.toString() - - } - - void 'trigger a build with parameters to a job with parameters'() { - given: - 1 * jenkinsService.getJobConfig(JOB_NAME) >> new JobConfig(buildable: true, parameterDefinitionList: [new ParameterDefinition(defaultParameterValue: [name: "name", value: null], description: "description")]) - 1 * jenkinsService.buildWithParameters(JOB_NAME, [name: "myName"]) >> new Response("http://test.com", HTTP_201, "", [new Header("Location", "foo/${BUILD_NUMBER}")], null) - - when: - MockHttpServletResponse response = mockMvc.perform(put("/masters/${JENKINS_SERVICE}/jobs/${JOB_NAME}") - .contentType(MediaType.APPLICATION_JSON).param("name", "myName")).andReturn().response - - then: - response.contentAsString == BUILD_NUMBER.toString() - } - - void 'trigger a build without parameters to a job with parameters with default values'() { - given: - 1 * jenkinsService.getJobConfig(JOB_NAME) >> new JobConfig(buildable: true, parameterDefinitionList: [new ParameterDefinition(defaultParameterValue: [name: "name", value: "value"], description: "description")]) - 1 * jenkinsService.buildWithParameters(JOB_NAME, ['startedBy': "igor"]) >> new Response("http://test.com", HTTP_201, "", [new Header("Location", "foo/${BUILD_NUMBER}")], null) - - when: - MockHttpServletResponse response = mockMvc.perform(put("/masters/${JENKINS_SERVICE}/jobs/${JOB_NAME}", "") - .accept(MediaType.APPLICATION_JSON)).andReturn().response - - then: - response.contentAsString == BUILD_NUMBER.toString() - } - - void 'trigger a build with parameters to a job without parameters'() { - given: - 1 * jenkinsService.getJobConfig(JOB_NAME) >> new JobConfig(buildable: true) - - when: - MockHttpServletResponse response = mockMvc.perform(put("/masters/${JENKINS_SERVICE}/jobs/${JOB_NAME}") - .contentType(MediaType.APPLICATION_JSON).param("foo", "bar")).andReturn().response - - then: - response.status == HttpStatus.INTERNAL_SERVER_ERROR.value() - } - - void 'trigger a build with an invalid choice'() { - given: - JobConfig config = new JobConfig(buildable: true) - config.parameterDefinitionList = [ - new ParameterDefinition(type: "ChoiceParameterDefinition", name: "foo", choices: ["bar", "baz"]) - ] - 1 * jenkinsService.getJobConfig(JOB_NAME) >> config - - when: - MockHttpServletResponse response = mockMvc.perform(put("/masters/${JENKINS_SERVICE}/jobs/${JOB_NAME}") - .contentType(MediaType.APPLICATION_JSON).param("foo", "bat")).andReturn().response - - then: - - response.status == HttpStatus.BAD_REQUEST.value() - response.errorMessage == "`bat` is not a valid choice for `foo`. Valid choices are: bar, baz" - } - - void 'trigger a disabled build'() { - given: - JobConfig config = new JobConfig() - 1 * jenkinsService.getJobConfig(JOB_NAME) >> config - - when: - MockHttpServletResponse response = mockMvc.perform(put("/masters/${JENKINS_SERVICE}/jobs/${JOB_NAME}") - .contentType(MediaType.APPLICATION_JSON).param("foo", "bat")).andReturn().response - - then: - response.status == HttpStatus.BAD_REQUEST.value() - response.errorMessage == "Job '${JOB_NAME}' is not buildable. It may be disabled." - } - - void 'validation successful for null list of choices'() { - given: - Map requestParams = ["hey" : "you"] - ParameterDefinition parameterDefinition = new ParameterDefinition() - parameterDefinition.choices = null - parameterDefinition.type = "ChoiceParameterDefinition" - parameterDefinition.name = "hey" - JobConfig jobConfig = new JobConfig() - jobConfig.parameterDefinitionList = [parameterDefinition] - - when: - validateJobParameters(jobConfig, requestParams) - - then: - noExceptionThrown() - } - - void 'validation failed for option not in list of choices'() { - given: - Map requestParams = ["hey" : "you"] - ParameterDefinition parameterDefinition = new ParameterDefinition() - parameterDefinition.choices = ["why", "not"] - parameterDefinition.type = "ChoiceParameterDefinition" - parameterDefinition.name = "hey" - JobConfig jobConfig = new JobConfig() - jobConfig.parameterDefinitionList = [parameterDefinition] - - when: - validateJobParameters(jobConfig, requestParams) - - then: - thrown(InvalidJobParameterException) - } - } diff --git a/igor-web/src/test/groovy/com/netflix/spinnaker/igor/build/InfoControllerSpec.groovy b/igor-web/src/test/groovy/com/netflix/spinnaker/igor/build/InfoControllerSpec.groovy index 8035e5bce..9f52c9213 100644 --- a/igor-web/src/test/groovy/com/netflix/spinnaker/igor/build/InfoControllerSpec.groovy +++ b/igor-web/src/test/groovy/com/netflix/spinnaker/igor/build/InfoControllerSpec.groovy @@ -21,8 +21,8 @@ import com.netflix.spinnaker.fiat.model.resources.Permissions import com.netflix.spinnaker.igor.config.GoogleCloudBuildProperties import com.netflix.spinnaker.igor.config.JenkinsConfig import com.netflix.spinnaker.igor.config.JenkinsProperties -import com.netflix.spinnaker.igor.jenkins.service.JenkinsService -import com.netflix.spinnaker.igor.model.BuildServiceProvider +import com.netflix.spinnaker.igor.jenkins.JenkinsService +import com.netflix.spinnaker.igor.service.BuildServiceProvider import com.netflix.spinnaker.igor.service.BuildOperations import com.netflix.spinnaker.igor.service.BuildServices import com.netflix.spinnaker.igor.travis.service.TravisService @@ -39,6 +39,7 @@ import spock.lang.Specification import spock.lang.Unroll import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get + /** * tests for the info controller */ @@ -240,35 +241,14 @@ class InfoControllerSpec extends Specification { .accept(MediaType.APPLICATION_JSON)).andReturn().response then: - jenkinsService.getJobs() >> ['list': [ - ['name': 'job1'], - ['name': 'job2'], - ['name': 'job3'] - ]] + jenkinsService.getJobNames() >> [ + 'job1', + 'job2', + 'job3' + ] response.contentAsString == '["job1","job2","job3"]' } - void 'is able to get jobs for a jenkins master with the folders plugin'() { - given: - JenkinsService jenkinsService = Stub(JenkinsService) - createMocks(['master1': jenkinsService]) - - when: - MockHttpServletResponse response = mockMvc.perform(get('/jobs/master1/') - .accept(MediaType.APPLICATION_JSON)).andReturn().response - - then: - jenkinsService.getBuildServiceProvider() >> BuildServiceProvider.JENKINS - jenkinsService.getJobs() >> ['list': [ - ['name': 'folder', 'list': [ - ['name': 'job1'], - ['name': 'job2'] - ] ], - ['name': 'job3'] - ]] - response.contentAsString == '["folder/job/job1","folder/job/job2","job3"]' - } - void 'is able to get jobs for a travis master'() { given: TravisService travisService = Stub(TravisService) @@ -297,9 +277,8 @@ class InfoControllerSpec extends Specification { then: werckerService.getBuildServiceProvider() >> BuildServiceProvider.WERCKER - werckerService.getJobs() >> [werckerJob] + werckerService.getJobNames() >> [werckerJob] response.contentAsString == '["' + werckerJob + '"]' - } private void setResponse(String body) { diff --git a/igor-web/src/test/groovy/com/netflix/spinnaker/igor/jenkins/JenkinsBuildMonitorSchedulingSpec.groovy b/igor-web/src/test/groovy/com/netflix/spinnaker/igor/jenkins/JenkinsBuildMonitorSchedulingSpec.groovy index 62f472b7f..c4bcb11b9 100644 --- a/igor-web/src/test/groovy/com/netflix/spinnaker/igor/jenkins/JenkinsBuildMonitorSchedulingSpec.groovy +++ b/igor-web/src/test/groovy/com/netflix/spinnaker/igor/jenkins/JenkinsBuildMonitorSchedulingSpec.groovy @@ -20,8 +20,7 @@ import com.netflix.spectator.api.NoopRegistry import com.netflix.spinnaker.igor.IgorConfigurationProperties import com.netflix.spinnaker.igor.config.JenkinsProperties import com.netflix.spinnaker.igor.jenkins.client.model.ProjectsList -import com.netflix.spinnaker.igor.jenkins.service.JenkinsService -import com.netflix.spinnaker.igor.model.BuildServiceProvider +import com.netflix.spinnaker.igor.service.BuildServiceProvider import com.netflix.spinnaker.igor.service.BuildServices import com.netflix.spinnaker.kork.eureka.RemoteStatusChangedEvent import rx.schedulers.TestScheduler diff --git a/igor-web/src/test/groovy/com/netflix/spinnaker/igor/jenkins/JenkinsBuildMonitorSpec.groovy b/igor-web/src/test/groovy/com/netflix/spinnaker/igor/jenkins/JenkinsBuildMonitorSpec.groovy index 7294d6236..3977ff950 100644 --- a/igor-web/src/test/groovy/com/netflix/spinnaker/igor/jenkins/JenkinsBuildMonitorSpec.groovy +++ b/igor-web/src/test/groovy/com/netflix/spinnaker/igor/jenkins/JenkinsBuildMonitorSpec.groovy @@ -24,7 +24,6 @@ import com.netflix.spinnaker.igor.history.model.Event import com.netflix.spinnaker.igor.jenkins.client.model.Build import com.netflix.spinnaker.igor.jenkins.client.model.Project import com.netflix.spinnaker.igor.jenkins.client.model.ProjectsList -import com.netflix.spinnaker.igor.jenkins.service.JenkinsService import com.netflix.spinnaker.igor.polling.PollContext import com.netflix.spinnaker.igor.service.BuildServices import org.slf4j.Logger @@ -57,7 +56,9 @@ class JenkinsBuildMonitorSpec extends Specification { buildServices, true, Optional.of(echoService), - new JenkinsProperties() + new JenkinsProperties(masters: [ + new JenkinsProperties.JenkinsHost(name: MASTER, address: "http://example.net") + ]) ) monitor.worker = Schedulers.immediate().createWorker() @@ -261,18 +262,13 @@ class JenkinsBuildMonitorSpec extends Specification { new Build(number: 3, timestamp: nowMinus30min, building: false, result: 'SUCCESS', duration: durationOf1min) ] - and: - monitor.log = Mock(Logger); - when: monitor.pollSingle(new PollContext(MASTER)) then: 'Builds are processed for job1' 1 * echoService.postEvent({ it.content.project.name == 'job1'} as Event) - and: 'Errors are logged for job2; no builds are processed' - 1 * monitor.log.error('Error communicating with jenkins for [{}:{}]: {}', _) - 1 * monitor.log.error('Error processing builds for [{}:{}]', _) + and: 'no builds are processed for job2 due to errors' 0 * echoService.postEvent({ it.content.project.name == 'job2'} as Event) and: 'Builds are not processed for job3' diff --git a/igor-web/src/test/groovy/com/netflix/spinnaker/igor/wercker/WerckerBuildMonitorSchedulingSpec.groovy b/igor-web/src/test/groovy/com/netflix/spinnaker/igor/wercker/WerckerBuildMonitorSchedulingSpec.groovy index 158fbc906..2e582fe0f 100644 --- a/igor-web/src/test/groovy/com/netflix/spinnaker/igor/wercker/WerckerBuildMonitorSchedulingSpec.groovy +++ b/igor-web/src/test/groovy/com/netflix/spinnaker/igor/wercker/WerckerBuildMonitorSchedulingSpec.groovy @@ -11,7 +11,7 @@ package com.netflix.spinnaker.igor.wercker import com.netflix.spectator.api.NoopRegistry import com.netflix.spinnaker.igor.IgorConfigurationProperties import com.netflix.spinnaker.igor.config.WerckerProperties -import com.netflix.spinnaker.igor.model.BuildServiceProvider +import com.netflix.spinnaker.igor.service.BuildServiceProvider import com.netflix.spinnaker.igor.service.BuildServices import com.netflix.spinnaker.kork.eureka.RemoteStatusChangedEvent diff --git a/igor-web/src/test/groovy/com/netflix/spinnaker/igor/wercker/WerckerBuildMonitorSpec.groovy b/igor-web/src/test/groovy/com/netflix/spinnaker/igor/wercker/WerckerBuildMonitorSpec.groovy index da765011c..c7c3885ae 100644 --- a/igor-web/src/test/groovy/com/netflix/spinnaker/igor/wercker/WerckerBuildMonitorSpec.groovy +++ b/igor-web/src/test/groovy/com/netflix/spinnaker/igor/wercker/WerckerBuildMonitorSpec.groovy @@ -14,7 +14,7 @@ import com.netflix.spinnaker.igor.IgorConfigurationProperties import com.netflix.spinnaker.igor.config.WerckerProperties import com.netflix.spinnaker.igor.config.WerckerProperties.WerckerHost import com.netflix.spinnaker.igor.history.EchoService -import com.netflix.spinnaker.igor.model.BuildServiceProvider +import com.netflix.spinnaker.igor.service.BuildServiceProvider import com.netflix.spinnaker.igor.service.BuildServices import com.netflix.spinnaker.igor.wercker.model.Application import com.netflix.spinnaker.igor.wercker.model.Owner diff --git a/settings.gradle b/settings.gradle index 4bcbf1fd9..7e6b6317e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -19,6 +19,7 @@ rootProject.name='igor' include 'igor-bom', 'igor-core', 'igor-monitor-artifactory', + 'igor-monitor-jenkins', 'igor-web' def setBuildFile(project) {