diff --git a/igor-web/igor-web.gradle b/igor-web/igor-web.gradle index 8f6b64306..a3a0fbb10 100644 --- a/igor-web/igor-web.gradle +++ b/igor-web/igor-web.gradle @@ -48,6 +48,7 @@ dependencies { implementation "com.squareup.okhttp:okhttp-apache" implementation "com.squareup.okhttp3:okhttp-sse" implementation "com.squareup.retrofit:converter-simplexml" + implementation "com.jakewharton.retrofit:retrofit1-okhttp3-client:1.1.0" implementation "com.netflix.spinnaker.fiat:fiat-api:$fiatVersion" implementation "com.netflix.spinnaker.fiat:fiat-core:$fiatVersion" diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/IgorConfigurationProperties.groovy b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/IgorConfigurationProperties.groovy index d67165f42..763afb611 100644 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/IgorConfigurationProperties.groovy +++ b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/IgorConfigurationProperties.groovy @@ -99,6 +99,8 @@ class IgorConfigurationProperties { final ServiceConfiguration clouddriver = new ServiceConfiguration() @NestedConfigurationProperty final ServiceConfiguration echo = new ServiceConfiguration() + @NestedConfigurationProperty + final ServiceConfiguration keel = new ServiceConfiguration() } @NestedConfigurationProperty diff --git a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/docker/DockerMonitor.groovy b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/docker/DockerMonitor.groovy index bdc8748c5..f4ac97ee6 100644 --- a/igor-web/src/main/groovy/com/netflix/spinnaker/igor/docker/DockerMonitor.groovy +++ b/igor-web/src/main/groovy/com/netflix/spinnaker/igor/docker/DockerMonitor.groovy @@ -26,11 +26,13 @@ import com.netflix.spinnaker.igor.docker.model.DockerRegistryAccounts import com.netflix.spinnaker.igor.docker.service.TaggedImage import com.netflix.spinnaker.igor.history.EchoService import com.netflix.spinnaker.igor.history.model.DockerEvent +import com.netflix.spinnaker.igor.keel.KeelService 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.kork.artifacts.model.Artifact import com.netflix.spinnaker.security.AuthenticatedRequest import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty @@ -48,6 +50,7 @@ class DockerMonitor extends CommonPollingMonitor private final DockerRegistryCache cache private final DockerRegistryAccounts dockerRegistryAccounts private final Optional echoService + private final Optional keelService private final Optional keysMigration private final DockerRegistryProperties dockerRegistryProperties @@ -59,6 +62,7 @@ class DockerMonitor extends CommonPollingMonitor DockerRegistryCache cache, DockerRegistryAccounts dockerRegistryAccounts, Optional echoService, + Optional keelService, Optional keysMigration, DockerRegistryProperties dockerRegistryProperties) { super(properties, registry, discoveryClient, lockService) @@ -67,6 +71,7 @@ class DockerMonitor extends CommonPollingMonitor this.echoService = echoService this.keysMigration = keysMigration this.dockerRegistryProperties = dockerRegistryProperties + this.keelService = keelService } @Override @@ -187,6 +192,26 @@ class DockerMonitor extends CommonPollingMonitor account: image.account, ), artifact: dockerArtifact)) } + + if (keelService.isPresent()) { + String imageReference = image.repository + ":" + image.tag + Artifact artifact = Artifact.builder() + .type("DOCKER") + .customKind(false) + .name(image.repository) + .version(image.tag) + .location(image.account) + .reference(imageId) + .metadata([fullname: imageReference, registry: image.account, tag: image.tag],) + .provenance(image.registry) + .build() + + Map artifactEvent = [ + payload: [artifacts: [artifact], details: [:]], + eventName: "spinnaker_artifacts_docker" + ] + AuthenticatedRequest.allowAnonymous { keelService.get().sendArtifactEvent(artifactEvent) } + } } @Override diff --git a/igor-web/src/main/java/com/netflix/spinnaker/igor/config/IgorRetrofitConfig.java b/igor-web/src/main/java/com/netflix/spinnaker/igor/config/IgorRetrofitConfig.java new file mode 100644 index 000000000..b62a12d9a --- /dev/null +++ b/igor-web/src/main/java/com/netflix/spinnaker/igor/config/IgorRetrofitConfig.java @@ -0,0 +1,29 @@ +/* + * Copyright 2019 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.jakewharton.retrofit.Ok3Client; +import com.netflix.spinnaker.config.OkHttp3ClientConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class IgorRetrofitConfig { + @Bean + public Ok3Client ok3Client(OkHttp3ClientConfiguration okHttpClientConfig) { + return new Ok3Client(okHttpClientConfig.create().build()); + } +} diff --git a/igor-web/src/main/java/com/netflix/spinnaker/igor/config/KeelConfig.java b/igor-web/src/main/java/com/netflix/spinnaker/igor/config/KeelConfig.java new file mode 100644 index 000000000..1bfce8acb --- /dev/null +++ b/igor-web/src/main/java/com/netflix/spinnaker/igor/config/KeelConfig.java @@ -0,0 +1,57 @@ +/* + * Copyright 2019 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.jakewharton.retrofit.Ok3Client; +import com.netflix.spinnaker.igor.keel.KeelService; +import com.netflix.spinnaker.retrofit.Slf4jRetrofitLogger; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import retrofit.Endpoint; +import retrofit.Endpoints; +import retrofit.RestAdapter; +import retrofit.converter.JacksonConverter; + +@ConditionalOnProperty("services.keel.base-url") +@Configuration +public class KeelConfig { + + @Bean + public RestAdapter.LogLevel retrofitLogLevel( + @Value("${retrofit.log-level:BASIC}") String retrofitLogLevel) { + return RestAdapter.LogLevel.valueOf(retrofitLogLevel); + } + + @Bean + public Endpoint keelEndpoint(@Value("${services.keel.base-url}") String keelBaseUrl) { + return Endpoints.newFixedEndpoint(keelBaseUrl); + } + + @Bean + public KeelService keelService( + Endpoint keelEndpoint, Ok3Client ok3Client, RestAdapter.LogLevel retrofitLogLevel) { + return new RestAdapter.Builder() + .setEndpoint(keelEndpoint) + .setConverter(new JacksonConverter()) + .setClient(ok3Client) + .setLogLevel(retrofitLogLevel) + .setLog(new Slf4jRetrofitLogger(KeelService.class)) + .build() + .create(KeelService.class); + } +} diff --git a/igor-web/src/main/java/com/netflix/spinnaker/igor/keel/KeelService.java b/igor-web/src/main/java/com/netflix/spinnaker/igor/keel/KeelService.java new file mode 100644 index 000000000..41da28d0a --- /dev/null +++ b/igor-web/src/main/java/com/netflix/spinnaker/igor/keel/KeelService.java @@ -0,0 +1,29 @@ +/* + * Copyright 2019 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.keel; + +import java.util.Map; +import retrofit.http.Body; +import retrofit.http.POST; + +public interface KeelService { + /** + * Events should be sent with this format (inherited from echo events): [ payload: [artifacts: + * List, details: Map], eventName: String ] + */ + @POST("/artifacts/events") + Void sendArtifactEvent(@Body Map event); +} diff --git a/igor-web/src/test/groovy/com/netflix/spinnaker/igor/docker/DockerMonitorSpec.groovy b/igor-web/src/test/groovy/com/netflix/spinnaker/igor/docker/DockerMonitorSpec.groovy index 5c88024c4..901319c9e 100644 --- a/igor-web/src/test/groovy/com/netflix/spinnaker/igor/docker/DockerMonitorSpec.groovy +++ b/igor-web/src/test/groovy/com/netflix/spinnaker/igor/docker/DockerMonitorSpec.groovy @@ -25,6 +25,7 @@ import com.netflix.spinnaker.igor.docker.model.DockerRegistryAccounts import com.netflix.spinnaker.igor.docker.service.TaggedImage import com.netflix.spinnaker.igor.history.EchoService import com.netflix.spinnaker.igor.history.model.DockerEvent +import com.netflix.spinnaker.igor.keel.KeelService import com.netflix.spinnaker.igor.polling.LockService import spock.lang.Specification import spock.lang.Unroll @@ -38,6 +39,7 @@ class DockerMonitorSpec extends Specification { def dockerRegistryCache = Mock(DockerRegistryCache) def dockerRegistryAccounts = Mock(DockerRegistryAccounts) def echoService = Mock(EchoService) + def keelService = Mock(KeelService) Optional keysMigration = Optional.empty() def dockerRegistryProperties = new DockerRegistryProperties(enabled: true, itemUpperThreshold: 5) @@ -53,7 +55,7 @@ class DockerMonitorSpec extends Specification { ) when: - new DockerMonitor(properties, registry, discoveryClient, lockService, dockerRegistryCache, dockerRegistryAccounts, Optional.of(echoService), Optional.empty(), dockerRegistryProperties) + new DockerMonitor(properties, registry, discoveryClient, lockService, dockerRegistryCache, dockerRegistryAccounts, Optional.of(echoService), Optional.of(keelService), Optional.empty(), dockerRegistryProperties) .postEvent(cachedImages, taggedImage, "imageId") then: @@ -102,7 +104,13 @@ class DockerMonitorSpec extends Specification { assert event.artifact.metadata.registry == taggedImage.registry return true }) - + 1 * keelService.sendArtifactEvent({ Map event -> + def artifacts = event.payload.artifacts + assert artifacts.size() == 1 + assert artifacts[0].name == "repository" + assert artifacts[0].type == "DOCKER" + return true + }) } @Unroll @@ -164,7 +172,7 @@ class DockerMonitorSpec extends Specification { } private DockerMonitor createSubject() { - return new DockerMonitor(properties, registry, discoveryClient, lockService, dockerRegistryCache, dockerRegistryAccounts, Optional.of(echoService), keysMigration, dockerRegistryProperties) + return new DockerMonitor(properties, registry, discoveryClient, lockService, dockerRegistryCache, dockerRegistryAccounts, Optional.of(echoService), Optional.of(keelService), keysMigration, dockerRegistryProperties) } private static String keyFromTaggedImage(TaggedImage taggedImage) {