diff --git a/echo-artifacts/echo-artifacts.gradle b/echo-artifacts/echo-artifacts.gradle index 73c310f5c..742dce95d 100644 --- a/echo-artifacts/echo-artifacts.gradle +++ b/echo-artifacts/echo-artifacts.gradle @@ -17,10 +17,10 @@ dependencies { implementation project(':echo-core') implementation project(':echo-model') - implementation "com.squareup.retrofit:retrofit" - implementation "com.squareup.retrofit:converter-jackson" - implementation "com.jakewharton.retrofit:retrofit1-okhttp3-client:1.1.0" + implementation "com.squareup.retrofit2:retrofit" + implementation "com.squareup.retrofit2:converter-jackson" implementation "io.spinnaker.kork:kork-web" implementation "io.spinnaker.kork:kork-artifacts" + implementation "io.spinnaker.kork:kork-retrofit" implementation "org.springframework.boot:spring-boot-starter-web" } diff --git a/echo-artifacts/src/main/java/com/netflix/spinnaker/echo/artifacts/ArtifactEmitter.java b/echo-artifacts/src/main/java/com/netflix/spinnaker/echo/artifacts/ArtifactEmitter.java index 6e06b6575..94e5e8c67 100644 --- a/echo-artifacts/src/main/java/com/netflix/spinnaker/echo/artifacts/ArtifactEmitter.java +++ b/echo-artifacts/src/main/java/com/netflix/spinnaker/echo/artifacts/ArtifactEmitter.java @@ -20,6 +20,7 @@ import com.netflix.spinnaker.echo.config.ArtifactEmitterProperties; import com.netflix.spinnaker.echo.model.ArtifactEvent; import com.netflix.spinnaker.echo.services.KeelService; +import com.netflix.spinnaker.kork.retrofit.Retrofit2SyncCall; import java.util.HashMap; import java.util.Map; import org.slf4j.Logger; @@ -64,7 +65,7 @@ public void processEvent(ArtifactEvent event) { sentEvent.put( artifactEmitterProperties.getFieldName(), objectMapper.convertValue(event, Map.class)); log.debug("Sending artifacts to Keel: {}", event.getArtifacts()); - keelService.sendArtifactEvent(sentEvent); + Retrofit2SyncCall.execute(keelService.sendArtifactEvent(sentEvent)); } catch (Exception e) { log.error("Could not send event {} to Keel", event, e); } diff --git a/echo-artifacts/src/main/java/com/netflix/spinnaker/echo/config/KeelConfig.java b/echo-artifacts/src/main/java/com/netflix/spinnaker/echo/config/KeelConfig.java index 9adb8874f..50a5c1159 100644 --- a/echo-artifacts/src/main/java/com/netflix/spinnaker/echo/config/KeelConfig.java +++ b/echo-artifacts/src/main/java/com/netflix/spinnaker/echo/config/KeelConfig.java @@ -1,47 +1,31 @@ package com.netflix.spinnaker.echo.config; -import com.jakewharton.retrofit.Ok3Client; -import com.netflix.spinnaker.config.DefaultServiceEndpoint; -import com.netflix.spinnaker.config.okhttp3.OkHttpClientProvider; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.netflix.spinnaker.config.OkHttp3ClientConfiguration; import com.netflix.spinnaker.echo.services.KeelService; -import com.netflix.spinnaker.retrofit.Slf4jRetrofitLogger; +import com.netflix.spinnaker.kork.retrofit.ErrorHandlingExecutorCallAdapterFactory; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import retrofit.Endpoint; -import retrofit.Endpoints; -import retrofit.RestAdapter; -import retrofit.RestAdapter.LogLevel; -import retrofit.converter.JacksonConverter; +import retrofit2.Retrofit; +import retrofit2.converter.jackson.JacksonConverterFactory; @Configuration @Slf4j @ConditionalOnExpression("${keel.enabled:false}") public class KeelConfig { - @Bean - public LogLevel retrofitLogLevel(@Value("${retrofit.log-level:BASIC}") String retrofitLogLevel) { - return LogLevel.valueOf(retrofitLogLevel); - } - - @Bean - public Endpoint keelEndpoint(@Value("${keel.base-url}") String keelBaseUrl) { - return Endpoints.newFixedEndpoint(keelBaseUrl); - } @Bean public KeelService keelService( - Endpoint keelEndpoint, OkHttpClientProvider clientProvider, LogLevel retrofitLogLevel) { - return new RestAdapter.Builder() - .setEndpoint(keelEndpoint) - .setConverter(new JacksonConverter()) - .setClient( - new Ok3Client( - clientProvider.getClient( - new DefaultServiceEndpoint("keel", keelEndpoint.getUrl())))) - .setLogLevel(retrofitLogLevel) - .setLog(new Slf4jRetrofitLogger(KeelService.class)) + @Value("${keel.base-url}") String keelBaseUrl, + OkHttp3ClientConfiguration okHttpClientConfig) { + return new Retrofit.Builder() + .baseUrl(keelBaseUrl) + .client(okHttpClientConfig.createForRetrofit2().build()) + .addCallAdapterFactory(ErrorHandlingExecutorCallAdapterFactory.getInstance()) + .addConverterFactory(JacksonConverterFactory.create(new ObjectMapper())) .build() .create(KeelService.class); } diff --git a/echo-artifacts/src/main/java/com/netflix/spinnaker/echo/services/KeelService.java b/echo-artifacts/src/main/java/com/netflix/spinnaker/echo/services/KeelService.java index 946cd839e..17aa13c22 100644 --- a/echo-artifacts/src/main/java/com/netflix/spinnaker/echo/services/KeelService.java +++ b/echo-artifacts/src/main/java/com/netflix/spinnaker/echo/services/KeelService.java @@ -1,10 +1,11 @@ package com.netflix.spinnaker.echo.services; import java.util.Map; -import retrofit.http.Body; -import retrofit.http.POST; +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.POST; public interface KeelService { @POST("/artifacts/events") - Void sendArtifactEvent(@Body Map event); + Call sendArtifactEvent(@Body Map event); } diff --git a/echo-core/echo-core.gradle b/echo-core/echo-core.gradle index f9c2bfe7f..3af68c997 100644 --- a/echo-core/echo-core.gradle +++ b/echo-core/echo-core.gradle @@ -20,15 +20,15 @@ dependencies { api "io.spinnaker.kork:kork-plugins" - implementation "com.squareup.retrofit:retrofit" - implementation "com.squareup.retrofit:converter-jackson" - implementation "com.jakewharton.retrofit:retrofit1-okhttp3-client:1.1.0" + implementation "com.squareup.retrofit2:retrofit" + implementation "com.squareup.retrofit2:converter-jackson" implementation "io.spinnaker.kork:kork-web" implementation "io.spinnaker.kork:kork-artifacts" implementation "io.spinnaker.kork:kork-core" implementation "io.spinnaker.kork:kork-exceptions" implementation "io.spinnaker.kork:kork-security" + implementation "io.spinnaker.kork:kork-retrofit" implementation "org.springframework.boot:spring-boot-starter-web" implementation "org.apache.commons:commons-lang3" @@ -42,4 +42,5 @@ dependencies { testImplementation "org.spockframework:spock-spring" testImplementation "org.springframework:spring-test" testImplementation "org.apache.groovy:groovy-json" + testImplementation "com.github.tomakehurst:wiremock-jre8" } diff --git a/echo-core/src/main/java/com/netflix/spinnaker/echo/artifacts/ArtifactInfoService.java b/echo-core/src/main/java/com/netflix/spinnaker/echo/artifacts/ArtifactInfoService.java index a9af6bc5d..04e779cde 100644 --- a/echo-core/src/main/java/com/netflix/spinnaker/echo/artifacts/ArtifactInfoService.java +++ b/echo-core/src/main/java/com/netflix/spinnaker/echo/artifacts/ArtifactInfoService.java @@ -19,6 +19,7 @@ import com.netflix.spinnaker.echo.services.IgorService; import com.netflix.spinnaker.kork.artifacts.model.Artifact; +import com.netflix.spinnaker.kork.retrofit.Retrofit2SyncCall; import java.util.List; /** Given an artifact, fetch the details from an artifact provider */ @@ -31,10 +32,11 @@ public ArtifactInfoService(IgorService igorService) { } public List getVersions(String provider, String packageName) { - return igorService.getVersions(provider, packageName); + return Retrofit2SyncCall.execute(igorService.getVersions(provider, packageName)); } public Artifact getArtifactByVersion(String provider, String packageName, String version) { - return igorService.getArtifactByVersion(provider, packageName, version); + return Retrofit2SyncCall.execute( + igorService.getArtifactByVersion(provider, packageName, version)); } } diff --git a/echo-core/src/main/java/com/netflix/spinnaker/echo/build/BuildInfoService.java b/echo-core/src/main/java/com/netflix/spinnaker/echo/build/BuildInfoService.java index 63452df9b..f7265e855 100644 --- a/echo-core/src/main/java/com/netflix/spinnaker/echo/build/BuildInfoService.java +++ b/echo-core/src/main/java/com/netflix/spinnaker/echo/build/BuildInfoService.java @@ -25,6 +25,7 @@ import com.netflix.spinnaker.echo.services.IgorService; import com.netflix.spinnaker.kork.artifacts.model.Artifact; import com.netflix.spinnaker.kork.core.RetrySupport; +import com.netflix.spinnaker.kork.retrofit.Retrofit2SyncCall; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -58,8 +59,10 @@ public BuildEvent getBuildEvent(String master, String job, int buildNumber) { Map rawBuild = retry( igorConfigurationProperties.isJobNameAsQueryParameter() - ? () -> igorService.getBuildStatusWithJobQueryParameter(buildNumber, master, job) - : () -> igorService.getBuild(buildNumber, master, job)); + ? () -> + Retrofit2SyncCall.execute( + igorService.getBuildStatusWithJobQueryParameter(buildNumber, master, job)) + : () -> Retrofit2SyncCall.execute(igorService.getBuild(buildNumber, master, job))); BuildEvent.Build build = objectMapper.convertValue(rawBuild, BuildEvent.Build.class); BuildEvent.Project project = new BuildEvent.Project(job, build); BuildEvent.Content content = new BuildEvent.Content(project, master); @@ -77,8 +80,9 @@ public Map getBuildInfo(BuildEvent event) { return retry( () -> igorConfigurationProperties.isJobNameAsQueryParameter() - ? igorService.getBuildStatusWithJobQueryParameter(buildNumber, master, job) - : igorService.getBuild(buildNumber, master, job)); + ? Retrofit2SyncCall.execute( + igorService.getBuildStatusWithJobQueryParameter(buildNumber, master, job)) + : Retrofit2SyncCall.execute(igorService.getBuild(buildNumber, master, job))); } return Collections.emptyMap(); } @@ -96,9 +100,11 @@ public Map getProperties(BuildEvent event, String propertyFile) return retry( () -> igorConfigurationProperties.isJobNameAsQueryParameter() - ? igorService.getPropertyFileWithJobQueryParameter( - buildNumber, propertyFileFinal, master, job) - : igorService.getPropertyFile(buildNumber, propertyFileFinal, master, job)); + ? Retrofit2SyncCall.execute( + igorService.getPropertyFileWithJobQueryParameter( + buildNumber, propertyFileFinal, master, job)) + : Retrofit2SyncCall.execute( + igorService.getPropertyFile(buildNumber, propertyFileFinal, master, job))); } return Collections.emptyMap(); } @@ -111,9 +117,11 @@ private List getArtifactsFromPropertyFile(BuildEvent event, String pro return retry( () -> igorConfigurationProperties.isJobNameAsQueryParameter() - ? igorService.getArtifactsWithJobQueryParameter( - buildNumber, propertyFile, master, job) - : igorService.getArtifacts(buildNumber, propertyFile, master, job)); + ? Retrofit2SyncCall.execute( + igorService.getArtifactsWithJobQueryParameter( + buildNumber, propertyFile, master, job)) + : Retrofit2SyncCall.execute( + igorService.getArtifacts(buildNumber, propertyFile, master, job))); } return Collections.emptyList(); } diff --git a/echo-core/src/main/java/com/netflix/spinnaker/echo/services/Front50Service.java b/echo-core/src/main/java/com/netflix/spinnaker/echo/services/Front50Service.java index 0f20bc703..e2bd3e18c 100644 --- a/echo-core/src/main/java/com/netflix/spinnaker/echo/services/Front50Service.java +++ b/echo-core/src/main/java/com/netflix/spinnaker/echo/services/Front50Service.java @@ -3,17 +3,23 @@ import com.netflix.spinnaker.echo.model.Pipeline; import java.util.List; import java.util.Map; -import retrofit.http.*; +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.GET; +import retrofit2.http.Headers; +import retrofit2.http.POST; +import retrofit2.http.Path; +import retrofit2.http.Query; public interface Front50Service { @GET("/pipelines?restricted=false") @Headers("Accept: application/json") - List> + Call>> getPipelines(); // Return Map here so we don't throw away MPT attributes. @GET("/pipelines?restricted=false") @Headers("Accept: application/json") - List> getPipelines( + Call>> getPipelines( @Query("enabledPipelines") Boolean enabledPipelines, @Query("enabledTriggers") Boolean enabledTriggers, @Query("triggerTypes") @@ -21,16 +27,16 @@ List> getPipelines( @GET("/pipelines/{application}?refresh=false") @Headers("Accept: application/json") - List getPipelines(@Path("application") String application); + Call> getPipelines(@Path("application") String application); @GET("/pipelines/{pipelineId}/get") - Map getPipeline(@Path("pipelineId") String pipelineId); + Call> getPipeline(@Path("pipelineId") String pipelineId); @GET("/pipelines/{application}/name/{name}?refresh=true") - Map getPipelineByName( + Call> getPipelineByName( @Path("application") String application, @Path("name") String name); @POST("/graphql") @Headers("Accept: application/json") - GraphQLQueryResponse query(@Body GraphQLQuery body); + Call query(@Body GraphQLQuery body); } diff --git a/echo-core/src/main/java/com/netflix/spinnaker/echo/services/IgorService.java b/echo-core/src/main/java/com/netflix/spinnaker/echo/services/IgorService.java index 3936d22c7..df1dcc12c 100644 --- a/echo-core/src/main/java/com/netflix/spinnaker/echo/services/IgorService.java +++ b/echo-core/src/main/java/com/netflix/spinnaker/echo/services/IgorService.java @@ -19,70 +19,75 @@ import com.netflix.spinnaker.kork.artifacts.model.Artifact; import java.util.List; import java.util.Map; +import okhttp3.ResponseBody; import org.jetbrains.annotations.NotNull; -import retrofit.client.Response; -import retrofit.http.*; import retrofit.mime.TypedInput; +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.GET; +import retrofit2.http.PUT; +import retrofit2.http.Path; +import retrofit2.http.Query; public interface IgorService { @GET("/builds/status/{buildNumber}/{master}/{job}") - Map getBuild( + Call> getBuild( @Path("buildNumber") Integer buildNumber, @Path("master") String master, - @Path(value = "job", encode = false) String job); + @Path(value = "job", encoded = true) String job); @GET("/builds/status/{buildNumber}/{master}") - Map getBuildStatusWithJobQueryParameter( + Call> getBuildStatusWithJobQueryParameter( @NotNull @Path("buildNumber") Integer buildNumber, @NotNull @Path("master") String master, @NotNull @Query(value = "job") String job); @GET("/builds/properties/{buildNumber}/{fileName}/{master}/{job}") - Map getPropertyFile( + Call> getPropertyFile( @Path("buildNumber") Integer buildNumber, @Path("fileName") String fileName, @Path("master") String master, - @Path(value = "job", encode = false) String job); + @Path(value = "job", encoded = true) String job); @GET("/builds/properties/{buildNumber}/{fileName}/{master}") - Map getPropertyFileWithJobQueryParameter( + Call> getPropertyFileWithJobQueryParameter( @Path("buildNumber") Integer buildNumber, @Path("fileName") String fileName, @Path("master") String master, @Query(value = "job") String job); @GET("/builds/artifacts/{buildNumber}/{master}/{job}") - List getArtifacts( + Call> getArtifacts( @Path("buildNumber") Integer buildNumber, @Query("propertyFile") String propertyFile, @Path("master") String master, - @Path(value = "job", encode = false) String job); + @Path(value = "job", encoded = true) String job); @GET("/builds/artifacts/{buildNumber}/{master}") - List getArtifactsWithJobQueryParameter( + Call> getArtifactsWithJobQueryParameter( @Path("buildNumber") Integer buildNumber, @Query("propertyFile") String propertyFile, @Path("master") String master, @Query(value = "job") String job); @GET("/artifacts/{provider}/{packageName}") - List getVersions( + Call> getVersions( @Path("provider") String provider, @Path("packageName") String packageName); @GET("/artifacts/{provider}/{packageName}/{version}") - Artifact getArtifactByVersion( + Call getArtifactByVersion( @Path("provider") String provider, @Path("packageName") String packageName, @Path("version") String version); @PUT("/gcb/builds/{account}/{buildId}") - Response updateBuildStatus( + Call updateBuildStatus( @Path("account") String account, @Path("buildId") String buildId, @Query("status") String status, @Body TypedInput build); @PUT("/gcb/artifacts/extract/{account}") - List extractGoogleCloudBuildArtifacts( + Call> extractGoogleCloudBuildArtifacts( @Path("account") String account, @Body TypedInput build); } diff --git a/echo-core/src/test/groovy/com/netflix/spinnaker/echo/services/Front50ServiceSpec.groovy b/echo-core/src/test/groovy/com/netflix/spinnaker/echo/services/Front50ServiceSpec.groovy index b563e19aa..cf2abd612 100644 --- a/echo-core/src/test/groovy/com/netflix/spinnaker/echo/services/Front50ServiceSpec.groovy +++ b/echo-core/src/test/groovy/com/netflix/spinnaker/echo/services/Front50ServiceSpec.groovy @@ -1,35 +1,58 @@ package com.netflix.spinnaker.echo.services -import groovy.json.JsonOutput +import com.fasterxml.jackson.databind.ObjectMapper +import com.github.tomakehurst.wiremock.WireMockServer +import com.github.tomakehurst.wiremock.core.WireMockConfiguration +import com.github.tomakehurst.wiremock.client.WireMock +import com.google.common.collect.ImmutableList +import com.netflix.spinnaker.config.DefaultServiceEndpoint +import com.netflix.spinnaker.config.okhttp3.OkHttpClientProvider +import com.netflix.spinnaker.config.okhttp3.InsecureOkHttpClientBuilderProvider import com.netflix.spinnaker.echo.model.Trigger -import retrofit.Endpoints -import retrofit.RestAdapter -import retrofit.converter.JacksonConverter -import spock.lang.Ignore +import com.netflix.spinnaker.kork.retrofit.ErrorHandlingExecutorCallAdapterFactory +import com.netflix.spinnaker.kork.retrofit.Retrofit2SyncCall +import okhttp3.OkHttpClient +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import retrofit2.Retrofit +import retrofit2.converter.jackson.JacksonConverterFactory +import spock.lang.Ignore; import spock.lang.Specification -import spock.lang.Subject -import com.google.common.collect.ImmutableList -import retrofit.client.Client -import retrofit.client.Header -import retrofit.client.Response -import retrofit.mime.TypedString +import spock.util.concurrent.BlockingVariable +@SpringBootTest(classes = [OkHttpClientProvider, InsecureOkHttpClientBuilderProvider, OkHttpClient], + webEnvironment = SpringBootTest.WebEnvironment.NONE) class Front50ServiceSpec extends Specification { - def endpoint = "http://front50-prestaging.prod.netflix.net" - def client = Stub(Client) - @Subject front50 = new RestAdapter.Builder() - .setEndpoint(Endpoints.newFixedEndpoint(endpoint)) - .setConverter(new JacksonConverter()) - .setClient(client) - .build() - .create(Front50Service) + WireMockServer wireMockServer + Front50Service front50Service + + @Autowired + OkHttpClientProvider clientProvider + + BlockingVariable>> pipelineResponse + + ObjectMapper objectMapper = new ObjectMapper() + + def setup() { + pipelineResponse = new BlockingVariable>>(5) + wireMockServer = new WireMockServer(WireMockConfiguration.options().dynamicPort()); + wireMockServer.start(); + } + + def cleanup(){ + wireMockServer.stop() + } def "parses pipelines"() { given: - client.execute(_) >> response(pipelineWithNoTriggers) + front50Service = front50Service(wireMockServer.baseUrl()) + wireMockServer.stubFor(WireMock.get(WireMock.urlEqualTo("/pipelines?restricted=false")) + .willReturn(WireMock.aResponse() + .withBody(objectMapper.writeValueAsString(pipelineWithNoTriggers)) + .withStatus(200))) when: - def pipelines = front50.getPipelines() + def pipelines = Retrofit2SyncCall.execute(front50Service.getPipelines()) then: !pipelines.empty @@ -37,10 +60,14 @@ class Front50ServiceSpec extends Specification { def "handles pipelines with empty triggers array"() { given: - client.execute(_) >> response(pipelineWithNoTriggers) + front50Service = front50Service(wireMockServer.baseUrl()) + wireMockServer.stubFor(WireMock.get(WireMock.urlEqualTo("/pipelines?restricted=false")) + .willReturn(WireMock.aResponse() + .withBody(objectMapper.writeValueAsString(pipelineWithNoTriggers)) + .withStatus(200))) when: - def pipelines = front50.getPipelines() + def pipelines = Retrofit2SyncCall.execute(front50Service.getPipelines()) then: def pipeline = pipelines.first() @@ -49,10 +76,14 @@ class Front50ServiceSpec extends Specification { def "handles pipelines with actual triggers"() { given: - client.execute(_) >> response(pipelineWithJenkinsTrigger) + front50Service = front50Service(wireMockServer.baseUrl()) + wireMockServer.stubFor(WireMock.get(WireMock.urlEqualTo("/pipelines?restricted=false")) + .willReturn(WireMock.aResponse() + .withBody(objectMapper.writeValueAsString(pipelineWithJenkinsTrigger)) + .withStatus(200))) when: - def pipelines = front50.getPipelines() + def pipelines = Retrofit2SyncCall.execute(front50Service.getPipelines()) then: def pipeline = pipelines.find { it.application == "rush" && it.name == "bob the sinner" } @@ -68,10 +99,14 @@ class Front50ServiceSpec extends Specification { def "handles parallel pipelines"() { given: - client.execute(_) >> response(parallelPipeline) + front50Service = front50Service(wireMockServer.baseUrl()) + wireMockServer.stubFor(WireMock.get(WireMock.urlEqualTo("/pipelines?restricted=false")) + .willReturn(WireMock.aResponse() + .withBody(objectMapper.writeValueAsString(parallelPipeline)) + .withStatus(200))) when: - def pipelines = front50.getPipelines() + def pipelines = Retrofit2SyncCall.execute(front50Service.getPipelines()) then: pipelines.first().parallel @@ -80,7 +115,7 @@ class Front50ServiceSpec extends Specification { @Ignore def "list properties are immutable"() { given: - def pipelines = front50.getPipelines() + def pipelines = front50Service.getPipelines() def pipeline = pipelines.find { it.application == "kato" } expect: @@ -93,171 +128,183 @@ class Front50ServiceSpec extends Specification { thrown UnsupportedOperationException } - private Response response(Map... pipelines) { - new Response("", 200, "OK", [new Header("Content-Type", "application/json")], new TypedString(JsonOutput.toJson(pipelines))) + private Front50Service front50Service(String baseUrl){ + new Retrofit.Builder() + .baseUrl(baseUrl) + .client(clientProvider.getClient(new DefaultServiceEndpoint("front50", baseUrl, false))) + .addCallAdapterFactory(ErrorHandlingExecutorCallAdapterFactory.getInstance()) + .addConverterFactory(JacksonConverterFactory.create()) + .build() + .create(Front50Service.class); } def pipelineWithNoTriggers = [ - name : "healthCheck", - stages : [ - [ - type : "wait", - name : "Wait", - waitTime: 10 - ] - ], - triggers : [], - application: "spindemo", - index : 0, - id : "58b700a0-ed12-11e4-a8b3-f5bd0633341b" + [ + name : "healthCheck", + stages : [ + [ + type : "wait", + name : "Wait", + waitTime: 10 + ] + ], + triggers : [], + application: "spindemo", + index : 0, + id : "58b700a0-ed12-11e4-a8b3-f5bd0633341b" + ] ] def pipelineWithJenkinsTrigger = [ - "name" : "bob the sinner", - "stages" : [ - [ - "isNew" : true, - "type" : "findAmi", - "name" : "Find AMI", - "master" : "spinnaker", - "job" : "Dummy_test_job_2", - "propertyFile" : "deb.properties", - "parameters" : [ - "apiDeb" : "\${trigger.properties.apiDeb}", - "newValue": "somthing else" - ], - "selectionStrategy": "NEWEST", - "onlyEnabled" : true, - "cluster" : "rush-main", - "account" : "prod", - "regions" : ["us-west-1"] - ] - ], - "triggers" : [ - [ - "enabled" : true, - "type" : "jenkins", - "master" : "spinnaker", - "job" : "Dummy_test_job", - "propertyFile": "deb.properties" - ] - ], - "application": "rush", - "appConfig" : null, - "index" : 1 + [ + "name" : "bob the sinner", + "stages" : [ + [ + "isNew" : true, + "type" : "findAmi", + "name" : "Find AMI", + "master" : "spinnaker", + "job" : "Dummy_test_job_2", + "propertyFile" : "deb.properties", + "parameters" : [ + "apiDeb" : "\${trigger.properties.apiDeb}", + "newValue": "somthing else" + ], + "selectionStrategy": "NEWEST", + "onlyEnabled" : true, + "cluster" : "rush-main", + "account" : "prod", + "regions" : ["us-west-1"] + ] + ], + "triggers" : [ + [ + "enabled" : true, + "type" : "jenkins", + "master" : "spinnaker", + "job" : "Dummy_test_job", + "propertyFile": "deb.properties" + ] + ], + "application": "rush", + "appConfig" : null, + "index" : 1 + ] ] def parallelPipeline = [ - name : "DZ parallel pipeline", - stages : [ - [ - type : "bake", - name : "Bake", - regions : [ - "us-east-1", - "us-west-2", - "eu-west-1" + [ + name : "DZ parallel pipeline", + stages : [ + [ + type : "bake", + name : "Bake", + regions : [ + "us-east-1", + "us-west-2", + "eu-west-1" + ], + user : "dzapata@netflix.com", + baseOs : "trusty", + baseLabel : "release", + vmType : "pv", + storeType : "ebs", + package : "api", + refId : "1", + requisiteStageRefIds: [] ], - user : "dzapata@netflix.com", - baseOs : "trusty", - baseLabel : "release", - vmType : "pv", - storeType : "ebs", - package : "api", - refId : "1", - requisiteStageRefIds: [] - ], - [ - type : "quickPatch", - name : "Quick Patch ASG", - application : "api", - healthProviders : [ - "Discovery" + [ + type : "quickPatch", + name : "Quick Patch ASG", + application : "api", + healthProviders : [ + "Discovery" + ], + account : "prod", + credentials : "prod", + region : "us-east-1", + clusterName : "api-ci-dzapata", + package : "api", + baseOs : "ubuntu", + refId : "2", + requisiteStageRefIds: [] ], - account : "prod", - credentials : "prod", - region : "us-east-1", - clusterName : "api-ci-dzapata", - package : "api", - baseOs : "ubuntu", - refId : "2", - requisiteStageRefIds: [] - ], - [ - requisiteStageRefIds: [ - "2" + [ + requisiteStageRefIds: [ + "2" + ], + refId : "3", + type : "jenkins", + name : "Smoke Test", + master : "edge", + job : "Edge-DZ-Smoke-Test", + parameters : [] ], - refId : "3", - type : "jenkins", - name : "Smoke Test", - master : "edge", - job : "Edge-DZ-Smoke-Test", - parameters : [] - ], - [ - requisiteStageRefIds: [ - "3", - "1" - ], - refId : "4", - type : "deploy", - name : "Deploy", - clusters : [ - [ - application : "api", - strategy : "redblack", - stack : "sandbox", - freeFormDetails : "dzapata", - cooldown : 10, - healthCheckGracePeriod: 600, - healthCheckType : "EC2", - terminationPolicies : [ - "Default" - ], - loadBalancers : [], - capacity : [ - min : 1, - max : 1, - desired: 1 - ], - availabilityZones : [ - "us-east-1": [ - "us-east-1c", - "us-east-1d", - "us-east-1e" - ] - ], - suspendedProcesses : [], - instanceType : "m2.4xlarge", - iamRole : "BaseIAMRole", - keyPair : "nf-prod-keypair-a", - instanceMonitoring : false, - ebsOptimized : false, - securityGroups : [ - "sg-31cd0758", - "sg-42c0132b", - "sg-ae9a5ec7", - "sg-d8e330b1" - ], - maxRemainingAsgs : 2, - provider : "aws", - account : "prod" + [ + requisiteStageRefIds: [ + "3", + "1" + ], + refId : "4", + type : "deploy", + name : "Deploy", + clusters : [ + [ + application : "api", + strategy : "redblack", + stack : "sandbox", + freeFormDetails : "dzapata", + cooldown : 10, + healthCheckGracePeriod: 600, + healthCheckType : "EC2", + terminationPolicies : [ + "Default" + ], + loadBalancers : [], + capacity : [ + min : 1, + max : 1, + desired: 1 + ], + availabilityZones : [ + "us-east-1": [ + "us-east-1c", + "us-east-1d", + "us-east-1e" + ] + ], + suspendedProcesses : [], + instanceType : "m2.4xlarge", + iamRole : "BaseIAMRole", + keyPair : "nf-prod-keypair-a", + instanceMonitoring : false, + ebsOptimized : false, + securityGroups : [ + "sg-31cd0758", + "sg-42c0132b", + "sg-ae9a5ec7", + "sg-d8e330b1" + ], + maxRemainingAsgs : 2, + provider : "aws", + account : "prod" + ] ] ] - ] - ], - triggers : [ - [ - enabled: true, - type : "jenkins", - master : "edge", - job : "EDGE-DZ-Branch-Build" - ] - ], - application : "api", - index : 0, - id : "ed5ed000-f412-11e4-a8b3-f5bd0633341b", - stageCounter: 4, - parallel : true + ], + triggers : [ + [ + enabled: true, + type : "jenkins", + master : "edge", + job : "EDGE-DZ-Branch-Build" + ] + ], + application : "api", + index : 0, + id : "ed5ed000-f412-11e4-a8b3-f5bd0633341b", + stageCounter: 4, + parallel : true + ] ] } diff --git a/echo-notifications/echo-notifications.gradle b/echo-notifications/echo-notifications.gradle index db7d87370..2eb195600 100644 --- a/echo-notifications/echo-notifications.gradle +++ b/echo-notifications/echo-notifications.gradle @@ -19,11 +19,11 @@ dependencies { implementation project(':echo-model') implementation project(":echo-pipelinetriggers") implementation "org.springframework.boot:spring-boot-starter-web" - implementation "com.squareup.retrofit:retrofit" - implementation "com.squareup.retrofit:converter-jackson" - implementation "com.jakewharton.retrofit:retrofit1-okhttp3-client:1.1.0" + implementation "com.squareup.retrofit2:retrofit" + implementation "com.squareup.retrofit2:converter-jackson" implementation "io.spinnaker.kork:kork-core" implementation "io.spinnaker.kork:kork-artifacts" + implementation "io.spinnaker.kork:kork-retrofit" implementation "io.spinnaker.kork:kork-web" implementation "commons-codec:commons-codec" implementation "org.springframework.boot:spring-boot-starter-mail" @@ -34,11 +34,21 @@ dependencies { implementation "io.cloudevents:cloudevents-http-basic:3.0.0" implementation "io.cloudevents:cloudevents-json-jackson:3.0.0" implementation ("dev.cdevents:cdevents-sdk-java:0.3.1") + testImplementation project(":echo-test") testImplementation("com.icegreen:greenmail:1.5.14") { exclude group: "com.sun.mail", module: "javax.mail" } testImplementation "org.apache.httpcomponents:httpclient" testImplementation "org.spockframework:spock-spring" testImplementation "org.springframework:spring-test" + testImplementation "com.squareup.retrofit2:retrofit-mock" + testImplementation "com.github.tomakehurst:wiremock-jre8" annotationProcessor "org.springframework.boot:spring-boot-configuration-processor" } + +sourceSets { + main { + java { srcDirs = [] } // no source dirs for the java compiler + groovy { srcDirs = ["src/main/java", "src/main/groovy"] } // compile everything in src/ with groovy + } +} diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/bearychat/BearychatService.groovy b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/bearychat/BearychatService.groovy index 89ce7db48..6e3339c3e 100644 --- a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/bearychat/BearychatService.groovy +++ b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/bearychat/BearychatService.groovy @@ -16,22 +16,24 @@ package com.netflix.spinnaker.echo.bearychat -import retrofit.http.Body -import retrofit.http.GET -import retrofit.http.POST -import retrofit.client.Response -import retrofit.http.Query +import okhttp3.ResponseBody +import retrofit2.Call +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Query interface BearychatService { @GET("/v1/user.list") - List getUserList(@Query("token") String token) + Call> getUserList(@Query("token") String token) @POST("/v1/p2p.create") - CreateP2PChannelResponse createp2pchannel(@Query("token") String token, + Call createp2pchannel(@Query("token") String token, @Body CreateP2PChannelPara para) @POST("/v1/message.create") - Response sendMessage(@Query("token") String token, - @Body SendMessagePara para) + Call> sendMessage(@Query("token") String token, + @Body SendMessagePara para) } diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/cdevents/CDEventsHTTPMessageConverter.java b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/cdevents/CDEventsHTTPMessageConverter.java deleted file mode 100644 index 5cb251560..000000000 --- a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/cdevents/CDEventsHTTPMessageConverter.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - Copyright (C) 2023 Nordix Foundation. - For a full list of individual contributors, please see the commit history. - 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. - - SPDX-License-Identifier: Apache-2.0 -*/ - -package com.netflix.spinnaker.echo.cdevents; - -import static com.fasterxml.jackson.databind.SerializationFeature.FAIL_ON_EMPTY_BEANS; - -import com.fasterxml.jackson.core.JsonParseException; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JavaType; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.netflix.spinnaker.kork.web.exceptions.InvalidRequestException; -import io.cloudevents.CloudEvent; -import io.cloudevents.jackson.JsonFormat; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.lang.reflect.Type; -import org.springframework.http.MediaType; -import retrofit.converter.ConversionException; -import retrofit.converter.Converter; -import retrofit.mime.TypedByteArray; -import retrofit.mime.TypedInput; -import retrofit.mime.TypedOutput; - -public class CDEventsHTTPMessageConverter implements Converter { - - private final ObjectMapper objectMapper; - - public CDEventsHTTPMessageConverter(ObjectMapper objectMapper) { - this.objectMapper = objectMapper; - } - - public static CDEventsHTTPMessageConverter create() { - ObjectMapper objectMapper = new ObjectMapper(); - objectMapper.registerModule(JsonFormat.getCloudEventJacksonModule()); - objectMapper.disable(FAIL_ON_EMPTY_BEANS); - return new CDEventsHTTPMessageConverter(objectMapper); - } - - public String convertCDEventToJson(CloudEvent cdEvent) { - try { - return objectMapper.writeValueAsString(cdEvent); - } catch (JsonProcessingException e) { - throw new InvalidRequestException("Unable to convert CDEvent to Json format.", e); - } - } - - @Override - public Object fromBody(TypedInput body, Type type) throws ConversionException { - try { - JavaType javaType = objectMapper.getTypeFactory().constructType(type); - return objectMapper.readValue(body.in(), javaType); - } catch (JsonParseException | JsonMappingException e) { - throw new ConversionException(e); - } catch (IOException e) { - throw new ConversionException(e); - } - } - - @Override - public TypedOutput toBody(Object object) { - try { - String json = objectMapper.writeValueAsString(object); - return new TypedByteArray(MediaType.APPLICATION_JSON_VALUE, json.getBytes("UTF-8")); - } catch (JsonProcessingException e) { - throw new AssertionError(e); - } catch (UnsupportedEncodingException e) { - throw new AssertionError(e); - } - } -} diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/config/BearychatConfig.groovy b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/config/BearychatConfig.groovy index 94927b792..8f72fb8ab 100644 --- a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/config/BearychatConfig.groovy +++ b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/config/BearychatConfig.groovy @@ -16,19 +16,17 @@ package com.netflix.spinnaker.echo.config +import com.netflix.spinnaker.config.OkHttp3ClientConfiguration import com.netflix.spinnaker.echo.bearychat.BearychatService -import com.netflix.spinnaker.retrofit.Slf4jRetrofitLogger -import retrofit.converter.JacksonConverter +import com.netflix.spinnaker.kork.retrofit.ErrorHandlingExecutorCallAdapterFactory +import retrofit2.Retrofit +import retrofit2.converter.jackson.JacksonConverterFactory -import static retrofit.Endpoints.newFixedEndpoint import groovy.transform.CompileStatic import groovy.util.logging.Slf4j import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration -import retrofit.Endpoint -import retrofit.RestAdapter -import retrofit.client.Client @Configuration @ConditionalOnProperty('bearychat.enabled') @@ -37,22 +35,16 @@ import retrofit.client.Client class BearychatConfig { final static String BEARYCHAT_BASE_URL = 'https://api.bearychat.com' - @Bean - Endpoint bearychatEndpoint() { - String endpoint = BEARYCHAT_BASE_URL - newFixedEndpoint(endpoint) - } @Bean - BearychatService bearychatService(Endpoint bearychatEndpoint, Client retrofitClient, RestAdapter.LogLevel retrofitLogLevel) { - log.info('bearchat service loaded') + BearychatService bearychatService(OkHttp3ClientConfiguration okHttpClientConfig) { + log.info('bearychat service loaded') - new RestAdapter.Builder() - .setEndpoint(bearychatEndpoint) - .setConverter(new JacksonConverter()) - .setClient(retrofitClient) - .setLogLevel(retrofitLogLevel) - .setLog(new Slf4jRetrofitLogger(BearychatService.class)) + new Retrofit.Builder() + .baseUrl(BEARYCHAT_BASE_URL) + .client(okHttpClientConfig.createForRetrofit2().build()) + .addCallAdapterFactory(ErrorHandlingExecutorCallAdapterFactory.getInstance()) + .addConverterFactory(JacksonConverterFactory.create()) .build() .create(BearychatService.class) } diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/config/GoogleChatConfig.groovy b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/config/GoogleChatConfig.groovy index a252389b1..f223e5ba6 100644 --- a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/config/GoogleChatConfig.groovy +++ b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/config/GoogleChatConfig.groovy @@ -16,20 +16,17 @@ package com.netflix.spinnaker.echo.config +import com.netflix.spinnaker.config.OkHttp3ClientConfiguration import com.netflix.spinnaker.echo.googlechat.GoogleChatService import com.netflix.spinnaker.echo.googlechat.GoogleChatClient -import com.netflix.spinnaker.retrofit.Slf4jRetrofitLogger +import com.netflix.spinnaker.kork.retrofit.ErrorHandlingExecutorCallAdapterFactory import groovy.transform.CompileStatic import groovy.util.logging.Slf4j import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration -import retrofit.Endpoint -import retrofit.RestAdapter -import retrofit.client.Client -import retrofit.converter.JacksonConverter - -import static retrofit.Endpoints.newFixedEndpoint +import retrofit2.Retrofit +import retrofit2.converter.jackson.JacksonConverterFactory @Configuration @ConditionalOnProperty('googlechat.enabled') @@ -37,22 +34,18 @@ import static retrofit.Endpoints.newFixedEndpoint @CompileStatic class GoogleChatConfig { - @Bean - Endpoint chatEndpoint() { - newFixedEndpoint("https://chat.googleapis.com") - } + private String baseUrl = "https://chat.googleapis.com" @Bean - GoogleChatService chatService(Endpoint chatEndpoint, Client retrofitClient, RestAdapter.LogLevel retrofitLogLevel) { + GoogleChatService chatService(OkHttp3ClientConfiguration okHttpClientConfig) { log.info("Chat service loaded"); - def chatClient = new RestAdapter.Builder() - .setConverter(new JacksonConverter()) - .setClient(retrofitClient) - .setEndpoint(chatEndpoint) - .setLogLevel(retrofitLogLevel) - .setLog(new Slf4jRetrofitLogger(GoogleChatClient.class)) + def chatClient = new Retrofit.Builder() + .baseUrl(baseUrl) + .client(okHttpClientConfig.createForRetrofit2().build()) + .addCallAdapterFactory(ErrorHandlingExecutorCallAdapterFactory.getInstance()) + .addConverterFactory(JacksonConverterFactory.create()) .build() .create(GoogleChatClient.class) diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/config/SlackConfig.groovy b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/config/SlackConfig.groovy index 9ab28b55c..4a2c0a1db 100644 --- a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/config/SlackConfig.groovy +++ b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/config/SlackConfig.groovy @@ -17,10 +17,11 @@ package com.netflix.spinnaker.echo.config +import com.netflix.spinnaker.config.OkHttp3ClientConfiguration import com.netflix.spinnaker.echo.slack.SlackAppService import com.netflix.spinnaker.echo.slack.SlackClient import com.netflix.spinnaker.echo.slack.SlackService -import com.netflix.spinnaker.retrofit.Slf4jRetrofitLogger +import com.netflix.spinnaker.kork.retrofit.ErrorHandlingExecutorCallAdapterFactory import groovy.transform.CompileStatic import groovy.util.logging.Slf4j import org.springframework.beans.factory.annotation.Qualifier @@ -28,12 +29,9 @@ 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.Endpoint -import retrofit.RestAdapter -import retrofit.client.Client -import retrofit.converter.JacksonConverter +import retrofit2.Retrofit +import retrofit2.converter.jackson.JacksonConverterFactory -import static retrofit.Endpoints.newFixedEndpoint @Configuration @ConditionalOnProperty('slack.enabled') @@ -51,20 +49,17 @@ class SlackConfig { @Bean @Qualifier("slackLegacyService") SlackService slackService(@Qualifier("slackLegacyConfig") SlackLegacyProperties config, - Client retrofitClient, - RestAdapter.LogLevel retrofitLogLevel) { + OkHttp3ClientConfiguration okHttpClientConfig) { - Endpoint slackEndpoint = newFixedEndpoint(config.baseUrl) log.info("Using Slack {}: {}.", config.useIncomingWebhook ? "incoming webhook" : "chat api", config.baseUrl) - def slackClient = new RestAdapter.Builder() - .setEndpoint(slackEndpoint) - .setConverter(new JacksonConverter()) - .setClient(retrofitClient) - .setLogLevel(retrofitLogLevel) - .setLog(new Slf4jRetrofitLogger(SlackClient.class)) + def slackClient = new Retrofit.Builder() + .baseUrl(config.baseUrl) + .client(okHttpClientConfig.createForRetrofit2().build()) + .addCallAdapterFactory(ErrorHandlingExecutorCallAdapterFactory.getInstance()) + .addConverterFactory(JacksonConverterFactory.create()) .build() - .create(SlackClient.class) + .create(SlackClient.class); log.info("Slack legacy service loaded") new SlackService(slackClient, config) @@ -82,16 +77,14 @@ class SlackConfig { @Bean @Qualifier("slackAppService") SlackAppService slackAppService(@Qualifier("slackAppConfig") SlackAppProperties config, - Client retrofitClient, - RestAdapter.LogLevel retrofitLogLevel) { - def slackClient = new RestAdapter.Builder() - .setEndpoint(newFixedEndpoint(config.baseUrl)) - .setClient(retrofitClient) - .setLogLevel(retrofitLogLevel) - .setLog(new Slf4jRetrofitLogger(SlackClient.class)) - .setConverter(new JacksonConverter()) + OkHttp3ClientConfiguration okHttpClientConfig) { + def slackClient = new Retrofit.Builder() + .baseUrl(config.baseUrl) + .client(okHttpClientConfig.createForRetrofit2().build()) + .addCallAdapterFactory(ErrorHandlingExecutorCallAdapterFactory.getInstance()) + .addConverterFactory(JacksonConverterFactory.create()) .build() - .create(SlackClient.class) + .create(SlackClient.class); log.info("Slack app service loaded") new SlackAppService(slackClient, config) diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/config/TwilioConfig.groovy b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/config/TwilioConfig.groovy index 67815a88a..e65ab5a83 100644 --- a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/config/TwilioConfig.groovy +++ b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/config/TwilioConfig.groovy @@ -16,25 +16,22 @@ package com.netflix.spinnaker.echo.config +import com.netflix.spinnaker.config.OkHttp3ClientConfiguration import com.netflix.spinnaker.echo.jackson.EchoObjectMapper -import com.netflix.spinnaker.retrofit.Slf4jRetrofitLogger - -import static retrofit.Endpoints.newFixedEndpoint - -import com.fasterxml.jackson.databind.ObjectMapper import com.netflix.spinnaker.echo.twilio.TwilioService +import com.netflix.spinnaker.kork.retrofit.ErrorHandlingExecutorCallAdapterFactory import groovy.transform.CompileStatic import groovy.util.logging.Slf4j +import okhttp3.Interceptor +import okhttp3.Request +import okhttp3.Response import org.apache.commons.codec.binary.Base64 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.RequestInterceptor -import retrofit.RestAdapter -import retrofit.client.Client -import retrofit.converter.JacksonConverter +import retrofit2.Retrofit +import retrofit2.converter.jackson.JacksonConverterFactory @Configuration @ConditionalOnProperty('twilio.enabled') @@ -42,40 +39,44 @@ import retrofit.converter.JacksonConverter @CompileStatic class TwilioConfig { - @Bean - Endpoint twilioEndpoint(@Value('${twilio.base-url:https://api.twilio.com/}') String twilioBaseUrl) { - newFixedEndpoint(twilioBaseUrl) - } - @Bean TwilioService twilioService( - @Value('${twilio.account}') String username, - @Value('${twilio.token}') String password, - Endpoint twilioEndpoint, - Client retrofitClient, - RestAdapter.LogLevel retrofitLogLevel) { + @Value('${twilio.account}') String username, + @Value('${twilio.token}') String password, + @Value('${twilio.base-url:https://api.twilio.com/}') String twilioBaseUrl, + OkHttp3ClientConfiguration okHttpClientConfig) { log.info('twilio service loaded') - RequestInterceptor authInterceptor = new RequestInterceptor() { - @Override - public void intercept(RequestInterceptor.RequestFacade request) { - String auth = "Basic " + Base64.encodeBase64String("${username}:${password}".getBytes()) - request.addHeader("Authorization", auth) - } - } - - JacksonConverter converter = new JacksonConverter(EchoObjectMapper.getInstance()) + String auth = "Basic " + Base64.encodeBase64String("${username}:${password}".getBytes()) + BasicAuthRequestInterceptor interceptor = new BasicAuthRequestInterceptor(auth); - new RestAdapter.Builder() - .setEndpoint(twilioEndpoint) - .setRequestInterceptor(authInterceptor) - .setClient(retrofitClient) - .setLogLevel(retrofitLogLevel) - .setLog(new Slf4jRetrofitLogger(TwilioService.class)) - .setConverter(converter) + new Retrofit.Builder() + .baseUrl(twilioBaseUrl) + .client(okHttpClientConfig.createForRetrofit2().addInterceptor(interceptor).build()) + .addCallAdapterFactory(ErrorHandlingExecutorCallAdapterFactory.getInstance()) + .addConverterFactory(JacksonConverterFactory.create(EchoObjectMapper.getInstance())) .build() - .create(TwilioService.class) + .create(TwilioService.class); } + private static class BasicAuthRequestInterceptor implements Interceptor { + + private final String basic + + BasicAuthRequestInterceptor(String basic) { + this.basic = basic + } + + @Override + Response intercept(Chain chain) throws IOException { + Request request = + chain + .request() + .newBuilder() + .addHeader("Authorization", basic) + .build() + return chain.proceed(request) + } + } } diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/BearychatNotificationAgent.groovy b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/BearychatNotificationAgent.groovy index 2e04b7dce..3ebf9fa54 100644 --- a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/BearychatNotificationAgent.groovy +++ b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/BearychatNotificationAgent.groovy @@ -22,6 +22,7 @@ import com.netflix.spinnaker.echo.bearychat.CreateP2PChannelPara import com.netflix.spinnaker.echo.bearychat.CreateP2PChannelResponse import com.netflix.spinnaker.echo.bearychat.SendMessagePara import com.netflix.spinnaker.echo.api.events.Event +import com.netflix.spinnaker.kork.retrofit.Retrofit2SyncCall import groovy.util.logging.Slf4j import org.apache.commons.lang3.text.WordUtils import org.springframework.beans.factory.annotation.Autowired @@ -84,12 +85,12 @@ class BearychatNotificationAgent extends AbstractEventNotificationAgent { .replace("{{link}}", link ?: "") } - List userList = bearychatService.getUserList(token) + List userList = Retrofit2SyncCall.execute(bearychatService.getUserList(token)) String userid = userList.find {it.email == preference.address}.id - CreateP2PChannelResponse channelInfo = bearychatService.createp2pchannel(token,new CreateP2PChannelPara(user_id: userid)) + CreateP2PChannelResponse channelInfo = Retrofit2SyncCall.execute(bearychatService.createp2pchannel(token,new CreateP2PChannelPara(user_id: userid))) String channelId = channelInfo.vchannel_id - bearychatService.sendMessage(token,new SendMessagePara(vchannel_id: channelId, + Retrofit2SyncCall.execute(bearychatService.sendMessage(token,new SendMessagePara(vchannel_id: channelId, text: message, - attachments: "" )) + attachments: "" ))) } } diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/InteractiveNotificationCallbackHandler.groovy b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/InteractiveNotificationCallbackHandler.groovy index c8bd8edb8..449f1e64c 100644 --- a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/InteractiveNotificationCallbackHandler.groovy +++ b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/InteractiveNotificationCallbackHandler.groovy @@ -16,14 +16,14 @@ package com.netflix.spinnaker.echo.notification; -import com.jakewharton.retrofit.Ok3Client -import com.netflix.spinnaker.config.DefaultServiceEndpoint -import com.netflix.spinnaker.config.okhttp3.OkHttpClientProvider; +import com.netflix.spinnaker.config.OkHttp3ClientConfiguration import com.netflix.spinnaker.echo.api.Notification; -import com.netflix.spinnaker.echo.api.Notification.InteractiveActionCallback; +import com.netflix.spinnaker.echo.api.Notification.InteractiveActionCallback +import com.netflix.spinnaker.kork.retrofit.ErrorHandlingExecutorCallAdapterFactory; +import com.netflix.spinnaker.kork.retrofit.Retrofit2SyncCall import com.netflix.spinnaker.kork.web.exceptions.InvalidRequestException; import com.netflix.spinnaker.kork.web.exceptions.NotFoundException; -import com.netflix.spinnaker.retrofit.Slf4jRetrofitLogger; +import okhttp3.ResponseBody; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -32,14 +32,12 @@ import org.springframework.http.HttpStatus; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component -import retrofit.Endpoint; -import retrofit.Endpoints; -import retrofit.RestAdapter; -import retrofit.client.Response; -import retrofit.converter.JacksonConverter; -import retrofit.http.Body; -import retrofit.http.Header; -import retrofit.http.POST; +import retrofit2.http.Body; +import retrofit2.http.Header; +import retrofit2.http.POST +import retrofit2.Call +import retrofit2.Retrofit +import retrofit2.converter.jackson.JacksonConverterFactory; /** * Implements the flow of interactive notification processing as described in {@link InteractiveNotificationService}. @@ -48,18 +46,18 @@ import retrofit.http.POST; class InteractiveNotificationCallbackHandler { private final Logger log = LoggerFactory.getLogger(InteractiveNotificationCallbackHandler.class); - private OkHttpClientProvider clientProvider; + private OkHttp3ClientConfiguration okHttp3ClientConfiguration private List notificationServices; private Environment environment; private Map spinnakerServices = new HashMap<>(); @Autowired InteractiveNotificationCallbackHandler( - OkHttpClientProvider clientProvider, + OkHttp3ClientConfiguration okHttp3ClientConfiguration, List notificationServices, Environment environment ) { - this.clientProvider = clientProvider; + this.okHttp3ClientConfiguration = okHttp3ClientConfiguration; this.notificationServices = notificationServices; this.environment = environment; } @@ -99,9 +97,9 @@ class InteractiveNotificationCallbackHandler { // TODO(lfp): error handling (retries?). I'd like to respond to the message in a thread, but // have been unable to make that work. Troubleshooting with Slack support. - // TODO(lfp): need to retrieve user's acccounts to pass in X-SPINNAKER-ACCOUNTS - final Response response = spinnakerService.notificationCallback(callback, callback.getUser()); - log.debug("Received callback response from downstream Spinnaker service: " + response.toString()); + // TODO(lfp): need to retrieve user's accounts to pass in X-SPINNAKER-ACCOUNTS + final ResponseBody response = Retrofit2SyncCall.execute(spinnakerService.notificationCallback(callback, callback.getUser())); + log.debug("Received callback response from downstream Spinnaker service: " + response.string()); // Allows the notification service implementation to respond to the callback as needed Optional> outwardResponse = @@ -125,24 +123,15 @@ class InteractiveNotificationCallbackHandler { "Base URL for service " + serviceId + " not found in the configuration."); } - Endpoint endpoint = Endpoints.newFixedEndpoint(baseUrl) - spinnakerServices.put( serviceId, - new RestAdapter.Builder() - .setEndpoint(endpoint) - .setClient( - new Ok3Client( - clientProvider.getClient( - new DefaultServiceEndpoint(serviceId, endpoint.getUrl()) - ) - ) - ) - .setLogLevel(RestAdapter.LogLevel.BASIC) - .setLog(new Slf4jRetrofitLogger(SpinnakerService.class)) - .setConverter(new JacksonConverter()) - .build() - .create(SpinnakerService.class) + new Retrofit.Builder() + .baseUrl(baseUrl) + .client(okHttp3ClientConfiguration.createForRetrofit2().build()) + .addCallAdapterFactory(ErrorHandlingExecutorCallAdapterFactory.getInstance()) + .addConverterFactory(JacksonConverterFactory.create()) + .build() + .create(SpinnakerService.class) ); } @@ -151,7 +140,7 @@ class InteractiveNotificationCallbackHandler { interface SpinnakerService { @POST("/notifications/callback") - Response notificationCallback( + Call notificationCallback( @Body InteractiveActionCallback callback, @Header("X-SPINNAKER-USER") String user); } } diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/TwilioNotificationAgent.groovy b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/TwilioNotificationAgent.groovy index 2595cbae6..5b4c45f62 100644 --- a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/TwilioNotificationAgent.groovy +++ b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/TwilioNotificationAgent.groovy @@ -18,6 +18,7 @@ package com.netflix.spinnaker.echo.notification import com.netflix.spinnaker.echo.api.events.Event import com.netflix.spinnaker.echo.twilio.TwilioService +import com.netflix.spinnaker.kork.retrofit.Retrofit2SyncCall import groovy.util.logging.Slf4j import org.apache.commons.lang3.text.WordUtils import org.springframework.beans.factory.annotation.Autowired @@ -70,12 +71,12 @@ class TwilioNotificationAgent extends AbstractEventNotificationAgent { status == 'complete' ? 'completed successfully' : status } ${link}""" - twilioService.sendMessage( + Retrofit2SyncCall.execute(twilioService.sendMessage( account, from, preference.address, message - ) + )) } @Override diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/pagerduty/PagerDutyNotificationService.groovy b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/pagerduty/PagerDutyNotificationService.groovy index 2a13a4089..f692aa929 100644 --- a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/pagerduty/PagerDutyNotificationService.groovy +++ b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/pagerduty/PagerDutyNotificationService.groovy @@ -19,9 +19,13 @@ package com.netflix.spinnaker.echo.pagerduty import com.fasterxml.jackson.databind.ObjectMapper import com.netflix.spinnaker.echo.api.Notification +import com.netflix.spinnaker.echo.config.PagerDutyConfigurationProperties import com.netflix.spinnaker.echo.controller.EchoResponse import com.netflix.spinnaker.echo.notification.NotificationService import com.netflix.spinnaker.echo.services.Front50Service +import com.netflix.spinnaker.kork.retrofit.Retrofit2SyncCall +import com.netflix.spinnaker.kork.retrofit.exceptions.SpinnakerHttpException +import com.netflix.spinnaker.kork.retrofit.exceptions.SpinnakerServerException import groovy.transform.InheritConstructors import groovy.util.logging.Slf4j import org.springframework.beans.factory.annotation.Autowired @@ -30,9 +34,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty import org.springframework.http.HttpStatus import org.springframework.stereotype.Component import org.springframework.web.bind.annotation.ResponseStatus -import retrofit.RetrofitError -import retrofit.mime.TypedByteArray -import retrofit.mime.TypedInput import static net.logstash.logback.argument.StructuredArguments.kv @@ -47,8 +48,8 @@ class PagerDutyNotificationService implements NotificationService { @Autowired PagerDutyService pagerDuty - @Value('${pager-duty.token:}') - String token + @Autowired + PagerDutyConfigurationProperties pagerDutyConfigurationProperties @Autowired Front50Service front50Service @@ -65,15 +66,15 @@ class PagerDutyNotificationService implements NotificationService { notification.to.each { serviceKey -> try { - Map response = pagerDuty.createEvent( - "Token token=${token}", + Map response = Retrofit2SyncCall.execute(pagerDuty.createEvent( + "Token token=${pagerDutyConfigurationProperties.token}", new PagerDutyService.PagerDutyCreateEvent( service_key: serviceKey, client: "Spinnaker (${notification.source.user})", description: notification.additionalContext.message, details: notification.additionalContext.details as Map ) - ) + )) if ("success".equals(response.status)) { // Page successful @@ -82,24 +83,23 @@ class PagerDutyNotificationService implements NotificationService { } else { pdErrors.put(serviceKey, response.message) } - } catch (RetrofitError error) { - String errorMessage = error.response.reason - TypedInput responseBody = error.response.getBody() - if (responseBody != null) { - PagerDutyErrorResponseBody errorResponse = mapper.readValue( - new String(((TypedByteArray)responseBody).getBytes()), - PagerDutyErrorResponseBody - ) - if (errorResponse.errors && errorResponse.errors.size() > 0) { - errorMessage = errorResponse.errors.join(", ") + } catch (SpinnakerServerException e){ + def errorMessage = null + if (e instanceof SpinnakerHttpException){ + Map errorResponse = ((SpinnakerHttpException) e).responseBody + if (errorResponse != null) { + if (errorResponse.errors && errorResponse.errors.size() > 0) { + errorMessage = errorResponse.errors.join(", ") + } } } + errorMessage = errorMessage == null ? e.message : errorMessage log.error('Failed to send page {} {} {}', kv('serviceKey', serviceKey), kv('message', notification.additionalContext.message), kv('error', errorMessage) ) - errors.put(serviceKey, errorMessage) + errors.put(serviceKey, e.message) } } @@ -129,12 +129,6 @@ class PagerDutyNotificationService implements NotificationService { } } -class PagerDutyErrorResponseBody { - String status - String message - List errors -} - @ResponseStatus(value = HttpStatus.BAD_REQUEST) @InheritConstructors class PagerDutyException extends RuntimeException {} diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/pagerduty/PagerDutyService.groovy b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/pagerduty/PagerDutyService.groovy index 80fe3a05f..7623a9b74 100644 --- a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/pagerduty/PagerDutyService.groovy +++ b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/pagerduty/PagerDutyService.groovy @@ -17,13 +17,14 @@ package com.netflix.spinnaker.echo.pagerduty -import retrofit.http.Body -import retrofit.http.Header -import retrofit.http.POST +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.Header +import retrofit2.http.POST interface PagerDutyService { @POST('/generic/2010-04-15/create_event.json') - Map createEvent(@Header("Authorization") String authorization, @Body PagerDutyCreateEvent pagerDutyCreateEvent) + Call createEvent(@Header("Authorization") String authorization, @Body PagerDutyCreateEvent pagerDutyCreateEvent) static class PagerDutyCreateEvent { String event_type = "trigger" diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/slack/SlackClient.groovy b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/slack/SlackClient.groovy index e7b7ae4e9..42187acf5 100644 --- a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/slack/SlackClient.groovy +++ b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/slack/SlackClient.groovy @@ -17,9 +17,16 @@ package com.netflix.spinnaker.echo.slack - -import retrofit.client.Response -import retrofit.http.* +import okhttp3.ResponseBody +import retrofit2.Response +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.Field +import retrofit2.http.FormUrlEncoded +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Path +import retrofit2.http.Query interface SlackClient { @@ -31,7 +38,7 @@ interface SlackClient { */ @FormUrlEncoded @POST('/api/chat.postMessage') - Response sendMessage( + Call sendMessage( @Field('token') String token, @Field('attachments') String attachments, @Field('channel') String channel, @@ -42,15 +49,15 @@ interface SlackClient { * Documentation: https://api.slack.com/incoming-webhooks */ @POST('/{token}') - Response sendUsingIncomingWebHook( - @Path(value = "token", encode = false) String token, + Call sendUsingIncomingWebHook( + @Path(value = "token", encoded = true) String token, @Body SlackRequest slackRequest) /** * Documentation: https://api.slack.com/methods/users.info */ @GET('/api/users.info') - SlackService.SlackUserInfo getUserInfo( + Call getUserInfo( @Query('token') String token, @Query('user') String userId ) diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/slack/SlackInteractiveNotificationService.groovy b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/slack/SlackInteractiveNotificationService.groovy index 87a71b636..ba5472ac7 100644 --- a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/slack/SlackInteractiveNotificationService.groovy +++ b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/slack/SlackInteractiveNotificationService.groovy @@ -17,27 +17,27 @@ package com.netflix.spinnaker.echo.slack import com.fasterxml.jackson.databind.ObjectMapper +import com.netflix.spinnaker.config.OkHttp3ClientConfiguration import com.netflix.spinnaker.echo.api.Notification import com.netflix.spinnaker.echo.notification.InteractiveNotificationService import com.netflix.spinnaker.echo.notification.NotificationTemplateEngine +import com.netflix.spinnaker.kork.retrofit.ErrorHandlingExecutorCallAdapterFactory; +import com.netflix.spinnaker.kork.retrofit.Retrofit2SyncCall; import com.netflix.spinnaker.kork.web.exceptions.InvalidRequestException -import com.netflix.spinnaker.retrofit.Slf4jRetrofitLogger import groovy.util.logging.Slf4j +import okhttp3.ResponseBody import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Qualifier import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty import org.springframework.http.RequestEntity import org.springframework.http.ResponseEntity import org.springframework.stereotype.Component -import retrofit.RestAdapter -import retrofit.client.Client -import retrofit.client.Response -import retrofit.converter.JacksonConverter -import retrofit.http.Body -import retrofit.http.POST -import retrofit.http.Path - -import static retrofit.Endpoints.newFixedEndpoint +import retrofit2.http.Body +import retrofit2.http.POST +import retrofit2.http.Path +import retrofit2.Call +import retrofit2.Retrofit +import retrofit2.converter.jackson.JacksonConverterFactory; @Slf4j @Component @@ -53,13 +53,13 @@ class SlackInteractiveNotificationService extends SlackNotificationService imple SlackInteractiveNotificationService( @Qualifier("slackAppService") SlackAppService slackAppService, NotificationTemplateEngine notificationTemplateEngine, - Client retrofitClient, + OkHttp3ClientConfiguration okHttp3ClientConfiguration, ObjectMapper objectMapper ) { super(slackAppService, notificationTemplateEngine) this.slackAppService = slackAppService as SlackAppService this.objectMapper = objectMapper - this.slackHookService = getSlackHookService(retrofitClient) + this.slackHookService = getSlackHookService(okHttp3ClientConfiguration) } // For access from tests only @@ -155,26 +155,25 @@ class SlackInteractiveNotificationService extends SlackNotificationService imple // Example: https://hooks.slack.com/actions/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX URI responseUrl = new URI(payload.response_url) log.info("POST ${SLACK_WEBHOOK_BASE_URL}${responseUrl.path}: ${message}") - Response response = slackHookService.respondToMessage(responseUrl.path, message) - log.info("Response from Slack: ${response.toString()}") + ResponseBody response = Retrofit2SyncCall.execute(slackHookService.respondToMessage(responseUrl.path, message)) + log.info("Response from Slack: ${response.string()}") return Optional.empty() } - private SlackHookService getSlackHookService(Client retrofitClient) { + private SlackHookService getSlackHookService(OkHttp3ClientConfiguration okHttp3ClientConfiguration) { log.info("Slack hook service loaded") - new RestAdapter.Builder() - .setEndpoint(newFixedEndpoint(SLACK_WEBHOOK_BASE_URL)) - .setClient(retrofitClient) - .setLogLevel(RestAdapter.LogLevel.BASIC) - .setLog(new Slf4jRetrofitLogger(SlackHookService.class)) - .setConverter(new JacksonConverter()) + new Retrofit.Builder() + .baseUrl(SLACK_WEBHOOK_BASE_URL) + .client(okHttp3ClientConfiguration.createForRetrofit2().build()) + .addCallAdapterFactory(ErrorHandlingExecutorCallAdapterFactory.getInstance()) + .addConverterFactory(JacksonConverterFactory.create()) .build() - .create(SlackHookService.class) + .create(SlackHookService.class); } interface SlackHookService { @POST('/{path}') - Response respondToMessage(@Path(value = "path", encode = false) path, @Body Map content) + Call respondToMessage(@Path(value = "path", encoded = true) path, @Body Map content) } } diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/slack/SlackNotificationService.groovy b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/slack/SlackNotificationService.groovy index c0bd5d4e5..aed8d4dc7 100644 --- a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/slack/SlackNotificationService.groovy +++ b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/slack/SlackNotificationService.groovy @@ -62,8 +62,12 @@ class SlackNotificationService implements NotificationService { new SlackAttachment(subject, text, (InteractiveActions)notification.interactiveActions), address, true) } - log.trace("Received response from Slack: {} {} for message '{}'. {}", - response?.status, response?.reason, text, response?.body) + try { + log.trace("Received response from Slack for message '{}'. {}", text, response?.string()) + } catch (IOException e) { + log.trace("Received response from Slack for message '{}' but unable to deserialize", text, e) + } + } new EchoResponse.Void() diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/slack/SlackService.groovy b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/slack/SlackService.groovy index dcac6e5b0..1d08b7198 100644 --- a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/slack/SlackService.groovy +++ b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/slack/SlackService.groovy @@ -20,10 +20,12 @@ package com.netflix.spinnaker.echo.slack import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.annotation.JsonProperty import com.netflix.spinnaker.echo.config.SlackLegacyProperties +import com.netflix.spinnaker.kork.retrofit.Retrofit2SyncCall import groovy.json.JsonBuilder import groovy.transform.Canonical import groovy.util.logging.Slf4j -import retrofit.client.Response +import okhttp3.ResponseBody +import retrofit2.Response @Canonical @Slf4j @@ -31,19 +33,19 @@ class SlackService { SlackClient slackClient SlackLegacyProperties config - Response sendCompactMessage(CompactSlackMessage message, String channel, boolean asUser) { - slackClient.sendMessage(config.token, message.buildMessage(), channel, asUser, config.expandUserNames ? 1 : 0) + ResponseBody sendCompactMessage(CompactSlackMessage message, String channel, boolean asUser) { + Retrofit2SyncCall.execute(slackClient.sendMessage(config.token, message.buildMessage(), channel, asUser, config.expandUserNames ? 1 : 0)) } - Response sendMessage(SlackAttachment message, String channel, boolean asUser) { + ResponseBody sendMessage(SlackAttachment message, String channel, boolean asUser) { config.useIncomingWebhook ? - slackClient.sendUsingIncomingWebHook(config.token, new SlackRequest([message], channel)) : - slackClient.sendMessage(config.token, toJson(message), channel, asUser, config.expandUserNames ? 1 : 0) + Retrofit2SyncCall.execute(slackClient.sendUsingIncomingWebHook(config.token, new SlackRequest([message], channel))) : + Retrofit2SyncCall.execute(slackClient.sendMessage(config.token, toJson(message), channel, asUser, config.expandUserNames ? 1 : 0)) } SlackUserInfo getUserInfo(String userId) { - slackClient.getUserInfo(config.token, userId) + Retrofit2SyncCall.execute(slackClient.getUserInfo(config.token, userId)) } diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/twilio/TwilioNotificationService.groovy b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/twilio/TwilioNotificationService.groovy index 5cb9e69aa..9eb3a0830 100644 --- a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/twilio/TwilioNotificationService.groovy +++ b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/twilio/TwilioNotificationService.groovy @@ -20,6 +20,7 @@ import com.netflix.spinnaker.echo.controller.EchoResponse import com.netflix.spinnaker.echo.notification.NotificationService import com.netflix.spinnaker.echo.api.Notification import com.netflix.spinnaker.echo.notification.NotificationTemplateEngine +import com.netflix.spinnaker.kork.retrofit.Retrofit2SyncCall import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Value import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty @@ -51,12 +52,12 @@ class TwilioNotificationService implements NotificationService { def body = notificationTemplateEngine.build(notification, NotificationTemplateEngine.Type.BODY) notification.to.each { - twilioService.sendMessage( + Retrofit2SyncCall.execute(twilioService.sendMessage( account, from, it, body - ) + )) } new EchoResponse.Void() diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/twilio/TwilioService.groovy b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/twilio/TwilioService.groovy index 0312f759d..d2e77c060 100644 --- a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/twilio/TwilioService.groovy +++ b/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/twilio/TwilioService.groovy @@ -18,20 +18,21 @@ package com.netflix.spinnaker.echo.twilio -import retrofit.client.Response -import retrofit.http.Field -import retrofit.http.FormUrlEncoded -import retrofit.http.POST -import retrofit.http.Path +import okhttp3.ResponseBody +import retrofit2.http.Field +import retrofit2.http.FormUrlEncoded +import retrofit2.http.POST +import retrofit2.http.Path +import retrofit2.Call interface TwilioService { @FormUrlEncoded @POST("/2010-04-01/Accounts/{account}/Messages.json") - Response sendMessage(@Path('account') String account, - @Field('From') String from, - @Field('To') String to, - @Field('Body') String body + Call sendMessage(@Path('account') String account, + @Field('From') String from, + @Field('To') String to, + @Field('Body') String body ) } diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/cdevents/BaseCDEvent.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/cdevents/BaseCDEvent.java similarity index 100% rename from echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/cdevents/BaseCDEvent.java rename to echo-notifications/src/main/java/com/netflix/spinnaker/echo/cdevents/BaseCDEvent.java diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/cdevents/CDEventPipelineRunFinished.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/cdevents/CDEventPipelineRunFinished.java similarity index 100% rename from echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/cdevents/CDEventPipelineRunFinished.java rename to echo-notifications/src/main/java/com/netflix/spinnaker/echo/cdevents/CDEventPipelineRunFinished.java diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/cdevents/CDEventPipelineRunQueued.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/cdevents/CDEventPipelineRunQueued.java similarity index 100% rename from echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/cdevents/CDEventPipelineRunQueued.java rename to echo-notifications/src/main/java/com/netflix/spinnaker/echo/cdevents/CDEventPipelineRunQueued.java diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/cdevents/CDEventPipelineRunStarted.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/cdevents/CDEventPipelineRunStarted.java similarity index 100% rename from echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/cdevents/CDEventPipelineRunStarted.java rename to echo-notifications/src/main/java/com/netflix/spinnaker/echo/cdevents/CDEventPipelineRunStarted.java diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/cdevents/CDEventTaskRunFinished.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/cdevents/CDEventTaskRunFinished.java similarity index 100% rename from echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/cdevents/CDEventTaskRunFinished.java rename to echo-notifications/src/main/java/com/netflix/spinnaker/echo/cdevents/CDEventTaskRunFinished.java diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/cdevents/CDEventTaskRunStarted.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/cdevents/CDEventTaskRunStarted.java similarity index 100% rename from echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/cdevents/CDEventTaskRunStarted.java rename to echo-notifications/src/main/java/com/netflix/spinnaker/echo/cdevents/CDEventTaskRunStarted.java diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/cdevents/CDEventsBuilderService.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/cdevents/CDEventsBuilderService.java similarity index 100% rename from echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/cdevents/CDEventsBuilderService.java rename to echo-notifications/src/main/java/com/netflix/spinnaker/echo/cdevents/CDEventsBuilderService.java diff --git a/echo-notifications/src/main/java/com/netflix/spinnaker/echo/cdevents/CDEventsConverterFactory.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/cdevents/CDEventsConverterFactory.java new file mode 100644 index 000000000..b9239f5d3 --- /dev/null +++ b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/cdevents/CDEventsConverterFactory.java @@ -0,0 +1,85 @@ +/* + * Copyright 2024 OpsMx, 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.echo.cdevents; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.netflix.spinnaker.kork.web.exceptions.InvalidRequestException; +import io.cloudevents.CloudEvent; +import io.cloudevents.jackson.JsonFormat; +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import okhttp3.MediaType; +import okhttp3.RequestBody; +import okhttp3.ResponseBody; +import retrofit2.Converter; +import retrofit2.Retrofit; + +public class CDEventsConverterFactory extends Converter.Factory { + private final ObjectMapper objectMapper; + + public CDEventsConverterFactory(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + public static CDEventsConverterFactory create() { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.registerModule(JsonFormat.getCloudEventJacksonModule()); + objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); + return new CDEventsConverterFactory(objectMapper); + } + + @Override + public Converter responseBodyConverter( + Type type, Annotation[] annotations, Retrofit retrofit) { + return (Converter) + value -> { + try (value) { + JavaType javaType = objectMapper.getTypeFactory().constructType(type); + return objectMapper.readValue(value.charStream(), javaType); + } + }; + } + + @Override + public Converter requestBodyConverter( + Type type, + Annotation[] parameterAnnotations, + Annotation[] methodAnnotations, + Retrofit retrofit) { + return (Converter) + value -> { + try { + String json = objectMapper.writeValueAsString(value); + return RequestBody.create(MediaType.parse("application/json"), json); + } catch (JsonProcessingException e) { + throw new IOException("Failed to serialize object to JSON", e); + } + }; + } + + public String convertCDEventToJson(CloudEvent cdEvent) { + try { + return objectMapper.writeValueAsString(cdEvent); + } catch (JsonProcessingException e) { + throw new InvalidRequestException("Unable to convert CDEvent to Json format.", e); + } + } +} diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/cdevents/CDEventsSenderClient.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/cdevents/CDEventsSenderClient.java similarity index 73% rename from echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/cdevents/CDEventsSenderClient.java rename to echo-notifications/src/main/java/com/netflix/spinnaker/echo/cdevents/CDEventsSenderClient.java index 1739f08bd..e15b53009 100644 --- a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/cdevents/CDEventsSenderClient.java +++ b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/cdevents/CDEventsSenderClient.java @@ -16,13 +16,15 @@ package com.netflix.spinnaker.echo.cdevents; -import retrofit.client.Response; -import retrofit.http.Body; -import retrofit.http.POST; -import retrofit.http.Path; +import okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.Response; +import retrofit2.http.Body; +import retrofit2.http.POST; +import retrofit2.http.Path; public interface CDEventsSenderClient { @POST("/{brokerUrl}") - Response sendCDEvent( - @Body String cdEvent, @Path(value = "brokerUrl", encode = false) String brokerUrl); + Call> sendCDEvent( + @Body String cdEvent, @Path(value = "brokerUrl", encoded = true) String brokerUrl); } diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/cdevents/CDEventsSenderService.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/cdevents/CDEventsSenderService.java similarity index 56% rename from echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/cdevents/CDEventsSenderService.java rename to echo-notifications/src/main/java/com/netflix/spinnaker/echo/cdevents/CDEventsSenderService.java index 4b71cae27..80afd2e71 100644 --- a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/cdevents/CDEventsSenderService.java +++ b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/cdevents/CDEventsSenderService.java @@ -16,58 +16,50 @@ package com.netflix.spinnaker.echo.cdevents; +import com.netflix.spinnaker.config.OkHttp3ClientConfiguration; +import com.netflix.spinnaker.kork.retrofit.ErrorHandlingExecutorCallAdapterFactory; +import com.netflix.spinnaker.kork.retrofit.Retrofit2SyncCall; import com.netflix.spinnaker.kork.web.exceptions.InvalidRequestException; -import com.netflix.spinnaker.retrofit.Slf4jRetrofitLogger; import io.cloudevents.CloudEvent; +import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import lombok.extern.slf4j.Slf4j; +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.ResponseBody; import org.apache.commons.lang3.StringUtils; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; -import retrofit.RequestInterceptor; -import retrofit.RestAdapter; -import retrofit.client.Client; -import retrofit.client.Response; +import retrofit2.Response; +import retrofit2.Retrofit; @Slf4j @Component public class CDEventsSenderService { - private Client retrofitClient; - private RestAdapter.LogLevel retrofitLogLevel; + private OkHttp3ClientConfiguration okHttpClientConfig; - public CDEventsSenderService(Client retrofitClient, RestAdapter.LogLevel retrofitLogLevel) { - this.retrofitClient = retrofitClient; - this.retrofitLogLevel = retrofitLogLevel; + public CDEventsSenderService(OkHttp3ClientConfiguration okHttpClientConfig) { + this.okHttpClientConfig = okHttpClientConfig; } - public Response sendCDEvent(CloudEvent cdEvent, String eventsBrokerUrl) { - CDEventsHTTPMessageConverter converterFactory = CDEventsHTTPMessageConverter.create(); - RequestInterceptor authInterceptor = - new RequestInterceptor() { - @Override - public void intercept(RequestInterceptor.RequestFacade request) { - request.addHeader("Ce-Id", cdEvent.getId()); - request.addHeader("Ce-Specversion", cdEvent.getSpecVersion().V1.toString()); - request.addHeader("Ce-Source", cdEvent.getSource().toString()); - request.addHeader("Ce-Type", cdEvent.getType()); - request.addHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE); - } - }; + public Response sendCDEvent(CloudEvent cdEvent, String eventsBrokerUrl) { + CDEventsConverterFactory converterFactory = CDEventsConverterFactory.create(); + + RequestInterceptor authInterceptor = new RequestInterceptor(cdEvent); CDEventsSenderClient cdEventsSenderClient = - new RestAdapter.Builder() - .setConverter(converterFactory) - .setClient(retrofitClient) - .setEndpoint(getEndpointUrl(eventsBrokerUrl)) - .setRequestInterceptor(authInterceptor) - .setLogLevel(retrofitLogLevel) - .setLog(new Slf4jRetrofitLogger(CDEventsSenderClient.class)) + new Retrofit.Builder() + .baseUrl(getEndpointUrl(eventsBrokerUrl)) + .client(okHttpClientConfig.createForRetrofit2().addInterceptor(authInterceptor).build()) + .addCallAdapterFactory(ErrorHandlingExecutorCallAdapterFactory.getInstance()) + .addConverterFactory(converterFactory) .build() .create(CDEventsSenderClient.class); String jsonEvent = converterFactory.convertCDEventToJson(cdEvent); log.info("Sending CDEvent Json {} ", jsonEvent); - return cdEventsSenderClient.sendCDEvent(jsonEvent, getRelativePath(eventsBrokerUrl)); + return Retrofit2SyncCall.execute( + cdEventsSenderClient.sendCDEvent(jsonEvent, getRelativePath(eventsBrokerUrl))); } private String getEndpointUrl(String webhookUrl) { @@ -106,4 +98,27 @@ private String getRelativePath(String webhookUrl) { return relativePath; } + + private static class RequestInterceptor implements Interceptor { + + private CloudEvent cdEvent; + + public RequestInterceptor(CloudEvent cdEvent) { + this.cdEvent = cdEvent; + } + + public okhttp3.Response intercept(Chain chain) throws IOException { + Request request = + chain + .request() + .newBuilder() + .addHeader("Ce-Id", cdEvent.getId()) + .addHeader("Ce-Specversion", cdEvent.getSpecVersion().V1.toString()) + .addHeader("Ce-Source", cdEvent.getSource().toString()) + .addHeader("Ce-Type", cdEvent.getType()) + .addHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE) + .build(); + return chain.proceed(request); + } + } } diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/config/DryRunConfig.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/config/DryRunConfig.java similarity index 66% rename from echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/config/DryRunConfig.java rename to echo-notifications/src/main/java/com/netflix/spinnaker/echo/config/DryRunConfig.java index 9688969eb..49a37a5f7 100644 --- a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/config/DryRunConfig.java +++ b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/config/DryRunConfig.java @@ -16,15 +16,12 @@ package com.netflix.spinnaker.echo.config; -import static retrofit.Endpoints.newFixedEndpoint; - -import com.jakewharton.retrofit.Ok3Client; import com.netflix.spinnaker.config.DefaultServiceEndpoint; import com.netflix.spinnaker.config.okhttp3.OkHttpClientProvider; import com.netflix.spinnaker.echo.notification.DryRunNotificationAgent; import com.netflix.spinnaker.echo.pipelinetriggers.orca.OrcaService; import com.netflix.spinnaker.echo.services.Front50Service; -import com.netflix.spinnaker.retrofit.Slf4jRetrofitLogger; +import com.netflix.spinnaker.kork.retrofit.ErrorHandlingExecutorCallAdapterFactory; import java.util.List; import lombok.Data; import lombok.extern.slf4j.Slf4j; @@ -33,9 +30,8 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import retrofit.Endpoint; -import retrofit.RestAdapter; -import retrofit.converter.JacksonConverter; +import retrofit2.Retrofit; +import retrofit2.converter.jackson.JacksonConverterFactory; @Configuration @EnableConfigurationProperties(DryRunConfig.DryRunProperties.class) @@ -43,29 +39,18 @@ @Slf4j public class DryRunConfig { - @Bean - Endpoint dryRunEndpoint(DryRunProperties properties) { - return newFixedEndpoint(properties.getBaseUrl()); - } - @Bean DryRunNotificationAgent dryRunNotificationAgent( - Front50Service front50, - OkHttpClientProvider clientProvider, - RestAdapter.LogLevel retrofitLogLevel, - Endpoint dryRunEndpoint, - DryRunProperties properties) { - log.info("Pipeline dry runs will execute at {}", dryRunEndpoint.getUrl()); + Front50Service front50, OkHttpClientProvider clientProvider, DryRunProperties properties) { + log.info("Pipeline dry runs will execute at {}", properties.getBaseUrl()); OrcaService orca = - new RestAdapter.Builder() - .setEndpoint(dryRunEndpoint) - .setConverter(new JacksonConverter()) - .setClient( - new Ok3Client( - clientProvider.getClient( - new DefaultServiceEndpoint("orca", dryRunEndpoint.getUrl())))) - .setLogLevel(retrofitLogLevel) - .setLog(new Slf4jRetrofitLogger(OrcaService.class)) + new Retrofit.Builder() + .baseUrl(properties.getBaseUrl()) + .client( + clientProvider.getClient( + new DefaultServiceEndpoint("orca", properties.getBaseUrl()))) + .addCallAdapterFactory(ErrorHandlingExecutorCallAdapterFactory.getInstance()) + .addConverterFactory(JacksonConverterFactory.create()) .build() .create(OrcaService.class); return new DryRunNotificationAgent(front50, orca, properties); diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/config/GithubConfig.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/config/GithubConfig.java similarity index 53% rename from echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/config/GithubConfig.java rename to echo-notifications/src/main/java/com/netflix/spinnaker/echo/config/GithubConfig.java index 56d791e86..c5fcfd88b 100644 --- a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/config/GithubConfig.java +++ b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/config/GithubConfig.java @@ -16,47 +16,38 @@ package com.netflix.spinnaker.echo.config; +import com.netflix.spinnaker.config.OkHttp3ClientConfiguration; import com.netflix.spinnaker.echo.github.GithubService; -import com.netflix.spinnaker.retrofit.Slf4jRetrofitLogger; +import com.netflix.spinnaker.kork.retrofit.ErrorHandlingExecutorCallAdapterFactory; import lombok.extern.slf4j.Slf4j; 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.client.Client; -import retrofit.converter.JacksonConverter; +import retrofit2.Retrofit; +import retrofit2.converter.jackson.JacksonConverterFactory; @Configuration @ConditionalOnProperty("github-status.enabled") @Slf4j public class GithubConfig { - @Value("${github-status.endpoint:https://api.github.com}") - private String endpoint; + private final String endpoint; - @Bean - public Endpoint githubEndpoint() { - return Endpoints.newFixedEndpoint(endpoint); + public GithubConfig(@Value("${github-status.endpoint:https://api.github.com}") String endpoint) { + this.endpoint = endpoint; } @Bean - public GithubService githubService( - Endpoint githubEndpoint, Client retrofitClient, RestAdapter.LogLevel retrofitLogLevel) { + public GithubService githubService(OkHttp3ClientConfiguration okHttpClientConfig) { log.info("Github service loaded"); - GithubService githubClient = - new RestAdapter.Builder() - .setEndpoint(githubEndpoint) - .setConverter(new JacksonConverter()) - .setClient(retrofitClient) - .setLogLevel(retrofitLogLevel != null ? retrofitLogLevel : RestAdapter.LogLevel.BASIC) - .setLog(new Slf4jRetrofitLogger(GithubService.class)) - .build() - .create(GithubService.class); - - return githubClient; + return new Retrofit.Builder() + .baseUrl(endpoint) + .client(okHttpClientConfig.createForRetrofit2().build()) + .addCallAdapterFactory(ErrorHandlingExecutorCallAdapterFactory.getInstance()) + .addConverterFactory(JacksonConverterFactory.create()) + .build() + .create(GithubService.class); } } diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/config/JiraConfig.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/config/JiraConfig.java similarity index 52% rename from echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/config/JiraConfig.java rename to echo-notifications/src/main/java/com/netflix/spinnaker/echo/config/JiraConfig.java index 7101a5171..b7f413167 100644 --- a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/config/JiraConfig.java +++ b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/config/JiraConfig.java @@ -16,13 +16,15 @@ package com.netflix.spinnaker.echo.config; -import static retrofit.Endpoints.newFixedEndpoint; - -import com.jakewharton.retrofit.Ok3Client; -import com.netflix.spinnaker.echo.jackson.EchoObjectMapper; +import com.netflix.spinnaker.config.OkHttp3ClientConfiguration; import com.netflix.spinnaker.echo.jira.JiraProperties; import com.netflix.spinnaker.echo.jira.JiraService; -import com.netflix.spinnaker.retrofit.Slf4jRetrofitLogger; +import com.netflix.spinnaker.kork.retrofit.ErrorHandlingExecutorCallAdapterFactory; +import java.io.IOException; +import okhttp3.Interceptor; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; import org.apache.commons.codec.binary.Base64; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -31,9 +33,8 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import retrofit.RestAdapter; -import retrofit.client.Client; -import retrofit.converter.JacksonConverter; +import retrofit2.Retrofit; +import retrofit2.converter.jackson.JacksonConverterFactory; @Configuration @ConditionalOnProperty("jira.enabled") @@ -42,36 +43,51 @@ public class JiraConfig { private static Logger LOGGER = LoggerFactory.getLogger(JiraConfig.class); @Autowired(required = false) - private Ok3Client x509ConfiguredClient; + private OkHttpClient x509ConfiguredClient; @Bean JiraService jiraService( - JiraProperties jiraProperties, Client retrofitClient, RestAdapter.LogLevel retrofitLogLevel) { + JiraProperties jiraProperties, OkHttp3ClientConfiguration okHttpClientConfig) { + OkHttpClient okHttpClient; if (x509ConfiguredClient != null) { LOGGER.info("Using X509 Cert for Jira Client"); - retrofitClient = x509ConfiguredClient; - } - - RestAdapter.Builder builder = - new RestAdapter.Builder() - .setEndpoint(newFixedEndpoint(jiraProperties.getBaseUrl())) - .setConverter(new JacksonConverter(EchoObjectMapper.getInstance())) - .setClient(retrofitClient) - .setLogLevel(retrofitLogLevel) - .setLog(new Slf4jRetrofitLogger(JiraService.class)); - - if (x509ConfiguredClient == null) { + okHttpClient = x509ConfiguredClient; + } else { String credentials = String.format("%s:%s", jiraProperties.getUsername(), jiraProperties.getPassword()); final String basic = String.format("Basic %s", Base64.encodeBase64String(credentials.getBytes())); - builder.setRequestInterceptor( - request -> { - request.addHeader("Authorization", basic); - request.addHeader("Accept", "application/json"); - }); + BasicAuthRequestInterceptor interceptor = new BasicAuthRequestInterceptor(basic); + okHttpClient = okHttpClientConfig.createForRetrofit2().addInterceptor(interceptor).build(); } - return builder.build().create(JiraService.class); + return new Retrofit.Builder() + .baseUrl(jiraProperties.getBaseUrl()) + .client(okHttpClient) + .addCallAdapterFactory(ErrorHandlingExecutorCallAdapterFactory.getInstance()) + .addConverterFactory(JacksonConverterFactory.create()) + .build() + .create(JiraService.class); + } + + private static class BasicAuthRequestInterceptor implements Interceptor { + + private final String basic; + + public BasicAuthRequestInterceptor(String basic) { + this.basic = basic; + } + + @Override + public Response intercept(Chain chain) throws IOException { + Request request = + chain + .request() + .newBuilder() + .addHeader("Authorization", basic) + .addHeader("Accept", "application/json") + .build(); + return chain.proceed(request); + } } } diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/config/MicrosoftTeamsConfig.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/config/MicrosoftTeamsConfig.java similarity index 83% rename from echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/config/MicrosoftTeamsConfig.java rename to echo-notifications/src/main/java/com/netflix/spinnaker/echo/config/MicrosoftTeamsConfig.java index 953687e5a..7a7ee71ab 100644 --- a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/config/MicrosoftTeamsConfig.java +++ b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/config/MicrosoftTeamsConfig.java @@ -16,8 +16,10 @@ package com.netflix.spinnaker.echo.config; +import com.netflix.spinnaker.config.OkHttp3ClientConfiguration; import com.netflix.spinnaker.echo.microsoftteams.MicrosoftTeamsService; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -29,11 +31,13 @@ @Slf4j public class MicrosoftTeamsConfig { + @Autowired OkHttp3ClientConfiguration okHttp3ClientConfiguration; + @Bean public MicrosoftTeamsService microsoftTeamsService( Client retrofitClient, RestAdapter.LogLevel retrofitLogLevel) { log.info("Microsoft Teams service loaded"); - return new MicrosoftTeamsService(retrofitClient, retrofitLogLevel); + return new MicrosoftTeamsService(okHttp3ClientConfiguration); } } diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/config/PagerDutyConfig.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/config/PagerDutyConfig.java similarity index 61% rename from echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/config/PagerDutyConfig.java rename to echo-notifications/src/main/java/com/netflix/spinnaker/echo/config/PagerDutyConfig.java index 11b306515..fcaf1e067 100644 --- a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/config/PagerDutyConfig.java +++ b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/config/PagerDutyConfig.java @@ -16,41 +16,35 @@ package com.netflix.spinnaker.echo.config; -import static retrofit.Endpoints.newFixedEndpoint; - +import com.netflix.spinnaker.config.OkHttp3ClientConfiguration; import com.netflix.spinnaker.echo.pagerduty.PagerDutyService; -import com.netflix.spinnaker.retrofit.Slf4jRetrofitLogger; +import com.netflix.spinnaker.kork.retrofit.ErrorHandlingExecutorCallAdapterFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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.Endpoint; -import retrofit.RestAdapter; -import retrofit.client.Client; -import retrofit.converter.JacksonConverter; +import retrofit2.Retrofit; +import retrofit2.converter.jackson.JacksonConverterFactory; @Configuration +@EnableConfigurationProperties(PagerDutyConfigurationProperties.class) @ConditionalOnProperty("pager-duty.enabled") public class PagerDutyConfig { private static final Logger log = LoggerFactory.getLogger(PagerDutyConfig.class); - @Bean - Endpoint pagerDutyEndpoint() { - return newFixedEndpoint("https://events.pagerduty.com"); - } - @Bean PagerDutyService pagerDutyService( - Endpoint pagerDutyEndpoint, Client retrofitClient, RestAdapter.LogLevel retrofitLogLevel) { + OkHttp3ClientConfiguration okHttpClientConfig, + PagerDutyConfigurationProperties pagerDutyProps) { log.info("Pager Duty service loaded"); - return new RestAdapter.Builder() - .setEndpoint(pagerDutyEndpoint) - .setConverter(new JacksonConverter()) - .setClient(retrofitClient) - .setLogLevel(retrofitLogLevel) - .setLog(new Slf4jRetrofitLogger(PagerDutyService.class)) + return new Retrofit.Builder() + .baseUrl(pagerDutyProps.getEndpoint()) + .client(okHttpClientConfig.createForRetrofit2().build()) + .addCallAdapterFactory(ErrorHandlingExecutorCallAdapterFactory.getInstance()) + .addConverterFactory(JacksonConverterFactory.create()) .build() .create(PagerDutyService.class); } diff --git a/echo-notifications/src/main/java/com/netflix/spinnaker/echo/config/PagerDutyConfigurationProperties.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/config/PagerDutyConfigurationProperties.java new file mode 100644 index 000000000..d585afc6c --- /dev/null +++ b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/config/PagerDutyConfigurationProperties.java @@ -0,0 +1,29 @@ +/* + * Copyright 2025 OpsMx, 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.echo.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Setter +@Getter +@ConfigurationProperties(prefix = "pager-duty") +public class PagerDutyConfigurationProperties { + private String endpoint = "https://events.pagerduty.com"; + private String token; +} diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/controller/EchoResponse.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/controller/EchoResponse.java similarity index 100% rename from echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/controller/EchoResponse.java rename to echo-notifications/src/main/java/com/netflix/spinnaker/echo/controller/EchoResponse.java diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/controller/NotificationController.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/controller/NotificationController.java similarity index 100% rename from echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/controller/NotificationController.java rename to echo-notifications/src/main/java/com/netflix/spinnaker/echo/controller/NotificationController.java diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/exceptions/FieldNotFoundException.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/exceptions/FieldNotFoundException.java similarity index 100% rename from echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/exceptions/FieldNotFoundException.java rename to echo-notifications/src/main/java/com/netflix/spinnaker/echo/exceptions/FieldNotFoundException.java diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/github/GithubCommit.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/github/GithubCommit.java similarity index 100% rename from echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/github/GithubCommit.java rename to echo-notifications/src/main/java/com/netflix/spinnaker/echo/github/GithubCommit.java diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/github/GithubCommitDetail.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/github/GithubCommitDetail.java similarity index 100% rename from echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/github/GithubCommitDetail.java rename to echo-notifications/src/main/java/com/netflix/spinnaker/echo/github/GithubCommitDetail.java diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/github/GithubService.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/github/GithubService.java similarity index 69% rename from echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/github/GithubService.java rename to echo-notifications/src/main/java/com/netflix/spinnaker/echo/github/GithubService.java index afa6f495a..f2a1360b4 100644 --- a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/github/GithubService.java +++ b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/github/GithubService.java @@ -16,20 +16,26 @@ package com.netflix.spinnaker.echo.github; -import retrofit.client.Response; -import retrofit.http.*; +import okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.Response; +import retrofit2.http.Body; +import retrofit2.http.GET; +import retrofit2.http.Header; +import retrofit2.http.POST; +import retrofit2.http.Path; public interface GithubService { @POST("/repos/{repo}/statuses/{sha}") - Response updateCheck( + Call> updateCheck( @Header("Authorization") String token, - @Path(value = "repo", encode = false) String repo, + @Path(value = "repo", encoded = true) String repo, @Path("sha") String sha, @Body GithubStatus status); @GET("/repos/{repo}/commits/{sha}") - Response getCommit( + Call getCommit( @Header("Authorization") String token, - @Path(value = "repo", encode = false) String repo, + @Path(value = "repo", encoded = true) String repo, @Path("sha") String sha); } diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/github/GithubStatus.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/github/GithubStatus.java similarity index 100% rename from echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/github/GithubStatus.java rename to echo-notifications/src/main/java/com/netflix/spinnaker/echo/github/GithubStatus.java diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/googlechat/GoogleChatClient.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/googlechat/GoogleChatClient.java similarity index 76% rename from echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/googlechat/GoogleChatClient.java rename to echo-notifications/src/main/java/com/netflix/spinnaker/echo/googlechat/GoogleChatClient.java index e47c78ea6..fd89ea507 100644 --- a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/googlechat/GoogleChatClient.java +++ b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/googlechat/GoogleChatClient.java @@ -16,13 +16,14 @@ package com.netflix.spinnaker.echo.googlechat; -import retrofit.client.Response; -import retrofit.http.Body; -import retrofit.http.POST; -import retrofit.http.Path; +import okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.POST; +import retrofit2.http.Path; public interface GoogleChatClient { @POST("/v1/spaces/{address}") - Response sendMessage( - @Path(value = "address", encode = false) String webhook, @Body GoogleChatMessage message); + Call sendMessage( + @Path(value = "address", encoded = true) String webhook, @Body GoogleChatMessage message); } diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/googlechat/GoogleChatMessage.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/googlechat/GoogleChatMessage.java similarity index 100% rename from echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/googlechat/GoogleChatMessage.java rename to echo-notifications/src/main/java/com/netflix/spinnaker/echo/googlechat/GoogleChatMessage.java diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/googlechat/GoogleChatNotificationService.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/googlechat/GoogleChatNotificationService.java similarity index 100% rename from echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/googlechat/GoogleChatNotificationService.java rename to echo-notifications/src/main/java/com/netflix/spinnaker/echo/googlechat/GoogleChatNotificationService.java diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/googlechat/GoogleChatService.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/googlechat/GoogleChatService.java similarity index 77% rename from echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/googlechat/GoogleChatService.java rename to echo-notifications/src/main/java/com/netflix/spinnaker/echo/googlechat/GoogleChatService.java index d358539e4..5e67ee7c1 100644 --- a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/googlechat/GoogleChatService.java +++ b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/googlechat/GoogleChatService.java @@ -16,8 +16,9 @@ package com.netflix.spinnaker.echo.googlechat; +import com.netflix.spinnaker.kork.retrofit.Retrofit2SyncCall; import groovy.transform.Canonical; -import retrofit.client.Response; +import okhttp3.ResponseBody; @Canonical public class GoogleChatService { @@ -27,7 +28,7 @@ public GoogleChatService(GoogleChatClient googleChatClient) { this.googleChatClient = googleChatClient; } - Response sendMessage(String webhook, GoogleChatMessage message) { - return googleChatClient.sendMessage(webhook, message); + ResponseBody sendMessage(String webhook, GoogleChatMessage message) { + return Retrofit2SyncCall.execute(googleChatClient.sendMessage(webhook, message)); } } diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/jira/JiraNotificationService.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/jira/JiraNotificationService.java similarity index 90% rename from echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/jira/JiraNotificationService.java rename to echo-notifications/src/main/java/com/netflix/spinnaker/echo/jira/JiraNotificationService.java index bc519a94c..317594372 100644 --- a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/jira/JiraNotificationService.java +++ b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/jira/JiraNotificationService.java @@ -30,11 +30,14 @@ import com.netflix.spinnaker.echo.jira.JiraService.TransitionIssueRequest; import com.netflix.spinnaker.echo.notification.NotificationService; import com.netflix.spinnaker.kork.core.RetrySupport; +import com.netflix.spinnaker.kork.retrofit.Retrofit2SyncCall; +import com.netflix.spinnaker.kork.retrofit.exceptions.SpinnakerHttpException; import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.function.Supplier; import java.util.stream.Collectors; +import okhttp3.ResponseBody; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -42,8 +45,6 @@ import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import org.springframework.web.bind.annotation.ResponseStatus; -import retrofit.RetrofitError; -import retrofit.client.Response; @Component @ConditionalOnProperty("jira.enabled") @@ -151,21 +152,27 @@ private EchoResponse create(Notification notification) { } private Supplier getIssueTransitions(String issueIdOrKey) { - return () -> jiraService.getIssueTransitions(issueIdOrKey); + return () -> Retrofit2SyncCall.execute(jiraService.getIssueTransitions(issueIdOrKey)); } - private Supplier transitionIssue( + private Supplier transitionIssue( String issueIdOrKey, Map transitionDetails) { return () -> - jiraService.transitionIssue(issueIdOrKey, new TransitionIssueRequest(transitionDetails)); + Retrofit2SyncCall.execute( + jiraService.transitionIssue( + issueIdOrKey, new TransitionIssueRequest(transitionDetails))); } - private Supplier addComment(String issueIdOrKey, String comment) { - return () -> jiraService.addComment(issueIdOrKey, new CommentIssueRequest(comment)); + private Supplier addComment(String issueIdOrKey, String comment) { + return () -> + Retrofit2SyncCall.execute( + jiraService.addComment(issueIdOrKey, new CommentIssueRequest(comment))); } private Supplier createIssue(Map issueRequestBody) { - return () -> jiraService.createIssue(new CreateIssueRequest(issueRequestBody)); + return () -> + Retrofit2SyncCall.execute( + jiraService.createIssue(new CreateIssueRequest(issueRequestBody))); } private Map issueRequestBody(Notification notification) { @@ -181,13 +188,8 @@ private Map issueRequestBody(Notification notification) { } private Map errors(Exception exception) { - if (exception instanceof RetrofitError) { - try { - return mapper.readValue( - ((RetrofitError) exception).getResponse().getBody().in(), Map.class); - } catch (Exception e) { - LOGGER.warn("failed retrieving error messages {}", e.getMessage()); - } + if (exception instanceof SpinnakerHttpException) { + return ((SpinnakerHttpException) exception).getResponseBody(); } return ImmutableMap.of("errors", exception.getMessage()); diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/jira/JiraProperties.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/jira/JiraProperties.java similarity index 100% rename from echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/jira/JiraProperties.java rename to echo-notifications/src/main/java/com/netflix/spinnaker/echo/jira/JiraProperties.java diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/jira/JiraService.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/jira/JiraService.java similarity index 87% rename from echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/jira/JiraService.java rename to echo-notifications/src/main/java/com/netflix/spinnaker/echo/jira/JiraService.java index dd0799e61..950e43969 100644 --- a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/jira/JiraService.java +++ b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/jira/JiraService.java @@ -20,26 +20,27 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import retrofit.client.Response; -import retrofit.http.Body; -import retrofit.http.GET; -import retrofit.http.POST; -import retrofit.http.Path; +import okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.GET; +import retrofit2.http.POST; +import retrofit2.http.Path; public interface JiraService { @POST("/rest/api/2/issue/") - CreateIssueResponse createIssue(@Body CreateIssueRequest createIssueRequest); + Call createIssue(@Body CreateIssueRequest createIssueRequest); @GET("/rest/api/2/issue/{issueIdOrKey}/transitions") - IssueTransitions getIssueTransitions(@Path("issueIdOrKey") String issueIdOrKey); + Call getIssueTransitions(@Path("issueIdOrKey") String issueIdOrKey); @POST("/rest/api/2/issue/{issueIdOrKey}/transitions") - Response transitionIssue( + Call transitionIssue( @Path("issueIdOrKey") String issueIdOrKey, @Body TransitionIssueRequest transitionIssueRequest); @POST("/rest/api/2/issue/{issueIdOrKey}/comment") - Response addComment( + Call addComment( @Path("issueIdOrKey") String issueIdOrKey, @Body CommentIssueRequest commentIssueRequest); class CreateIssueRequest extends HashMap { diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/microsoftteams/MicrosoftTeamsClient.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/microsoftteams/MicrosoftTeamsClient.java similarity index 76% rename from echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/microsoftteams/MicrosoftTeamsClient.java rename to echo-notifications/src/main/java/com/netflix/spinnaker/echo/microsoftteams/MicrosoftTeamsClient.java index e92f8cca9..4fc0d34e6 100644 --- a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/microsoftteams/MicrosoftTeamsClient.java +++ b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/microsoftteams/MicrosoftTeamsClient.java @@ -16,14 +16,15 @@ package com.netflix.spinnaker.echo.microsoftteams; -import retrofit.client.Response; -import retrofit.http.Body; -import retrofit.http.POST; -import retrofit.http.Path; +import okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.POST; +import retrofit2.http.Path; public interface MicrosoftTeamsClient { @POST("/{webhookUrl}") - Response sendMessage( - @Path(value = "webhookUrl", encode = false) String webhook, + Call sendMessage( + @Path(value = "webhookUrl", encoded = true) String webhook, @Body MicrosoftTeamsMessage message); } diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/microsoftteams/MicrosoftTeamsMessage.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/microsoftteams/MicrosoftTeamsMessage.java similarity index 100% rename from echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/microsoftteams/MicrosoftTeamsMessage.java rename to echo-notifications/src/main/java/com/netflix/spinnaker/echo/microsoftteams/MicrosoftTeamsMessage.java diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/microsoftteams/MicrosoftTeamsNotificationService.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/microsoftteams/MicrosoftTeamsNotificationService.java similarity index 100% rename from echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/microsoftteams/MicrosoftTeamsNotificationService.java rename to echo-notifications/src/main/java/com/netflix/spinnaker/echo/microsoftteams/MicrosoftTeamsNotificationService.java diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/microsoftteams/MicrosoftTeamsService.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/microsoftteams/MicrosoftTeamsService.java similarity index 60% rename from echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/microsoftteams/MicrosoftTeamsService.java rename to echo-notifications/src/main/java/com/netflix/spinnaker/echo/microsoftteams/MicrosoftTeamsService.java index 2328c7752..cdbcc750f 100644 --- a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/microsoftteams/MicrosoftTeamsService.java +++ b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/microsoftteams/MicrosoftTeamsService.java @@ -16,51 +16,40 @@ package com.netflix.spinnaker.echo.microsoftteams; +import com.netflix.spinnaker.config.OkHttp3ClientConfiguration; +import com.netflix.spinnaker.kork.retrofit.ErrorHandlingExecutorCallAdapterFactory; +import com.netflix.spinnaker.kork.retrofit.Retrofit2SyncCall; import com.netflix.spinnaker.kork.web.exceptions.InvalidRequestException; -import com.netflix.spinnaker.retrofit.Slf4jRetrofitLogger; import java.net.MalformedURLException; import java.net.URL; import lombok.extern.slf4j.Slf4j; +import okhttp3.ResponseBody; import org.apache.commons.lang3.StringUtils; -import retrofit.RestAdapter; -import retrofit.client.Client; -import retrofit.client.Response; -import retrofit.converter.JacksonConverter; +import retrofit2.Retrofit; +import retrofit2.converter.jackson.JacksonConverterFactory; @Slf4j public class MicrosoftTeamsService { - private Client retrofitClient; - private RestAdapter.LogLevel retrofitLogLevel; + private final OkHttp3ClientConfiguration okHttp3ClientConfiguration; - public MicrosoftTeamsService(Client retrofitClient, RestAdapter.LogLevel retrofitLogLevel) { - this.retrofitClient = retrofitClient; - this.retrofitLogLevel = retrofitLogLevel; + public MicrosoftTeamsService(OkHttp3ClientConfiguration okHttp3ClientConfiguration) { + this.okHttp3ClientConfiguration = okHttp3ClientConfiguration; } - public Response sendMessage(String webhookUrl, MicrosoftTeamsMessage message) { + public ResponseBody sendMessage(String webhookUrl, MicrosoftTeamsMessage message) { // The RestAdapter instantiation needs to occur for each message to be sent as // the incoming webhook base URL and path may be different for each Teams channel MicrosoftTeamsClient microsoftTeamsClient = - new RestAdapter.Builder() - .setConverter(new JacksonConverter()) - .setClient(retrofitClient) - .setEndpoint(getEndpointUrl(webhookUrl)) - .setLogLevel(retrofitLogLevel) - .setLog(new Slf4jRetrofitLogger(MicrosoftTeamsClient.class)) + new Retrofit.Builder() + .baseUrl(webhookUrl) + .client(okHttp3ClientConfiguration.createForRetrofit2().build()) + .addCallAdapterFactory(ErrorHandlingExecutorCallAdapterFactory.getInstance()) + .addConverterFactory(JacksonConverterFactory.create()) .build() .create(MicrosoftTeamsClient.class); - return microsoftTeamsClient.sendMessage(getRelativePath(webhookUrl), message); - } - - private String getEndpointUrl(String webhookUrl) { - try { - URL url = new URL(webhookUrl); - return url.getProtocol() + "://" + url.getHost(); - } catch (MalformedURLException e) { - throw new InvalidRequestException( - "Unable to determine base URL from Microsoft Teams webhook URL.", e); - } + return Retrofit2SyncCall.execute( + microsoftTeamsClient.sendMessage(getRelativePath(webhookUrl), message)); } private String getRelativePath(String webhookUrl) { diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/microsoftteams/api/MicrosoftTeamsFact.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/microsoftteams/api/MicrosoftTeamsFact.java similarity index 100% rename from echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/microsoftteams/api/MicrosoftTeamsFact.java rename to echo-notifications/src/main/java/com/netflix/spinnaker/echo/microsoftteams/api/MicrosoftTeamsFact.java diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/microsoftteams/api/MicrosoftTeamsPotentialAction.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/microsoftteams/api/MicrosoftTeamsPotentialAction.java similarity index 100% rename from echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/microsoftteams/api/MicrosoftTeamsPotentialAction.java rename to echo-notifications/src/main/java/com/netflix/spinnaker/echo/microsoftteams/api/MicrosoftTeamsPotentialAction.java diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/microsoftteams/api/MicrosoftTeamsPotentialActionChoice.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/microsoftteams/api/MicrosoftTeamsPotentialActionChoice.java similarity index 100% rename from echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/microsoftteams/api/MicrosoftTeamsPotentialActionChoice.java rename to echo-notifications/src/main/java/com/netflix/spinnaker/echo/microsoftteams/api/MicrosoftTeamsPotentialActionChoice.java diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/microsoftteams/api/MicrosoftTeamsPotentialActionTarget.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/microsoftteams/api/MicrosoftTeamsPotentialActionTarget.java similarity index 100% rename from echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/microsoftteams/api/MicrosoftTeamsPotentialActionTarget.java rename to echo-notifications/src/main/java/com/netflix/spinnaker/echo/microsoftteams/api/MicrosoftTeamsPotentialActionTarget.java diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/microsoftteams/api/MicrosoftTeamsSection.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/microsoftteams/api/MicrosoftTeamsSection.java similarity index 100% rename from echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/microsoftteams/api/MicrosoftTeamsSection.java rename to echo-notifications/src/main/java/com/netflix/spinnaker/echo/microsoftteams/api/MicrosoftTeamsSection.java diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/CDEventsNotificationAgent.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/notification/CDEventsNotificationAgent.java similarity index 78% rename from echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/CDEventsNotificationAgent.java rename to echo-notifications/src/main/java/com/netflix/spinnaker/echo/notification/CDEventsNotificationAgent.java index a6cd43172..5d08e8949 100644 --- a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/CDEventsNotificationAgent.java +++ b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/notification/CDEventsNotificationAgent.java @@ -21,14 +21,15 @@ import com.netflix.spinnaker.echo.cdevents.CDEventsSenderService; import com.netflix.spinnaker.echo.exceptions.FieldNotFoundException; import io.cloudevents.CloudEvent; +import java.io.IOException; import java.util.Map; import java.util.Optional; import lombok.extern.slf4j.Slf4j; +import okhttp3.ResponseBody; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; -import retrofit.client.Response; -import retrofit.mime.TypedByteArray; +import retrofit2.Response; @Slf4j @ConditionalOnProperty("cdevents.enabled") @@ -70,16 +71,24 @@ public void sendNotifications( preference, application, event, config, status, getSpinnakerUrl()); log.info( "Sending CDEvent {} notification to events broker url {}", cdEventsType, eventsBrokerUrl); - Response response = cdEventsSenderService.sendCDEvent(cdEvent, eventsBrokerUrl); + Response response = cdEventsSenderService.sendCDEvent(cdEvent, eventsBrokerUrl); if (response != null) { - log.info( - "Received response from events broker : {} {} for execution id {}. {}", - response.getStatus(), - response.getReason(), - executionId, - response.getBody() != null - ? new String(((TypedByteArray) response.getBody()).getBytes()) - : ""); + try { + log.info( + "Received response from events broker : {} {} for execution id {}. {}", + response.code(), + response.message(), + executionId, + response.body() != null ? response.body().string() : ""); + } catch (IOException e) { + log.info( + "Received response from events broker : {} {} for execution id {} " + + "but unable to serialize the response body: {}", + response.code(), + response.message(), + executionId, + e.getMessage()); + } } } } diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/DryRunNotificationAgent.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/notification/DryRunNotificationAgent.java similarity index 85% rename from echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/DryRunNotificationAgent.java rename to echo-notifications/src/main/java/com/netflix/spinnaker/echo/notification/DryRunNotificationAgent.java index 4d66d2dff..f2db4c4c7 100644 --- a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/DryRunNotificationAgent.java +++ b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/notification/DryRunNotificationAgent.java @@ -26,6 +26,7 @@ import com.netflix.spinnaker.echo.model.Trigger; import com.netflix.spinnaker.echo.pipelinetriggers.orca.OrcaService; import com.netflix.spinnaker.echo.services.Front50Service; +import com.netflix.spinnaker.kork.retrofit.Retrofit2SyncCall; import java.util.List; import java.util.Map; import java.util.Optional; @@ -64,7 +65,7 @@ public void sendNotifications( } log.info("Received dry run notification for {}", pipelineConfigId); Optional match = - front50.getPipelines(application).stream() + Retrofit2SyncCall.execute(front50.getPipelines(application)).stream() .filter(pipeline -> pipeline.getId().equals(pipelineConfigId)) .findFirst(); @@ -82,13 +83,14 @@ public void sendNotifications( .lastSuccessfulExecution(execution) .build(); OrcaService.TriggerResponse response = - orca.trigger( - pipeline - .withName(format("%s (dry run)", pipeline.getName())) - .withId(null) - .withTrigger(trigger) - .withNotifications( - mapper.convertValue(properties.getNotifications(), List.class))); + Retrofit2SyncCall.execute( + orca.trigger( + pipeline + .withName(format("%s (dry run)", pipeline.getName())) + .withId(null) + .withTrigger(trigger) + .withNotifications( + mapper.convertValue(properties.getNotifications(), List.class)))); log.info("Pipeline triggered: {}", response); } catch (Exception ex) { log.error("Error triggering dry run of {}", pipelineConfigId, ex); diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/EventContent.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/notification/EventContent.java similarity index 100% rename from echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/EventContent.java rename to echo-notifications/src/main/java/com/netflix/spinnaker/echo/notification/EventContent.java diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/GithubNotificationAgent.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/notification/GithubNotificationAgent.java similarity index 91% rename from echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/GithubNotificationAgent.java rename to echo-notifications/src/main/java/com/netflix/spinnaker/echo/notification/GithubNotificationAgent.java index 7f0ebfd9d..ddf2098e9 100644 --- a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/GithubNotificationAgent.java +++ b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/notification/GithubNotificationAgent.java @@ -16,16 +16,15 @@ package com.netflix.spinnaker.echo.notification; -import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.ImmutableMap; import com.netflix.spinnaker.echo.api.events.Event; import com.netflix.spinnaker.echo.exceptions.FieldNotFoundException; import com.netflix.spinnaker.echo.github.GithubCommit; import com.netflix.spinnaker.echo.github.GithubService; import com.netflix.spinnaker.echo.github.GithubStatus; -import com.netflix.spinnaker.echo.jackson.EchoObjectMapper; import com.netflix.spinnaker.kork.core.RetrySupport; -import java.io.IOException; +import com.netflix.spinnaker.kork.retrofit.Retrofit2SyncCall; +import com.netflix.spinnaker.kork.retrofit.exceptions.SpinnakerServerException; import java.util.Map; import java.util.Optional; import java.util.regex.Matcher; @@ -35,7 +34,6 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; -import retrofit.client.Response; @Slf4j @ConditionalOnProperty("github-status.enabled") @@ -115,7 +113,9 @@ public void sendNotifications( try { final String repo = content.getRepo(); retrySupport.retry( - () -> githubService.updateCheck("token " + token, repo, branchCommit, githubStatus), + () -> + Retrofit2SyncCall.execute( + githubService.updateCheck("token " + token, repo, branchCommit, githubStatus)), MAX_RETRY, RETRY_BACKOFF, false); @@ -128,15 +128,12 @@ public void sendNotifications( } private String getBranchCommit(String repo, String sha) { - Response response = githubService.getCommit("token " + token, repo, sha); - ObjectMapper objectMapper = EchoObjectMapper.getInstance(); - GithubCommit message = null; + GithubCommit message; try { - message = objectMapper.readValue(response.getBody().in(), GithubCommit.class); - } catch (IOException e) { + message = Retrofit2SyncCall.execute(githubService.getCommit("token " + token, repo, sha)); + } catch (SpinnakerServerException e) { return sha; } - Pattern pattern = Pattern.compile( "Merge (?[0-9a-f]{5,40}) into (?[0-9a-f]{5,40})"); @@ -144,6 +141,7 @@ private String getBranchCommit(String repo, String sha) { if (matcher.matches()) { return matcher.group("branchCommit"); } + return sha; } diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/GoogleCloudBuildNotificationAgent.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/notification/GoogleCloudBuildNotificationAgent.java similarity index 100% rename from echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/GoogleCloudBuildNotificationAgent.java rename to echo-notifications/src/main/java/com/netflix/spinnaker/echo/notification/GoogleCloudBuildNotificationAgent.java diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/HtmlToPlainText.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/notification/HtmlToPlainText.java similarity index 100% rename from echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/HtmlToPlainText.java rename to echo-notifications/src/main/java/com/netflix/spinnaker/echo/notification/HtmlToPlainText.java diff --git a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/MicrosoftTeamsNotificationAgent.java b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/notification/MicrosoftTeamsNotificationAgent.java similarity index 91% rename from echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/MicrosoftTeamsNotificationAgent.java rename to echo-notifications/src/main/java/com/netflix/spinnaker/echo/notification/MicrosoftTeamsNotificationAgent.java index 1c51718a9..e7db1753f 100644 --- a/echo-notifications/src/main/groovy/com/netflix/spinnaker/echo/notification/MicrosoftTeamsNotificationAgent.java +++ b/echo-notifications/src/main/java/com/netflix/spinnaker/echo/notification/MicrosoftTeamsNotificationAgent.java @@ -20,15 +20,15 @@ import com.netflix.spinnaker.echo.microsoftteams.MicrosoftTeamsMessage; import com.netflix.spinnaker.echo.microsoftteams.MicrosoftTeamsService; import com.netflix.spinnaker.echo.microsoftteams.api.MicrosoftTeamsSection; +import java.io.IOException; import java.util.Map; import java.util.Optional; import lombok.extern.slf4j.Slf4j; +import okhttp3.ResponseBody; import org.apache.commons.lang3.text.WordUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; -import retrofit.client.Response; -import retrofit.mime.TypedByteArray; @Slf4j @ConditionalOnProperty("microsoftteams.enabled") @@ -141,13 +141,17 @@ public void sendNotifications( String webhookUrl = Optional.ofNullable(preference).map(p -> (String) p.get("address")).orElse(null); - Response response = teamsService.sendMessage(webhookUrl, teamsMessage); - - log.info( - "Received response from Microsoft Teams Webhook : {} {} for execution id {}. {}", - response.getStatus(), - response.getReason(), - executionId, - new String(((TypedByteArray) response.getBody()).getBytes())); + ResponseBody response = teamsService.sendMessage(webhookUrl, teamsMessage); + + try { + log.info( + "Received response from Microsoft Teams Webhook for execution id {}. {}", + executionId, + response.string()); + } catch (IOException e) { + log.info( + "Received response from Microsoft Teams Webhook for execution id {} but failed to deserialize", + executionId); + } } } diff --git a/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/config/GithubConfigSpec.groovy b/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/config/GithubConfigSpec.groovy index 1f55faa42..d6d2479ff 100644 --- a/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/config/GithubConfigSpec.groovy +++ b/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/config/GithubConfigSpec.groovy @@ -1,143 +1,170 @@ package com.netflix.spinnaker.echo.config +import com.github.tomakehurst.wiremock.core.WireMockConfiguration +import com.github.tomakehurst.wiremock.WireMockServer +import com.github.tomakehurst.wiremock.client.WireMock +import com.netflix.spinnaker.config.OkHttp3ClientConfiguration +import com.netflix.spinnaker.echo.github.GithubService +import com.netflix.spinnaker.echo.test.config.Retrofit2BasicLogTestConfig +import com.netflix.spinnaker.echo.test.config.Retrofit2HeadersLogTestConfig +import com.netflix.spinnaker.echo.test.config.Retrofit2NoneLogTestConfig +import com.netflix.spinnaker.echo.test.config.Retrofit2TestConfig +import com.netflix.spinnaker.kork.retrofit.Retrofit2SyncCall import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest -import retrofit.Endpoint -import retrofit.Endpoints -import retrofit.RestAdapter -import retrofit.client.Client -import retrofit.client.Header -import retrofit.client.Response -import retrofit.mime.TypedByteArray import spock.lang.Specification -import spock.lang.Subject @SpringBootTest( - classes = [GithubConfig.class, MockRetrofitConfig.class], + classes = [Retrofit2TestConfig, Retrofit2BasicLogTestConfig], properties = ["github-status.enabled=true"], webEnvironment = SpringBootTest.WebEnvironment.NONE) -class GithubConfigSpec extends Specification { +class GithubConfigLogLevelBasicSpec extends Specification { + @Autowired - @Subject - GithubConfig githubConfig + OkHttp3ClientConfiguration okHttpClientConfig - def 'test github endpoint default is not wrapped in single quotes'() { - given: - String ownEndpoint = "'https://api.github.com'" + WireMockServer wireMockServer + GithubService ghService + PrintStream systemError + ByteArrayOutputStream testErr + int port - when: - Endpoint endpoint = githubConfig.githubEndpoint() + def setup() { + systemError = System.out; + testErr = new ByteArrayOutputStream(); + System.setOut(new PrintStream(testErr)) - then: - endpoint.url != ownEndpoint - } + wireMockServer = new WireMockServer(WireMockConfiguration.options().dynamicPort()) + wireMockServer.start() + port = wireMockServer.port() - def 'test github endpoint default is set'() { - given: - String ownEndpoint = "https://api.github.com" + wireMockServer.stubFor(WireMock.get(WireMock.urlEqualTo("/repos/repo-name/commits/sha12345")) + .willReturn(WireMock.aResponse() + .withHeader("Content-Type", "application/json") + .withBody("{\"message\": \"response\", \"code\": 200}"))); - when: - Endpoint endpoint = githubConfig.githubEndpoint() + wireMockServer.stubFor(WireMock.get(WireMock.urlEqualTo("/repos//commits/")) + .willReturn(WireMock.aResponse() + .withHeader("Content-Type", "application/json") + .withBody("{\"message\": \"response\", \"code\": 200}"))); - then: - endpoint.url == ownEndpoint - } + GithubConfig config = new GithubConfig(wireMockServer.baseUrl()) + ghService = config.githubService(okHttpClientConfig) + } - def 'default log level does not output authorization headers and matches basic API call structure'() { - given: - def systemError = System.out; - def testErr = new ByteArrayOutputStream(); - System.setOut(new PrintStream(testErr)) + def cleanup() { + wireMockServer.stop() + System.setOut(systemError) + System.out.print(testErr) + } - Client mockClient = Stub(Client) { - execute(_) >> { - return new Response("http://example.com", 200, "Success!", new ArrayList
(), new TypedByteArray("", "SOmething workedddd".bytes)) - } - } - def ghService = new GithubConfig().githubService(Endpoints.newFixedEndpoint("http://example.com"), mockClient, null) + def 'default log level does not output authorization headers and matches basic API call structure'() { when: - ghService.getCommit("SECRET", "repo-name", "sha12345"); + + Retrofit2SyncCall.execute(ghService.getCommit("SECRET", "repo-name", "sha12345")) then: def logOutput = testErr.toString() - logOutput.contains("HTTP GET http://example.com/repos/repo-name/commits/sha12345") + logOutput.contains("--> GET http://localhost:" + port + "/repos/repo-name/commits/sha12345") !logOutput.contains("SECRET") !logOutput.contains("Authorization") - cleanup: - System.setOut(systemError) - System.out.print(testErr) } +} - def 'When no log set, no log output!'() { - given: - def systemError = System.out; - def testErr = new ByteArrayOutputStream(); +@SpringBootTest( + classes = [Retrofit2TestConfig, Retrofit2NoneLogTestConfig], + properties = ["github-status.enabled=true"], + webEnvironment = SpringBootTest.WebEnvironment.NONE) +class GithubConfigLogLevelNoneSpec extends Specification { + + @Autowired + OkHttp3ClientConfiguration okHttpClientConfig + + WireMockServer wireMockServer + GithubService ghService + PrintStream systemError + ByteArrayOutputStream testErr + + def setup() { + systemError = System.out; + testErr = new ByteArrayOutputStream(); System.setOut(new PrintStream(testErr)) - Client mockClient = Stub(Client) { - execute(_) >> new Response("http://example.com", 200, "Ok", new ArrayList
(), new TypedByteArray("", "response".bytes)) - } - def ghService = new GithubConfig().githubService(Endpoints.newFixedEndpoint("http://example.com"), mockClient, RestAdapter.LogLevel.NONE) + wireMockServer = new WireMockServer(WireMockConfiguration.options().dynamicPort()) + wireMockServer.start() - when: - ghService.getCommit("", "", ""); + wireMockServer.stubFor(WireMock.get(WireMock.urlEqualTo("/repos//commits/")) + .willReturn(WireMock.aResponse() + .withHeader("Content-Type", "application/json") + .withBody("{\"message\": \"response\", \"code\": 200}"))); - then: - !testErr.toString().contains("GET") + GithubConfig config = new GithubConfig(wireMockServer.baseUrl()) + ghService = config.githubService(okHttpClientConfig) + } - cleanup: + def cleanup() { + wireMockServer.stop() System.setOut(systemError) System.out.print(testErr) } - def 'Log when full has header information and auth headers- dont do this in prod!'() { - given: - def systemError = System.out; - def testErr = new ByteArrayOutputStream(); - System.setOut(new PrintStream(testErr)) - - Client mockClient = Stub(Client) { - execute(_) >> new Response("http://example.com", 200, "Ok", new ArrayList
(), new TypedByteArray("", "response".bytes)) - } - def ghService = new GithubConfig().githubService(Endpoints.newFixedEndpoint("http://example.com"), mockClient, RestAdapter.LogLevel.FULL) - + def 'When no log set, no log output!'() { when: - ghService.getCommit("", "", ""); + Retrofit2SyncCall.execute(ghService.getCommit("", "", "")); then: - testErr.toString().contains("Authorization") - - cleanup: - System.setOut(systemError) - System.out.print(testErr) + !testErr.toString().contains("--> GET") } } @SpringBootTest( - classes = [GithubConfig.class, MockRetrofitConfig.class], - properties = [ - "github-status.enabled=true", - "github-status.endpoint=https://my.github.com" - ], + classes = [Retrofit2TestConfig, Retrofit2HeadersLogTestConfig], + properties = ["github-status.enabled=true"], webEnvironment = SpringBootTest.WebEnvironment.NONE) -class GithubConfigEndpointSetSpec extends Specification { +class GithubConfigLogLevelHeadersSpec extends Specification { + @Autowired - @Subject - GithubConfig githubConfig + OkHttp3ClientConfiguration okHttpClientConfig - def 'test github endpoint in config is set'() { - given: - String ownEndpoint = "https://my.github.com" + WireMockServer wireMockServer + GithubService ghService + PrintStream systemError + ByteArrayOutputStream testErr + def setup() { + systemError = System.out; + testErr = new ByteArrayOutputStream(); + System.setOut(new PrintStream(testErr)) + + wireMockServer = new WireMockServer(WireMockConfiguration.options().dynamicPort()) + wireMockServer.start() + + wireMockServer.stubFor(WireMock.get(WireMock.urlEqualTo("/repos//commits/")) + .willReturn(WireMock.aResponse() + .withHeader("Content-Type", "application/json") + .withBody("{\"message\": \"response\", \"code\": 200}"))); + + GithubConfig config = new GithubConfig(wireMockServer.baseUrl()) + ghService = config.githubService(okHttpClientConfig) + + } + + def cleanup() { + wireMockServer.stop() + System.setOut(systemError) + System.out.print(testErr) + } + + def 'Log when full has header information and auth headers- dont do this in prod!'() { when: - Endpoint endpoint = githubConfig.githubEndpoint() + Retrofit2SyncCall.execute(ghService.getCommit("", "", "")); then: - endpoint.url == ownEndpoint + testErr.toString().contains("Authorization") } } diff --git a/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/config/MockRetrofitConfig.groovy b/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/config/MockRetrofitConfig.groovy deleted file mode 100644 index b147a79f7..000000000 --- a/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/config/MockRetrofitConfig.groovy +++ /dev/null @@ -1,19 +0,0 @@ -package com.netflix.spinnaker.echo.config - -import org.springframework.boot.test.mock.mockito.MockBean -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import retrofit.RestAdapter -import retrofit.client.Client -import spock.lang.Specification - -@Configuration -class MockRetrofitConfig extends Specification { - @MockBean - Client client - - @Bean - RestAdapter.LogLevel getRetrofitLogLevel() { - return RestAdapter.LogLevel.BASIC - } -} diff --git a/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/config/SlackConfigSpec.groovy b/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/config/SlackConfigSpec.groovy index f123e41d6..1f9288f26 100644 --- a/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/config/SlackConfigSpec.groovy +++ b/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/config/SlackConfigSpec.groovy @@ -2,6 +2,8 @@ package com.netflix.spinnaker.echo.config import com.netflix.spinnaker.echo.slack.SlackAppService import com.netflix.spinnaker.echo.slack.SlackService +import com.netflix.spinnaker.echo.test.config.Retrofit2BasicLogTestConfig +import com.netflix.spinnaker.echo.test.config.Retrofit2TestConfig import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Qualifier import org.springframework.boot.test.context.SpringBootTest @@ -9,7 +11,7 @@ import spock.lang.Specification import spock.lang.Subject @SpringBootTest( - classes = [SlackConfig, MockRetrofitConfig], + classes = [SlackConfig, Retrofit2TestConfig, Retrofit2BasicLogTestConfig], properties = [ "slack.enabled = true", // Used for the old bot diff --git a/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/jira/JiraNotificationServiceSpec.groovy b/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/jira/JiraNotificationServiceSpec.groovy index cf940d788..e939c1987 100644 --- a/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/jira/JiraNotificationServiceSpec.groovy +++ b/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/jira/JiraNotificationServiceSpec.groovy @@ -6,6 +6,7 @@ import com.netflix.spinnaker.echo.jackson.EchoObjectMapper import com.netflix.spinnaker.kork.core.RetrySupport import spock.lang.Specification import spock.lang.Unroll +import retrofit2.mock.Calls; class JiraNotificationServiceSpec extends Specification { @@ -36,9 +37,9 @@ class JiraNotificationServiceSpec extends Specification { service.handle(notification) then: - 1 * jiraService.getIssueTransitions(_) >> new JiraService.IssueTransitions(transitions: [new JiraService.IssueTransitions.Transition(name: "Done", id: "4")]) - 1 * jiraService.transitionIssue(_, _) - addCommentCall * jiraService.addComment(_, _) + 1 * jiraService.getIssueTransitions(_) >> Calls.response(new JiraService.IssueTransitions(transitions: [new JiraService.IssueTransitions.Transition(name: "Done", id: "4")])) + 1 * jiraService.transitionIssue(_, _) >> Calls.response(null) + addCommentCall * jiraService.addComment(_, _) >> Calls.response(null) where: comment || addCommentCall diff --git a/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/notification/DryRunNotificationAgentSpec.groovy b/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/notification/DryRunNotificationAgentSpec.groovy index e84e7ba4d..fcf36f83e 100644 --- a/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/notification/DryRunNotificationAgentSpec.groovy +++ b/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/notification/DryRunNotificationAgentSpec.groovy @@ -20,6 +20,7 @@ import com.netflix.spinnaker.echo.api.events.Event import com.netflix.spinnaker.echo.model.Pipeline import com.netflix.spinnaker.echo.pipelinetriggers.orca.OrcaService import com.netflix.spinnaker.echo.services.Front50Service +import retrofit2.mock.Calls import spock.lang.Specification import spock.lang.Subject import spock.lang.Unroll @@ -71,11 +72,13 @@ class DryRunNotificationAgentSpec extends Specification { def "triggers a pipeline run for a pipeline:complete notification"() { given: - front50.getPipelines(application) >> [pipeline] + front50.getPipelines(application) >> Calls.response([pipeline]) and: - def captor = new BlockingVariable() - orca.trigger(_) >> { captor.set(it[0]) } + def captor = new BlockingVariable(5) + orca.trigger(_) >> { captor.set(it[0]) + Calls.response(null) + } when: agent.processEvent(event) @@ -118,7 +121,7 @@ class DryRunNotificationAgentSpec extends Specification { def "adds notifications to triggered pipeline"() { given: - front50.getPipelines(application) >> [pipeline] + front50.getPipelines(application) >> Calls.response([pipeline]) and: properties.notifications = [ @@ -131,8 +134,10 @@ class DryRunNotificationAgentSpec extends Specification { ] and: - def captor = new BlockingVariable() - orca.trigger(_) >> { captor.set(it[0]) } + def captor = new BlockingVariable(5) + orca.trigger(_) >> { captor.set(it[0]) + Calls.response(null) + } when: agent.processEvent(event) diff --git a/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/notification/GithubNotificationAgentSpec.groovy b/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/notification/GithubNotificationAgentSpec.groovy index 5b7240f75..9844b6458 100644 --- a/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/notification/GithubNotificationAgentSpec.groovy +++ b/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/notification/GithubNotificationAgentSpec.groovy @@ -16,16 +16,14 @@ package com.netflix.spinnaker.echo.notification -import com.fasterxml.jackson.databind.ObjectMapper import com.netflix.spinnaker.echo.github.GithubCommitDetail import com.netflix.spinnaker.echo.github.GithubCommit import com.netflix.spinnaker.echo.github.GithubService import com.netflix.spinnaker.echo.github.GithubStatus import com.netflix.spinnaker.echo.api.events.Event -import com.netflix.spinnaker.echo.jackson.EchoObjectMapper -import retrofit.RetrofitError -import retrofit.client.Response -import retrofit.mime.TypedByteArray +import com.netflix.spinnaker.kork.retrofit.exceptions.SpinnakerNetworkException +import okhttp3.Request +import retrofit2.mock.Calls import spock.lang.Specification import spock.lang.Subject import spock.lang.Unroll @@ -49,10 +47,10 @@ class GithubNotificationAgentSpec extends Specification { def actualMessage = new BlockingVariable() github.updateCheck(*_) >> { token, repo, sha, status_local -> actualMessage.set(status_local) + Calls.response(null) } - github.getCommit(*_) >> { token, repo, sha -> - new Response("url", 200, "nothing", [], new TypedByteArray("application/json", "message".bytes)) - } + + github.getCommit(*_) >> Calls.response(new GithubCommit(new GithubCommitDetail("some message"))) when: agent.sendNotifications(null, application, event, [type: type], status) @@ -98,10 +96,9 @@ class GithubNotificationAgentSpec extends Specification { def actualMessage = new BlockingVariable() github.updateCheck(*_) >> { token, repo, sha, status_local -> actualMessage.set(status_local) + Calls.response(null) } - github.getCommit(*_) >> { token, repo, sha -> - new Response("url", 200, "nothing", [], new TypedByteArray("application/json", "message".bytes)) - } + github.getCommit(*_) >> Calls.response(new GithubCommit(new GithubCommitDetail("some message"))) when: agent.sendNotifications(null, application, event, [type: type], status) @@ -153,17 +150,13 @@ class GithubNotificationAgentSpec extends Specification { def "if commit is a merge commit of the head branch and the default branch then return the head commit"() { given: GithubCommit commit = new GithubCommit(new GithubCommitDetail(commitMessage)) - ObjectMapper mapper = EchoObjectMapper.getInstance() - String response = mapper.writeValueAsString(commit) - github.getCommit(*_) >> { token, repo, sha -> - new Response("url", 200, "nothing", [], new TypedByteArray("application/json", response.bytes)) - } + github.getCommit(*_) >> Calls.response(commit) when: agent.sendNotifications(null, application, event, [type: type], status) then: - 1 * github.updateCheck(_, _, expectedSha, _) + 1 * github.updateCheck(_, _, expectedSha, _) >> Calls.response(null) where: commitMessage || expectedSha @@ -197,15 +190,13 @@ class GithubNotificationAgentSpec extends Specification { def "retries if updating the github check fails"() { given: - github.getCommit(*_) >> { token, repo, sha -> - new Response("url", 200, "nothing", [], new TypedByteArray("application/json", "message".bytes)) - } + github.getCommit(*_) >> Calls.response(new GithubCommit(new GithubCommitDetail("message"))) when: agent.sendNotifications(null, application, event, [type: type], status) then: - 5 * github.updateCheck(_, _, _, _) >> RetrofitError.networkError("timeout", new IOException()) + 5 * github.updateCheck(_, _, _, _) >> new SpinnakerNetworkException(new IOException("timeout"), new Request.Builder().url("http://some-url").build()) where: application = "whatever" diff --git a/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/notification/NotificationControllerSpec.groovy b/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/notification/NotificationControllerSpec.groovy index 4215d58e6..c16a3ec49 100644 --- a/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/notification/NotificationControllerSpec.groovy +++ b/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/notification/NotificationControllerSpec.groovy @@ -23,19 +23,18 @@ import com.netflix.spinnaker.echo.api.events.NotificationParameter import com.netflix.spinnaker.echo.controller.NotificationController import com.netflix.spinnaker.echo.notification.InteractiveNotificationCallbackHandler.SpinnakerService import com.netflix.spinnaker.kork.web.exceptions.InvalidRequestException +import okhttp3.MediaType +import okhttp3.ResponseBody import org.springframework.core.env.Environment import org.springframework.http.HttpHeaders import org.springframework.http.HttpMethod import org.springframework.http.HttpStatus import org.springframework.http.RequestEntity import org.springframework.http.ResponseEntity -import retrofit.client.Response -import retrofit.mime.TypedByteArray +import retrofit2.mock.Calls import spock.lang.Specification import spock.lang.Subject -import static java.util.Collections.emptyList - class NotificationControllerSpec extends Specification { static final String INTERACTIVE_CALLBACK_URI = "/notifications/callbacks" SpinnakerService spinnakerService @@ -128,7 +127,7 @@ class NotificationControllerSpec extends Specification { callbackObject.user = "john.doe" interactiveNotificationService.supportsType("SLACK") >> true - spinnakerService.notificationCallback(*_) >> { mockResponse() } + spinnakerService.notificationCallback(*_) >> Calls.response(mockResponse()) when: notificationController.processCallback("slack", request) @@ -169,7 +168,7 @@ class NotificationControllerSpec extends Specification { callbackObject.user = "john.doe" interactiveNotificationService.supportsType("SLACK") >> true - spinnakerService.notificationCallback(*_) >> { mockResponse() } + spinnakerService.notificationCallback(*_) >> Calls.response(mockResponse()) when: ResponseEntity response = notificationController.processCallback("slack", request) @@ -189,8 +188,8 @@ class NotificationControllerSpec extends Specification { response[0] instanceof MyNotificationAgent } - static Response mockResponse() { - new Response("url", 200, "nothing", emptyList(), new TypedByteArray("application/json", "response".bytes)) + static ResponseBody mockResponse() { + ResponseBody.create("{}", MediaType.parse("application/json")) } static class MyNotificationAgent implements NotificationAgent { diff --git a/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/notification/NotificationServiceSpec.groovy b/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/notification/NotificationServiceSpec.groovy index 792d5e359..72e2e63ca 100644 --- a/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/notification/NotificationServiceSpec.groovy +++ b/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/notification/NotificationServiceSpec.groovy @@ -21,6 +21,7 @@ import com.netflix.spinnaker.echo.twilio.TwilioNotificationService import com.netflix.spinnaker.echo.twilio.TwilioService import org.springframework.boot.autoconfigure.freemarker.FreeMarkerNonWebConfiguration import org.springframework.boot.autoconfigure.freemarker.FreeMarkerProperties +import retrofit2.mock.Calls import spock.lang.Shared import spock.lang.Specification @@ -59,6 +60,6 @@ class NotificationServiceSpec extends Specification { twilioNotificationService.handle(notification) then: - 1 * twilioService.sendMessage("account", "222-333-4444", "111-222-3333", "generic SPINNAKER_URL application") + 1 * twilioService.sendMessage("account", "222-333-4444", "111-222-3333", "generic SPINNAKER_URL application") >> Calls.response(null) } } diff --git a/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/slack/SlackInteractiveNotificationServiceSpec.groovy b/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/slack/SlackInteractiveNotificationServiceSpec.groovy index cb09d7c4b..ad2b9717f 100644 --- a/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/slack/SlackInteractiveNotificationServiceSpec.groovy +++ b/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/slack/SlackInteractiveNotificationServiceSpec.groovy @@ -20,9 +20,12 @@ import com.netflix.spinnaker.echo.api.Notification import com.netflix.spinnaker.echo.jackson.EchoObjectMapper import com.netflix.spinnaker.echo.notification.NotificationTemplateEngine import com.netflix.spinnaker.kork.web.exceptions.InvalidRequestException +import okhttp3.MediaType +import okhttp3.ResponseBody import org.springframework.http.HttpHeaders import org.springframework.http.HttpMethod import org.springframework.http.RequestEntity +import retrofit2.mock.Calls import spock.lang.Specification import spock.lang.Subject @@ -154,6 +157,6 @@ class SlackInteractiveNotificationServiceSpec extends Specification { service.respondToCallback(request) then: - 1 * slackHookService.respondToMessage("/actions/T00000000/0123456789/abcdefgh1234567", expectedResponse) + 1 * slackHookService.respondToMessage("/actions/T00000000/0123456789/abcdefgh1234567", expectedResponse) >> Calls.response(ResponseBody.create("{}", MediaType.parse("application/json"))) } } diff --git a/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/slack/SlackServiceSpec.groovy b/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/slack/SlackServiceSpec.groovy index 9318cf522..d7c63108f 100644 --- a/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/slack/SlackServiceSpec.groovy +++ b/echo-notifications/src/test/groovy/com/netflix/spinnaker/echo/slack/SlackServiceSpec.groovy @@ -1,18 +1,27 @@ package com.netflix.spinnaker.echo.slack +import com.github.tomakehurst.wiremock.WireMockServer +import com.github.tomakehurst.wiremock.core.WireMockConfiguration +import com.github.tomakehurst.wiremock.client.WireMock +import com.github.tomakehurst.wiremock.http.RequestListener +import com.netflix.spinnaker.config.OkHttp3ClientConfiguration import com.netflix.spinnaker.echo.api.Notification +import com.netflix.spinnaker.echo.test.config.Retrofit2BasicLogTestConfig +import com.netflix.spinnaker.echo.test.config.Retrofit2TestConfig import com.netflix.spinnaker.echo.config.SlackAppProperties import com.netflix.spinnaker.echo.config.SlackConfig import com.netflix.spinnaker.echo.config.SlackLegacyProperties import com.netflix.spinnaker.kork.web.exceptions.InvalidRequestException import groovy.json.JsonSlurper + +import java.nio.charset.Charset import org.apache.http.NameValuePair import org.apache.http.client.utils.URLEncodedUtils +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest import org.springframework.http.HttpHeaders import org.springframework.http.HttpMethod import org.springframework.http.RequestEntity -import retrofit.client.Client -import retrofit.client.Request import retrofit.client.Response import retrofit.mime.TypedByteArray import retrofit.mime.TypedOutput @@ -20,42 +29,67 @@ import spock.lang.Specification import spock.lang.Subject import spock.util.concurrent.BlockingVariable -import java.nio.charset.Charset - import static java.util.Collections.emptyList -import static retrofit.RestAdapter.LogLevel +@SpringBootTest(classes = [Retrofit2TestConfig, Retrofit2BasicLogTestConfig], + properties = ["slack.enabled=true"], + webEnvironment = SpringBootTest.WebEnvironment.NONE) class SlackServiceSpec extends Specification { @Subject slackConfig = new SlackConfig() - @Subject mockHttpClient @Subject BlockingVariable actualUrl @Subject BlockingVariable actualPayload SlackLegacyProperties configProperties SlackAppProperties appProperties + WireMockServer wireMockServer - def setup() { - actualUrl = new BlockingVariable() - actualPayload = new BlockingVariable() + @Autowired + OkHttp3ClientConfiguration okHttpClientConfig - mockHttpClient = Mock(Client) - // intercepting the HTTP call - mockHttpClient.execute(*_) >> { Request request -> - actualUrl.set(request.url) - actualPayload.set(getString(request.body)) - mockResponse() - } + def setup() { + actualUrl = new BlockingVariable(5) + actualPayload = new BlockingVariable(5) configProperties = new SlackLegacyProperties() appProperties = new SlackAppProperties() + + wireMockServer = new WireMockServer(WireMockConfiguration.options().dynamicPort()); + + wireMockServer.addMockServiceRequestListener(new RequestListener() { + + @Override + void requestReceived(com.github.tomakehurst.wiremock.http.Request request, com.github.tomakehurst.wiremock.http.Response response) { + actualUrl.set(request.absoluteUrl) + actualPayload.set(request.bodyAsString) + } + }); + + wireMockServer.start(); + configProperties.baseUrl = wireMockServer.baseUrl() + + wireMockServer.stubFor(WireMock.post(WireMock.urlEqualTo("/NEW/TYPE/TOKEN")) + .willReturn(WireMock.aResponse() + .withHeader("Content-Type", "application/json") + .withBody("{\"message\": \"response\", \"code\": 200}"))) + + wireMockServer.stubFor(WireMock.post(WireMock.urlEqualTo("/api/chat.postMessage")) + .willReturn(WireMock.aResponse() + .withHeader("Content-Type", "application/json") + .withBody("{\"message\": \"response\", \"code\": 200}"))) + } + + def cleanup(){ + wireMockServer.stop() } def 'test sending Slack notification using incoming web hook'() { - given: "a SlackService configured to send using a mocked HTTP client and useIncomingHook=true" + given: "a SlackService configured to send using a mocked response and useIncomingHook=true" configProperties.forceUseIncomingWebhook = true - configProperties.token = token + configProperties.token = "NEW/TYPE/TOKEN" + + def expectedUrl = wireMockServer.baseUrl() + "/NEW/TYPE/TOKEN" - def slackService = slackConfig.slackService(configProperties, mockHttpClient, LogLevel.FULL) + def slackService = slackConfig.slackService(configProperties, okHttpClientConfig) when: "sending a notification" slackService.sendMessage(new SlackAttachment("Title", "the text"), "#testing", true) @@ -70,9 +104,6 @@ class SlackServiceSpec extends Specification { responseJson.attachments[0]["mrkdwn_in"] == ["text"] responseJson.channel == "#testing" - where: - token | expectedUrl - "NEW/TYPE/TOKEN" | "https://hooks.slack.com/services/NEW/TYPE/TOKEN" } @@ -80,9 +111,11 @@ class SlackServiceSpec extends Specification { given: "a SlackService configured to send using a mocked HTTP client and useIncomingHook=false" configProperties.forceUseIncomingWebhook = false - configProperties.token = token + configProperties.token = "oldStyleToken" - def slackService = slackConfig.slackService(configProperties, mockHttpClient, LogLevel.FULL) + def expectedUrl = wireMockServer.baseUrl() + "/api/chat.postMessage" + + def slackService = slackConfig.slackService(configProperties, okHttpClientConfig) when: "sending a notification" slackService.sendMessage(new SlackAttachment("Title", "the text"), "#testing", true) @@ -105,10 +138,6 @@ class SlackServiceSpec extends Specification { attachmentsJson[0]["mrkdwn_in"] == ["text"] channelField.value == "#testing" asUserField.value == "true" - - where: - token | expectedUrl - "oldStyleToken" | "https://slack.com/api/chat.postMessage" } def 'sending an interactive Slack notification'() { @@ -117,7 +146,7 @@ class SlackServiceSpec extends Specification { configProperties.forceUseIncomingWebhook = false configProperties.token = "shhh" - def slackService = slackConfig.slackService(configProperties, mockHttpClient, LogLevel.FULL) + def slackService = slackConfig.slackService(configProperties, okHttpClientConfig) when: "sending a notification with interactive actions" slackService.sendMessage( @@ -154,7 +183,7 @@ class SlackServiceSpec extends Specification { given: "a SlackAppService configured with a signing secret and an incoming callback" appProperties.signingSecret = "d41090bb6ec741bb9f68f4d77d34fa0ad897c5af" - def slackAppService = slackConfig.slackAppService(appProperties, mockHttpClient, LogLevel.FULL) + def slackAppService = slackConfig.slackAppService(appProperties, okHttpClientConfig) String timestamp = "1581528126" String payload = getClass().getResource("/slack/callbackRequestBody.txt").text diff --git a/echo-pipelinetriggers/echo-pipelinetriggers.gradle b/echo-pipelinetriggers/echo-pipelinetriggers.gradle index ebf04cd67..45843f0e1 100644 --- a/echo-pipelinetriggers/echo-pipelinetriggers.gradle +++ b/echo-pipelinetriggers/echo-pipelinetriggers.gradle @@ -19,9 +19,9 @@ dependencies { implementation "io.spinnaker.kork:kork-artifacts" implementation "io.spinnaker.fiat:fiat-api:$fiatVersion" implementation "io.spinnaker.fiat:fiat-core:$fiatVersion" - implementation "com.squareup.retrofit:retrofit" - implementation "com.squareup.retrofit:converter-jackson" - implementation "com.jakewharton.retrofit:retrofit1-okhttp3-client:1.1.0" + implementation "com.squareup.retrofit2:retrofit" + implementation "com.squareup.retrofit2:converter-jackson" + implementation "io.spinnaker.kork:kork-retrofit" implementation "io.spinnaker.kork:kork-web" implementation project(':echo-core') @@ -45,6 +45,7 @@ dependencies { testImplementation "org.junit.jupiter:junit-jupiter-api" testImplementation "org.assertj:assertj-core" testImplementation "org.apache.groovy:groovy-json" + testImplementation "com.squareup.retrofit2:retrofit-mock" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine" } diff --git a/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/config/IgorConfig.java b/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/config/IgorConfig.java index 344cc11ac..66f7365a6 100644 --- a/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/config/IgorConfig.java +++ b/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/config/IgorConfig.java @@ -16,49 +16,32 @@ package com.netflix.spinnaker.echo.config; -import com.jakewharton.retrofit.Ok3Client; -import com.netflix.spinnaker.config.DefaultServiceEndpoint; -import com.netflix.spinnaker.config.okhttp3.OkHttpClientProvider; +import com.netflix.spinnaker.config.OkHttp3ClientConfiguration; import com.netflix.spinnaker.echo.services.IgorService; -import com.netflix.spinnaker.okhttp.SpinnakerRequestInterceptor; -import com.netflix.spinnaker.retrofit.Slf4jRetrofitLogger; +import com.netflix.spinnaker.kork.retrofit.ErrorHandlingExecutorCallAdapterFactory; import lombok.extern.slf4j.Slf4j; 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.Builder; -import retrofit.RestAdapter.LogLevel; -import retrofit.converter.JacksonConverter; +import retrofit2.Retrofit; +import retrofit2.converter.jackson.JacksonConverterFactory; @Configuration @ConditionalOnProperty("igor.enabled") @Slf4j public class IgorConfig { - @Bean - public Endpoint igorEndpoint(@Value("${igor.base-url}") String igorBaseUrl) { - return Endpoints.newFixedEndpoint(igorBaseUrl); - } @Bean public IgorService igorService( - Endpoint igorEndpoint, - OkHttpClientProvider clientProvider, - LogLevel retrofitLogLevel, - SpinnakerRequestInterceptor spinnakerRequestInterceptor) { + @Value("${igor.base-url}") String igorBaseUrl, + OkHttp3ClientConfiguration okHttp3ClientConfiguration) { log.info("igor service loaded"); - return new Builder() - .setEndpoint(igorEndpoint) - .setConverter(new JacksonConverter()) - .setClient( - new Ok3Client( - clientProvider.getClient( - new DefaultServiceEndpoint("igor", igorEndpoint.getUrl())))) - .setRequestInterceptor(spinnakerRequestInterceptor) - .setLogLevel(retrofitLogLevel) - .setLog(new Slf4jRetrofitLogger(IgorService.class)) + return new Retrofit.Builder() + .baseUrl(igorBaseUrl) + .client(okHttp3ClientConfiguration.createForRetrofit2().build()) + .addCallAdapterFactory(ErrorHandlingExecutorCallAdapterFactory.getInstance()) + .addConverterFactory(JacksonConverterFactory.create()) .build() .create(IgorService.class); } diff --git a/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/config/PipelineTriggerConfiguration.java b/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/config/PipelineTriggerConfiguration.java index 4663bf868..c5faf6ee1 100644 --- a/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/config/PipelineTriggerConfiguration.java +++ b/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/config/PipelineTriggerConfiguration.java @@ -1,10 +1,8 @@ package com.netflix.spinnaker.echo.config; import com.fasterxml.jackson.databind.ObjectMapper; -import com.jakewharton.retrofit.Ok3Client; import com.netflix.spectator.api.Registry; -import com.netflix.spinnaker.config.DefaultServiceEndpoint; -import com.netflix.spinnaker.config.okhttp3.OkHttpClientProvider; +import com.netflix.spinnaker.config.OkHttp3ClientConfiguration; import com.netflix.spinnaker.echo.jackson.EchoObjectMapper; import com.netflix.spinnaker.echo.pipelinetriggers.PipelineCacheConfigurationProperties; import com.netflix.spinnaker.echo.pipelinetriggers.eventhandlers.PubsubEventHandler; @@ -14,7 +12,7 @@ import com.netflix.spinnaker.fiat.shared.FiatStatus; import com.netflix.spinnaker.kork.dynamicconfig.DynamicConfigService; import com.netflix.spinnaker.kork.expressions.config.ExpressionProperties; -import com.netflix.spinnaker.retrofit.Slf4jRetrofitLogger; +import com.netflix.spinnaker.kork.retrofit.ErrorHandlingExecutorCallAdapterFactory; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import lombok.extern.slf4j.Slf4j; @@ -24,10 +22,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; -import retrofit.RequestInterceptor; -import retrofit.RestAdapter; -import retrofit.RestAdapter.LogLevel; -import retrofit.converter.JacksonConverter; +import retrofit2.Retrofit; +import retrofit2.converter.jackson.JacksonConverterFactory; @Slf4j @Configuration @@ -39,20 +35,14 @@ ExpressionProperties.class }) public class PipelineTriggerConfiguration { - private OkHttpClientProvider clientProvider; - private RequestInterceptor requestInterceptor; + private OkHttp3ClientConfiguration okHttp3ClientConfiguration; @Value("${trigger.git.shared-secret:}") private String gitSharedSecret; @Autowired - public void setRequestInterceptor(RequestInterceptor spinnakerRequestInterceptor) { - this.requestInterceptor = spinnakerRequestInterceptor; - } - - @Autowired - public void setRetrofitClient(OkHttpClientProvider clientProvider) { - this.clientProvider = clientProvider; + public void setOkHttp3ClientConfiguration(OkHttp3ClientConfiguration okHttp3ClientConfiguration) { + this.okHttp3ClientConfiguration = okHttp3ClientConfiguration; } public String getGitSharedSecret() { @@ -89,14 +79,11 @@ public ExecutorService executorService( private T bindRetrofitService(final Class type, final String endpoint) { log.info("Connecting {} to {}", type.getSimpleName(), endpoint); - return new RestAdapter.Builder() - .setClient( - new Ok3Client(clientProvider.getClient(new DefaultServiceEndpoint("orca", endpoint)))) - .setRequestInterceptor(requestInterceptor) - .setConverter(new JacksonConverter(EchoObjectMapper.getInstance())) - .setEndpoint(endpoint) - .setLogLevel(LogLevel.BASIC) - .setLog(new Slf4jRetrofitLogger(type)) + return new Retrofit.Builder() + .baseUrl(endpoint) + .client(okHttp3ClientConfiguration.createForRetrofit2().build()) + .addCallAdapterFactory(ErrorHandlingExecutorCallAdapterFactory.getInstance()) + .addConverterFactory(JacksonConverterFactory.create(EchoObjectMapper.getInstance())) .build() .create(type); } diff --git a/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/PipelineCache.java b/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/PipelineCache.java index 05a34ea2a..7ad2f87ee 100644 --- a/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/PipelineCache.java +++ b/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/PipelineCache.java @@ -27,6 +27,7 @@ import com.netflix.spinnaker.echo.pipelinetriggers.eventhandlers.TriggerEventHandler; import com.netflix.spinnaker.echo.pipelinetriggers.orca.OrcaService; import com.netflix.spinnaker.echo.services.Front50Service; +import com.netflix.spinnaker.kork.retrofit.Retrofit2SyncCall; import com.netflix.spinnaker.security.AuthenticatedRequest; import java.time.Duration; import java.time.Instant; @@ -233,10 +234,13 @@ private List> fetchRawPipelines() { AuthenticatedRequest.allowAnonymous( () -> { if (pipelineCacheConfigurationProperties.isFilterFront50Pipelines()) { - return front50.getPipelines( - true /* enabledPipelines */, true /* enabledTriggers */, supportedTriggerTypes); + return Retrofit2SyncCall.execute( + front50.getPipelines( + true /* enabledPipelines */, + true /* enabledTriggers */, + supportedTriggerTypes)); } - return front50.getPipelines(); + return Retrofit2SyncCall.execute(front50.getPipelines()); }); return (rawPipelines == null) ? Collections.emptyList() : rawPipelines; } @@ -288,7 +292,7 @@ private static Map> extractEnabledTriggersFrom(List pipeline = front50.getPipeline(cached.getId()); + Map pipeline = Retrofit2SyncCall.execute(front50.getPipeline(cached.getId())); Optional processed = process(pipeline); if (processed.isEmpty()) { log.warn( @@ -312,7 +316,7 @@ public Pipeline refresh(Pipeline cached) { */ public Optional getPipelineById(String id) { try { - Map pipeline = front50.getPipeline(id); + Map pipeline = Retrofit2SyncCall.execute(front50.getPipeline(id)); Optional processed = process(pipeline); if (processed.isEmpty()) { log.warn("Failed to process raw pipeline id {}, latestVersion={}", id, pipeline); @@ -333,7 +337,8 @@ public Optional getPipelineById(String id) { */ public Optional getPipelineByName(String application, String name) { try { - Map pipeline = front50.getPipelineByName(application, name); + Map pipeline = + Retrofit2SyncCall.execute(front50.getPipelineByName(application, name)); Optional processed = process(pipeline); if (processed.isEmpty()) { log.warn( @@ -360,7 +365,8 @@ private Map planPipelineIfNeeded( Map pipeline, Predicate> isV2Pipeline) { if (isV2Pipeline.test(pipeline)) { try { - return AuthenticatedRequest.allowAnonymous(() -> orca.v2Plan(pipeline)); + return AuthenticatedRequest.allowAnonymous( + () -> Retrofit2SyncCall.execute(orca.v2Plan(pipeline))); } catch (Exception e) { // Don't fail the entire cache cycle if we fail a plan. log.error("Caught exception while planning templated pipeline: {}", pipeline, e); diff --git a/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/ManualEventHandler.java b/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/ManualEventHandler.java index 8cd07c0a3..c7c20e6fe 100644 --- a/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/ManualEventHandler.java +++ b/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/ManualEventHandler.java @@ -29,6 +29,7 @@ import com.netflix.spinnaker.echo.model.trigger.ManualEvent.Content; import com.netflix.spinnaker.echo.pipelinetriggers.PipelineCache; import com.netflix.spinnaker.kork.artifacts.model.Artifact; +import com.netflix.spinnaker.kork.retrofit.exceptions.SpinnakerHttpException; import java.util.*; import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; @@ -39,7 +40,6 @@ import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; -import retrofit.RetrofitError; /** * Implementation of TriggerEventHandler for events of type {@link ManualEvent}, which occur when a @@ -276,9 +276,8 @@ protected List resolveArtifacts(List> manualTrigge .getArtifactByVersion( artifact.getLocation(), artifact.getName(), artifact.getVersion()); resolvedArtifacts.add(resolvedArtifact); - } catch (RetrofitError e) { - if (e.getResponse() != null - && e.getResponse().getStatus() == HttpStatus.NOT_FOUND.value()) { + } catch (SpinnakerHttpException e) { + if (e.getResponseCode() == HttpStatus.NOT_FOUND.value()) { log.error( "Artifact " + artifact.getName() diff --git a/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/orca/OrcaService.java b/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/orca/OrcaService.java index 5c7848f53..10274da69 100644 --- a/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/orca/OrcaService.java +++ b/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/orca/OrcaService.java @@ -24,28 +24,29 @@ import java.util.Map; import lombok.Getter; import lombok.NoArgsConstructor; -import retrofit.client.Response; -import retrofit.http.Body; -import retrofit.http.GET; -import retrofit.http.POST; -import retrofit.http.Query; +import okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.GET; +import retrofit2.http.POST; +import retrofit2.http.Query; public interface OrcaService { @POST("/orchestrate") - TriggerResponse trigger(@Body Pipeline pipeline); + Call trigger(@Body Pipeline pipeline); @POST("/fail") - Response recordFailure(@Body Pipeline pipeline); + Call recordFailure(@Body Pipeline pipeline); @POST("/plan") - Map plan(@Body Map pipelineConfig, @Query("resolveArtifacts") boolean resolveArtifacts); + Call plan(@Body Map pipelineConfig, @Query("resolveArtifacts") boolean resolveArtifacts); @POST("/v2/pipelineTemplates/plan") - Map v2Plan(@Body Map pipelineConfig); + Call> v2Plan(@Body Map pipelineConfig); @GET("/pipelines") - Collection getLatestPipelineExecutions( + Call> getLatestPipelineExecutions( @Query("pipelineConfigIds") Collection pipelineIds, @Query("limit") Integer limit); class TriggerResponse { diff --git a/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/orca/PipelineInitiator.java b/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/orca/PipelineInitiator.java index a6ba7caaa..124f9f861 100644 --- a/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/orca/PipelineInitiator.java +++ b/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/orca/PipelineInitiator.java @@ -29,6 +29,9 @@ import com.netflix.spinnaker.fiat.shared.FiatStatus; import com.netflix.spinnaker.kork.discovery.DiscoveryStatusListener; import com.netflix.spinnaker.kork.dynamicconfig.DynamicConfigService; +import com.netflix.spinnaker.kork.retrofit.Retrofit2SyncCall; +import com.netflix.spinnaker.kork.retrofit.exceptions.SpinnakerHttpException; +import com.netflix.spinnaker.kork.retrofit.exceptions.SpinnakerServerException; import com.netflix.spinnaker.security.AuthenticatedRequest; import java.util.Collection; import java.util.Collections; @@ -45,12 +48,7 @@ import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; -import retrofit.RetrofitError; -import retrofit.RetrofitError.Kind; -import retrofit.client.Response; -import retrofit.mime.TypedByteArray; /** Triggers a {@link Pipeline} by invoking _Orca_. */ @Component @@ -115,7 +113,7 @@ public enum TriggerSource { } public void recordPipelineFailure(Pipeline pipeline) { - orca.recordFailure(pipeline); + Retrofit2SyncCall.execute(orca.recordFailure(pipeline)); } public void startPipeline(Pipeline pipeline, TriggerSource triggerSource) { @@ -174,16 +172,14 @@ public void startPipeline(Pipeline pipeline, TriggerSource triggerSource) { try { Map pipelineToPlan = objectMapper.convertValue(pipeline, Map.class); Map resolvedPipelineMap = - AuthenticatedRequest.allowAnonymous(() -> orca.plan(pipelineToPlan, true)); + AuthenticatedRequest.allowAnonymous( + () -> Retrofit2SyncCall.execute(orca.plan(pipelineToPlan, true))); pipeline = objectMapper.convertValue(resolvedPipelineMap, Pipeline.class); - } catch (RetrofitError e) { - String orcaResponse = "N/A"; - - if (e.getResponse() != null && e.getResponse().getBody() != null) { - orcaResponse = new String(((TypedByteArray) e.getResponse().getBody()).getBytes()); - } - - log.error("Failed planning {}: \n{}", pipeline, orcaResponse); + } catch (SpinnakerServerException e) { + log.error( + "Failed planning {}: \n{}", + pipeline, + e.getMessage() == null ? "N/A" : e.getMessage()); // Continue anyway, so that the execution will appear in Deck pipeline = pipeline.withPlan(false); @@ -260,22 +256,16 @@ private Void triggerPipelineImpl(Pipeline pipeline, TriggerSource triggerSource) "triggerType", getTriggerType(pipeline)) .increment(); - } catch (RetrofitError e) { + } catch (SpinnakerHttpException e) { String orcaResponse = "N/A"; - int status = 0; - - if (e.getResponse() != null) { - status = e.getResponse().getStatus(); - - if (e.getResponse().getBody() != null) { - orcaResponse = new String(((TypedByteArray) e.getResponse().getBody()).getBytes()); - } + if (e.getResponseBody() != null) { + orcaResponse = e.getResponseBody().toString(); } log.error( "Failed to trigger {} HTTP: {}\norca error: {}\npayload: {}", pipeline, - status, + e.getResponseCode(), orcaResponse, pipelineAsString(pipeline)); @@ -296,9 +286,9 @@ private TriggerResponse triggerWithRetries(Pipeline pipeline) { while (true) { try { attempts++; - return orca.trigger(pipeline); - } catch (RetrofitError e) { - if ((attempts >= retryCount) || !isRetryableError(e)) { + return Retrofit2SyncCall.execute(orca.trigger(pipeline)); + } catch (SpinnakerServerException e) { + if ((attempts >= retryCount) || (e.getRetryable() != null && !e.getRetryable())) { throw e; } else { log.warn( @@ -410,22 +400,4 @@ private boolean isEnabled(TriggerSource triggerSource) { return triggerEnabled && dynamicConfigService.isEnabled("orca", true); } - - private static boolean isRetryableError(Throwable error) { - if (!(error instanceof RetrofitError)) { - return false; - } - RetrofitError retrofitError = (RetrofitError) error; - - if (retrofitError.getKind() == Kind.NETWORK) { - return true; - } - - if (retrofitError.getKind() == Kind.HTTP) { - Response response = retrofitError.getResponse(); - return (response != null && response.getStatus() != HttpStatus.BAD_REQUEST.value()); - } - - return false; - } } diff --git a/echo-pipelinetriggers/src/test/groovy/com/netflix/spinnaker/echo/pipelinetriggers/PipelineCacheSpec.groovy b/echo-pipelinetriggers/src/test/groovy/com/netflix/spinnaker/echo/pipelinetriggers/PipelineCacheSpec.groovy index ff26df0a6..f407b2ca9 100644 --- a/echo-pipelinetriggers/src/test/groovy/com/netflix/spinnaker/echo/pipelinetriggers/PipelineCacheSpec.groovy +++ b/echo-pipelinetriggers/src/test/groovy/com/netflix/spinnaker/echo/pipelinetriggers/PipelineCacheSpec.groovy @@ -27,6 +27,7 @@ import com.netflix.spinnaker.echo.pipelinetriggers.eventhandlers.BaseTriggerEven import com.netflix.spinnaker.echo.pipelinetriggers.orca.OrcaService import com.netflix.spinnaker.echo.services.Front50Service import com.netflix.spinnaker.echo.test.RetrofitStubs +import retrofit2.mock.Calls import spock.lang.Unroll import spock.lang.Shared import spock.lang.Specification @@ -71,7 +72,7 @@ class PipelineCacheSpec extends Specification implements RetrofitStubs { def pipeline = Pipeline.builder().application('application').name('Pipeline').id('P1').build() def initialLoad = [] - front50.getPipelines(true, true, supportedTriggers) >> initialLoad >> { throw unavailable() } >> [pipelineMap] + front50.getPipelines(true, true, supportedTriggers) >> Calls.response(initialLoad) >> { throw unavailable() } >> Calls.response([pipelineMap]) pipelineCache.start() expect: 'null pipelines when we have not polled yet' @@ -107,9 +108,9 @@ class PipelineCacheSpec extends Specification implements RetrofitStubs { then: if (filterFront50Pipelines) { - 1 * front50.getPipelines(true, true, supportedTriggers) >> [] // arbitrary return value + 1 * front50.getPipelines(true, true, supportedTriggers) >> Calls.response([]) // arbitrary return value } else { - 1 * front50.getPipelines() >> [] // arbitrary return value + 1 * front50.getPipelines() >> Calls.response([]) // arbitrary return value } 0 * front50._ @@ -133,7 +134,7 @@ class PipelineCacheSpec extends Specification implements RetrofitStubs { Optional result = pipelineCache.getPipelineById(pipelineId) then: - 1 * front50.getPipeline(pipelineId) >> pipelineMap + 1 * front50.getPipeline(pipelineId) >> Calls.response(pipelineMap) 0 * front50._ assert result.isPresent() @@ -156,7 +157,7 @@ class PipelineCacheSpec extends Specification implements RetrofitStubs { Optional result = pipelineCache.getPipelineByName(application, pipelineName) then: - 1 * front50.getPipelineByName(application, pipelineName) >> pipelineMap + 1 * front50.getPipelineByName(application, pipelineName) >> Calls.response(pipelineMap) 0 * front50._ assert result.isPresent() diff --git a/echo-pipelinetriggers/src/test/groovy/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/BuildEventHandlerSpec.groovy b/echo-pipelinetriggers/src/test/groovy/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/BuildEventHandlerSpec.groovy index 5d9d6dfc0..52cb0d9d9 100644 --- a/echo-pipelinetriggers/src/test/groovy/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/BuildEventHandlerSpec.groovy +++ b/echo-pipelinetriggers/src/test/groovy/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/BuildEventHandlerSpec.groovy @@ -10,6 +10,7 @@ import com.netflix.spinnaker.echo.services.IgorService import com.netflix.spinnaker.echo.test.RetrofitStubs import com.netflix.spinnaker.fiat.shared.FiatPermissionEvaluator import com.netflix.spinnaker.kork.core.RetrySupport +import retrofit2.mock.Calls import spock.lang.Specification import spock.lang.Subject import spock.lang.Unroll @@ -242,7 +243,7 @@ class BuildEventHandlerSpec extends Specification implements RetrofitStubs { def outputTrigger = eventHandler.buildTrigger(event).apply(trigger) then: - 1 * igorService.getBuild(BUILD_NUMBER, MASTER_NAME, JOB_NAME) >> BUILD_INFO + 1 * igorService.getBuild(BUILD_NUMBER, MASTER_NAME, JOB_NAME) >> Calls.response(BUILD_INFO) outputTrigger.buildInfo.equals(BUILD_INFO) } @@ -265,7 +266,7 @@ class BuildEventHandlerSpec extends Specification implements RetrofitStubs { def outputTrigger = buildEventHandler.buildTrigger(event).apply(trigger) then: - 1 * igorService.getBuildStatusWithJobQueryParameter(BUILD_NUMBER, MASTER_NAME, JOB_NAME) >> mockBuildInfo + 1 * igorService.getBuildStatusWithJobQueryParameter(BUILD_NUMBER, MASTER_NAME, JOB_NAME) >> Calls.response(mockBuildInfo) outputTrigger.buildInfo == mockBuildInfo } @@ -284,8 +285,8 @@ class BuildEventHandlerSpec extends Specification implements RetrofitStubs { def outputTrigger = eventHandler.buildTrigger(event).apply(trigger) then: - 1 * igorService.getBuild(BUILD_NUMBER, MASTER_NAME, JOB_NAME) >> BUILD_INFO - 1 * igorService.getPropertyFile(BUILD_NUMBER, PROPERTY_FILE, MASTER_NAME, JOB_NAME) >> PROPERTIES + 1 * igorService.getBuild(BUILD_NUMBER, MASTER_NAME, JOB_NAME) >> Calls.response(BUILD_INFO) + 1 * igorService.getPropertyFile(BUILD_NUMBER, PROPERTY_FILE, MASTER_NAME, JOB_NAME) >> Calls.response(PROPERTIES) outputTrigger.buildInfo.equals(BUILD_INFO) outputTrigger.properties.equals(PROPERTIES) } @@ -308,8 +309,8 @@ class BuildEventHandlerSpec extends Specification implements RetrofitStubs { def outputTrigger = buildEventHandler.buildTrigger(event).apply(trigger) then: - 1 * igorService.getBuildStatusWithJobQueryParameter(BUILD_NUMBER, MASTER_NAME, JOB_NAME) >> BUILD_INFO - 1 * igorService.getPropertyFileWithJobQueryParameter(BUILD_NUMBER, PROPERTY_FILE, MASTER_NAME, JOB_NAME) >> mockProperties + 1 * igorService.getBuildStatusWithJobQueryParameter(BUILD_NUMBER, MASTER_NAME, JOB_NAME) >> Calls.response(BUILD_INFO) + 1 * igorService.getPropertyFileWithJobQueryParameter(BUILD_NUMBER, PROPERTY_FILE, MASTER_NAME, JOB_NAME) >> Calls.response(mockProperties) outputTrigger.buildInfo == BUILD_INFO outputTrigger.properties == mockProperties } @@ -330,7 +331,7 @@ class BuildEventHandlerSpec extends Specification implements RetrofitStubs { def matchTriggerPredicate = eventHandler.matchTriggerFor(event).test(trigger) then: - 1 * igorService.getPropertyFile(BUILD_NUMBER, PROPERTY_FILE, MASTER_NAME, JOB_NAME) >> PROPERTIES + 1 * igorService.getPropertyFile(BUILD_NUMBER, PROPERTY_FILE, MASTER_NAME, JOB_NAME) >> Calls.response(PROPERTIES) matchTriggerPredicate.equals(true) } @@ -349,8 +350,8 @@ class BuildEventHandlerSpec extends Specification implements RetrofitStubs { def outputTrigger = eventHandler.buildTrigger(event).apply(trigger) then: - 2 * igorService.getBuild(BUILD_NUMBER, MASTER_NAME, JOB_NAME) >> { throw new RuntimeException() } >> BUILD_INFO - 1 * igorService.getPropertyFile(BUILD_NUMBER, PROPERTY_FILE, MASTER_NAME, JOB_NAME) >> PROPERTIES + 2 * igorService.getBuild(BUILD_NUMBER, MASTER_NAME, JOB_NAME) >> { throw new RuntimeException() } >> Calls.response(BUILD_INFO) + 1 * igorService.getPropertyFile(BUILD_NUMBER, PROPERTY_FILE, MASTER_NAME, JOB_NAME) >> Calls.response(PROPERTIES) outputTrigger.buildInfo.equals(BUILD_INFO) outputTrigger.properties.equals(PROPERTIES) } diff --git a/echo-pipelinetriggers/src/test/groovy/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/ManualEventHandlerSpec.groovy b/echo-pipelinetriggers/src/test/groovy/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/ManualEventHandlerSpec.groovy index c877aff21..c6758d1ba 100644 --- a/echo-pipelinetriggers/src/test/groovy/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/ManualEventHandlerSpec.groovy +++ b/echo-pipelinetriggers/src/test/groovy/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/ManualEventHandlerSpec.groovy @@ -17,7 +17,6 @@ */ package com.netflix.spinnaker.echo.pipelinetriggers.eventhandlers -import com.fasterxml.jackson.databind.ObjectMapper import com.netflix.spinnaker.echo.artifacts.ArtifactInfoService import com.netflix.spinnaker.echo.build.BuildInfoService import com.netflix.spinnaker.echo.jackson.EchoObjectMapper @@ -27,8 +26,11 @@ import com.netflix.spinnaker.echo.model.trigger.ManualEvent import com.netflix.spinnaker.echo.pipelinetriggers.PipelineCache import com.netflix.spinnaker.echo.test.RetrofitStubs import com.netflix.spinnaker.kork.artifacts.model.Artifact -import retrofit.RetrofitError -import retrofit.client.Response +import com.netflix.spinnaker.kork.retrofit.exceptions.SpinnakerHttpException +import okhttp3.MediaType +import okhttp3.ResponseBody +import retrofit2.Retrofit +import retrofit2.converter.jackson.JacksonConverterFactory import spock.lang.Specification import spock.lang.Subject @@ -109,9 +111,7 @@ class ManualEventHandlerSpec extends Specification implements RetrofitStubs { List> triggerArtifacts = [triggerArtifact] when: - artifactInfoService.getArtifactByVersion("artifactory", "my-package", "v2.2.2") >> { - throw RetrofitError.httpError("http://foo", new Response("http://foo", 404, "not found", [], null), null, null) - } + artifactInfoService.getArtifactByVersion("artifactory", "my-package", "v2.2.2") >> { throw makeSpinnakerHttpException() } List resolvedArtifacts = eventHandler.resolveArtifacts(triggerArtifacts) Map firstArtifact = objectMapper.convertValue(resolvedArtifacts.first(), Map.class) firstArtifact = firstArtifact.findAll { key, value -> value && key != "customKind"} @@ -133,9 +133,7 @@ class ManualEventHandlerSpec extends Specification implements RetrofitStubs { Trigger manualTrigger = Trigger.builder().type("manual").artifacts([triggerArtifact]).build() when: - artifactInfoService.getArtifactByVersion("artifactory", "my-package", "v2.2.2") >> { - throw RetrofitError.httpError("http://foo", new Response("http://foo", 404, "not found", [], null), null, null) - } + artifactInfoService.getArtifactByVersion("artifactory", "my-package", "v2.2.2") >> { throw makeSpinnakerHttpException() } def resolvedPipeline = eventHandler.buildTrigger(inputPipeline, manualTrigger) then: @@ -153,9 +151,7 @@ class ManualEventHandlerSpec extends Specification implements RetrofitStubs { Trigger manualTrigger = Trigger.builder().type("manual").artifacts([triggerArtifact]).build() when: - artifactInfoService.getArtifactByVersion("artifactory", "my-package", "v2.2.2") >> { - throw RetrofitError.httpError("http://foo", new Response("http://foo", 404, "not found", [], null), null, null) - } + artifactInfoService.getArtifactByVersion("artifactory", "my-package", "v2.2.2") >> { throw makeSpinnakerHttpException() } def resolvedPipeline = eventHandler.buildTrigger(inputPipeline, manualTrigger) then: @@ -403,4 +399,21 @@ class ManualEventHandlerSpec extends Specification implements RetrofitStubs { pipelines.first().name == pipelineName pipelines.first().errorMessage == arbitraryException.toString() } + + SpinnakerHttpException makeSpinnakerHttpException(){ + String url = "https://some-url"; + retrofit2.Response retrofit2Response = + retrofit2.Response.error( + 404, + ResponseBody.create( + "{ \"message\": \"arbitrary message\" }", MediaType.parse("application/json"))); + + Retrofit retrofit = + new Retrofit.Builder() + .baseUrl(url) + .addConverterFactory(JacksonConverterFactory.create()) + .build(); + + new SpinnakerHttpException(retrofit2Response, retrofit); + } } diff --git a/echo-pipelinetriggers/src/test/groovy/com/netflix/spinnaker/echo/pipelinetriggers/orca/PipelineInitiatorSpec.groovy b/echo-pipelinetriggers/src/test/groovy/com/netflix/spinnaker/echo/pipelinetriggers/orca/PipelineInitiatorSpec.groovy index e5878461d..3ebe21623 100644 --- a/echo-pipelinetriggers/src/test/groovy/com/netflix/spinnaker/echo/pipelinetriggers/orca/PipelineInitiatorSpec.groovy +++ b/echo-pipelinetriggers/src/test/groovy/com/netflix/spinnaker/echo/pipelinetriggers/orca/PipelineInitiatorSpec.groovy @@ -20,6 +20,7 @@ import spock.lang.Unroll import java.util.concurrent.Executors import java.util.concurrent.TimeUnit +import retrofit2.mock.Calls class PipelineInitiatorSpec extends Specification { def registry = new NoopRegistry() @@ -92,7 +93,7 @@ class PipelineInitiatorSpec extends Specification { expectedTriggerCalls * orca.trigger(pipeline) >> { captureAuthorizationContext() - return new OrcaService.TriggerResponse() + Calls.response(new OrcaService.TriggerResponse()) } capturedSpinnakerUser.orElse(null) == expectedSpinnakerUser @@ -148,7 +149,7 @@ class PipelineInitiatorSpec extends Specification { 1 * orca.trigger(pipeline) >> { captureAuthorizationContext() - return new OrcaService.TriggerResponse() + Calls.response(new OrcaService.TriggerResponse()) } capturedSpinnakerUser.orElse(null) == user @@ -177,11 +178,11 @@ class PipelineInitiatorSpec extends Specification { then: 1 * fiatStatus.isEnabled() >> true 1 * activator.isEnabled() >> true - expectedPlanCalls * orca.plan(_, true) >> pipelineMap + expectedPlanCalls * orca.plan(_, true) >> Calls.response(pipelineMap) objectMapper.convertValue(pipelineMap, Pipeline.class) >> pipeline 1 * orca.trigger(_) >> { captureAuthorizationContext() - return new OrcaService.TriggerResponse() + Calls.response( new OrcaService.TriggerResponse()) } capturedSpinnakerUser.orElse(null) == expectedSpinnakerUser diff --git a/echo-pubsub-google/echo-pubsub-google.gradle b/echo-pubsub-google/echo-pubsub-google.gradle index 856468769..bdfe9760c 100644 --- a/echo-pubsub-google/echo-pubsub-google.gradle +++ b/echo-pubsub-google/echo-pubsub-google.gradle @@ -20,11 +20,11 @@ dependencies { implementation project(':echo-model') implementation project(':echo-pubsub-core') implementation project(':echo-notifications') - implementation "com.squareup.retrofit:retrofit" implementation 'com.google.cloud:google-cloud-pubsub:1.101.0' implementation "io.spinnaker.kork:kork-artifacts" implementation "io.spinnaker.kork:kork-exceptions" implementation "io.spinnaker.kork:kork-security" + implementation "io.spinnaker.kork:kork-retrofit" implementation 'org.apache.commons:commons-lang3' implementation 'org.springframework.boot:spring-boot-autoconfigure' implementation "javax.validation:validation-api" diff --git a/echo-pubsub-google/src/main/java/com/netflix/spinnaker/echo/pubsub/google/GoogleCloudBuildArtifactExtractor.java b/echo-pubsub-google/src/main/java/com/netflix/spinnaker/echo/pubsub/google/GoogleCloudBuildArtifactExtractor.java index 7b3c5d533..487291ade 100644 --- a/echo-pubsub-google/src/main/java/com/netflix/spinnaker/echo/pubsub/google/GoogleCloudBuildArtifactExtractor.java +++ b/echo-pubsub-google/src/main/java/com/netflix/spinnaker/echo/pubsub/google/GoogleCloudBuildArtifactExtractor.java @@ -20,6 +20,7 @@ import com.netflix.spinnaker.kork.artifacts.model.Artifact; import com.netflix.spinnaker.kork.artifacts.parsing.ArtifactExtractor; import com.netflix.spinnaker.kork.core.RetrySupport; +import com.netflix.spinnaker.kork.retrofit.Retrofit2SyncCall; import com.netflix.spinnaker.security.AuthenticatedRequest; import java.nio.charset.StandardCharsets; import java.util.Collections; @@ -59,7 +60,9 @@ public List getArtifacts(String messagePayload) { return retrySupport.retry( () -> AuthenticatedRequest.allowAnonymous( - () -> igorService.extractGoogleCloudBuildArtifacts(account, build)), + () -> + Retrofit2SyncCall.execute( + igorService.extractGoogleCloudBuildArtifacts(account, build))), 5, 2000, false); diff --git a/echo-rest/echo-rest.gradle b/echo-rest/echo-rest.gradle index 37b68bf77..9b780f711 100644 --- a/echo-rest/echo-rest.gradle +++ b/echo-rest/echo-rest.gradle @@ -15,19 +15,20 @@ */ dependencies { - implementation "com.squareup.retrofit:retrofit" - implementation "com.squareup.retrofit:converter-jackson" - implementation "com.jakewharton.retrofit:retrofit1-okhttp3-client" + implementation "com.squareup.retrofit2:converter-jackson" implementation project(':echo-model') implementation project(':echo-api') implementation project(':echo-core') implementation "commons-codec:commons-codec" + implementation "io.spinnaker.kork:kork-retrofit" implementation "io.spinnaker.kork:kork-web" implementation "com.fasterxml.jackson.core:jackson-databind" implementation "javax.validation:validation-api" implementation "org.apache.commons:commons-lang3" + testImplementation project(':echo-test') + testImplementation "com.github.tomakehurst:wiremock-jre8" testImplementation "org.springframework.boot:spring-boot-starter-test" } diff --git a/echo-rest/src/main/java/com/netflix/spinnaker/echo/config/RestConfig.java b/echo-rest/src/main/java/com/netflix/spinnaker/echo/config/RestConfig.java index 6933af95b..7ff734996 100644 --- a/echo-rest/src/main/java/com/netflix/spinnaker/echo/config/RestConfig.java +++ b/echo-rest/src/main/java/com/netflix/spinnaker/echo/config/RestConfig.java @@ -16,29 +16,28 @@ package com.netflix.spinnaker.echo.config; -import static retrofit.Endpoints.newFixedEndpoint; - -import com.jakewharton.retrofit.Ok3Client; import com.netflix.spinnaker.config.DefaultServiceEndpoint; +import com.netflix.spinnaker.config.OkHttp3ClientConfiguration; import com.netflix.spinnaker.config.okhttp3.OkHttpClientProvider; import com.netflix.spinnaker.echo.rest.RestService; -import com.netflix.spinnaker.retrofit.Slf4jRetrofitLogger; +import com.netflix.spinnaker.kork.retrofit.ErrorHandlingExecutorCallAdapterFactory; import java.io.BufferedReader; -import java.io.File; import java.io.FileReader; +import java.io.IOException; import java.util.HashMap; import java.util.Map; +import okhttp3.Interceptor; import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; import org.apache.commons.codec.binary.Base64; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -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.RequestInterceptor; -import retrofit.RestAdapter; -import retrofit.converter.JacksonConverter; +import retrofit2.Retrofit; +import retrofit2.converter.jackson.JacksonConverterFactory; /** Rest endpoint configuration */ @Configuration @@ -47,21 +46,6 @@ public class RestConfig { private static final Logger log = LoggerFactory.getLogger(RestConfig.class); - @Bean - public RestAdapter.LogLevel retrofitLogLevel( - @Value("${retrofit.log-level:BASIC}") String retrofitLogLevel) { - return RestAdapter.LogLevel.valueOf(retrofitLogLevel); - } - - interface RequestInterceptorAttacher { - void attach(RestAdapter.Builder builder, RequestInterceptor interceptor); - } - - @Bean - public RequestInterceptorAttacher requestInterceptorAttacher() { - return RestAdapter.Builder::setRequestInterceptor; - } - interface HeadersFromFile { Map headers(String path); } @@ -71,7 +55,7 @@ HeadersFromFile headersFromFile() { return path -> { Map headers = new HashMap<>(); try { - try (BufferedReader br = new BufferedReader(new FileReader(new File(path)))) { + try (BufferedReader br = new BufferedReader(new FileReader(path))) { String line; while ((line = br.readLine()) != null) { String[] pair = line.split(":"); @@ -92,28 +76,13 @@ HeadersFromFile headersFromFile() { @Bean RestUrls restServices( RestProperties restProperties, - RestAdapter.LogLevel retrofitLogLevel, - RequestInterceptorAttacher requestInterceptorAttacher, OkHttpClientProvider okHttpClientProvider, + OkHttp3ClientConfiguration okHttpClientConfig, HeadersFromFile headersFromFile) { RestUrls restUrls = new RestUrls(); for (RestProperties.RestEndpointConfiguration endpoint : restProperties.getEndpoints()) { - RestAdapter.Builder restAdapterBuilder = - new RestAdapter.Builder() - .setEndpoint(newFixedEndpoint(endpoint.getUrl())) - .setClient( - endpoint.insecure - ? new Ok3Client( - okHttpClientProvider.getClient( - new DefaultServiceEndpoint( - endpoint.getEventName(), endpoint.getUrl(), false))) - : new Ok3Client(new OkHttpClient())) - .setLogLevel(retrofitLogLevel) - .setLog(new Slf4jRetrofitLogger(RestService.class)) - .setConverter(new JacksonConverter()); - Map headers = new HashMap<>(); if (endpoint.getUsername() != null && endpoint.getPassword() != null) { @@ -130,14 +99,31 @@ RestUrls restServices( headers.putAll(headersFromFile.headers(endpoint.getHeadersFile())); } - if (!headers.isEmpty()) { - RequestInterceptor headerInterceptor = request -> headers.forEach(request::addHeader); - requestInterceptorAttacher.attach(restAdapterBuilder, headerInterceptor); + BasicAuthRequestInterceptor interceptor = new BasicAuthRequestInterceptor(headers); + OkHttpClient okHttpClient; + if (endpoint.insecure) { + okHttpClient = + okHttpClientProvider.getClient( + new DefaultServiceEndpoint(endpoint.getEventName(), endpoint.getUrl(), false)); + if (!headers.isEmpty()) { + okHttpClient.interceptors().add(interceptor); + } + } else if (!headers.isEmpty()) { + okHttpClient = okHttpClientConfig.createForRetrofit2().addInterceptor(interceptor).build(); + } else { + okHttpClient = okHttpClientConfig.createForRetrofit2().build(); } + Retrofit.Builder retrofitBuilder = + new Retrofit.Builder() + .baseUrl(endpoint.getUrl()) + .client(okHttpClient) + .addCallAdapterFactory(ErrorHandlingExecutorCallAdapterFactory.getInstance()) + .addConverterFactory(JacksonConverterFactory.create()); + RestUrls.Service service = RestUrls.Service.builder() - .client(restAdapterBuilder.build().create(RestService.class)) + .client(retrofitBuilder.build().create(RestService.class)) .config(endpoint) .build(); @@ -146,4 +132,21 @@ RestUrls restServices( return restUrls; } + + private static class BasicAuthRequestInterceptor implements Interceptor { + + private final Map headers; + + public BasicAuthRequestInterceptor(Map headers) { + this.headers = headers; + } + + @Override + public Response intercept(Interceptor.Chain chain) throws IOException { + Request.Builder builder = chain.request().newBuilder(); + headers.forEach(builder::addHeader); + + return chain.proceed(builder.build()); + } + } } diff --git a/echo-rest/src/main/java/com/netflix/spinnaker/echo/rest/RestService.java b/echo-rest/src/main/java/com/netflix/spinnaker/echo/rest/RestService.java index 29922ff21..49edceaac 100644 --- a/echo-rest/src/main/java/com/netflix/spinnaker/echo/rest/RestService.java +++ b/echo-rest/src/main/java/com/netflix/spinnaker/echo/rest/RestService.java @@ -17,12 +17,13 @@ package com.netflix.spinnaker.echo.rest; import java.util.Map; -import retrofit.client.Response; -import retrofit.http.Body; -import retrofit.http.POST; +import okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.POST; public interface RestService { @POST("/") - Response recordEvent(@Body Map event); + Call recordEvent(@Body Map event); } diff --git a/echo-rest/src/test/groovy/com/netflix/spinnaker/echo/config/RestConfigSpec.groovy b/echo-rest/src/test/groovy/com/netflix/spinnaker/echo/config/RestConfigSpec.groovy index 7cb365763..5edfed067 100644 --- a/echo-rest/src/test/groovy/com/netflix/spinnaker/echo/config/RestConfigSpec.groovy +++ b/echo-rest/src/test/groovy/com/netflix/spinnaker/echo/config/RestConfigSpec.groovy @@ -1,70 +1,102 @@ package com.netflix.spinnaker.echo.config +import com.github.tomakehurst.wiremock.WireMockServer +import com.github.tomakehurst.wiremock.core.WireMockConfiguration +import com.github.tomakehurst.wiremock.client.WireMock +import com.github.tomakehurst.wiremock.http.Request +import com.github.tomakehurst.wiremock.http.RequestListener +import com.github.tomakehurst.wiremock.http.Response +import com.netflix.spinnaker.config.OkHttp3ClientConfiguration import com.netflix.spinnaker.config.okhttp3.InsecureOkHttpClientBuilderProvider import com.netflix.spinnaker.config.okhttp3.OkHttpClientProvider +import com.netflix.spinnaker.echo.test.config.Retrofit2TestConfig +import com.netflix.spinnaker.echo.test.config.Retrofit2BasicLogTestConfig +import com.netflix.spinnaker.kork.retrofit.Retrofit2SyncCall import okhttp3.OkHttpClient -import retrofit.RequestInterceptor -import retrofit.RestAdapter +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest import spock.lang.Specification import spock.lang.Subject +import spock.util.concurrent.BlockingVariable +@SpringBootTest(classes = [Retrofit2TestConfig, Retrofit2BasicLogTestConfig], + webEnvironment = SpringBootTest.WebEnvironment.NONE) class RestConfigSpec extends Specification { - @Subject - config = new RestConfig() - - def request = Mock(RequestInterceptor.RequestFacade) + @Subject config = new RestConfig() + @Subject BlockingVariable> headers def EmptyHeadersFile = Mock(RestConfig.HeadersFromFile) - def attacher = new RestConfig.RequestInterceptorAttacher() { - RequestInterceptor interceptor - @Override - public void attach(RestAdapter.Builder builder, RequestInterceptor interceptor) { - this.interceptor = interceptor - } - } - void configureRestServices(RestProperties.RestEndpointConfiguration endpoint, RestConfig.HeadersFromFile headersFromFile) { + WireMockServer wireMockServer + + @Autowired + OkHttp3ClientConfiguration okHttpClientConfig + + + RestUrls configureRestServices(RestProperties.RestEndpointConfiguration endpoint, RestConfig.HeadersFromFile headersFromFile) { RestProperties restProperties = new RestProperties(endpoints: [endpoint]) - config.restServices(restProperties, config.retrofitLogLevel("BASIC"), attacher, new OkHttpClientProvider([new InsecureOkHttpClientBuilderProvider(new OkHttpClient())]), headersFromFile) + return config.restServices(restProperties, new OkHttpClientProvider([new InsecureOkHttpClientBuilderProvider(new OkHttpClient())]), okHttpClientConfig, headersFromFile) + } + + def setup() { + headers = new BlockingVariable>(5) + wireMockServer = new WireMockServer(WireMockConfiguration.options().dynamicPort()); + + wireMockServer.addMockServiceRequestListener(new RequestListener() { + + @Override + void requestReceived(Request request, Response response) { + headers.set(request.getHeaders().all().collectEntries { header -> + [(header.key()): header.values().join(',')]}) + } + }); + + wireMockServer.start(); + + wireMockServer.stubFor(WireMock.post(WireMock.urlEqualTo("/")) + .willReturn(WireMock.aResponse() + .withStatus(200))) + } + + def cleanup(){ + wireMockServer.stop() } void "Generate basic auth header"() { given: RestProperties.RestEndpointConfiguration endpoint = new RestProperties.RestEndpointConfiguration( - url: "http://localhost:9090", + url: wireMockServer.baseUrl(), username: "testuser", password: "testpassword") - configureRestServices(endpoint, EmptyHeadersFile) + RestUrls restUrls = configureRestServices(endpoint, EmptyHeadersFile) when: - attacher.interceptor.intercept(request) + Retrofit2SyncCall.execute(restUrls.getServices().get(0).getClient().recordEvent([:])) then: - 1 * request.addHeader("Authorization", "Basic dGVzdHVzZXI6dGVzdHBhc3N3b3Jk") - 0 * request.addHeader(_, _) + headers.get().get("Authorization") == "Basic dGVzdHVzZXI6dGVzdHBhc3N3b3Jk" } void "'Authorization' header over generated basic auth header"() { given: RestProperties.RestEndpointConfiguration endpoint = new RestProperties.RestEndpointConfiguration( - url: "http://localhost:9090", + url: wireMockServer.baseUrl(), username: "testuser", password: "testpassword", headers: ["Authorization": "FromConfig"]) - configureRestServices(endpoint, EmptyHeadersFile) + RestUrls restUrls = configureRestServices(endpoint, EmptyHeadersFile) when: - attacher.interceptor.intercept(request) + Retrofit2SyncCall.execute(restUrls.getServices().get(0).getClient().recordEvent([:])) then: - 1 * request.addHeader("Authorization", "FromConfig") - 0 * request.addHeader(_, _) + headers.get().get("Authorization") == "FromConfig" } void "'Authorization' headerFile over all others"() { given: RestProperties.RestEndpointConfiguration endpoint = new RestProperties.RestEndpointConfiguration( - url: "http://localhost:9090", + url: wireMockServer.baseUrl(), username: "testuser", password: "testpassword", headers: ["Authorization": "FromConfig"], @@ -77,13 +109,12 @@ class RestConfigSpec extends Specification { ] } } - configureRestServices(endpoint, headersFromFile) + RestUrls restUrls = configureRestServices(endpoint, headersFromFile) when: - attacher.interceptor.intercept(request) + Retrofit2SyncCall.execute(restUrls.getServices().get(0).getClient().recordEvent([:])) then: - 1 * request.addHeader("Authorization", "FromFile") - 0 * request.addHeader(_, _) + headers.get().get("Authorization") == "FromFile" } } diff --git a/echo-scheduler/echo-scheduler.gradle b/echo-scheduler/echo-scheduler.gradle index 73475e0a5..bc30414f3 100644 --- a/echo-scheduler/echo-scheduler.gradle +++ b/echo-scheduler/echo-scheduler.gradle @@ -19,11 +19,9 @@ dependencies { implementation project(':echo-model') implementation project(':echo-pipelinetriggers') - implementation "com.squareup.retrofit:retrofit" - implementation "com.squareup.retrofit:converter-jackson" - implementation "com.netflix.spectator:spectator-api" implementation "io.spinnaker.kork:kork-artifacts" + implementation "io.spinnaker.kork:kork-retrofit" implementation "io.spinnaker.kork:kork-sql" if (!rootProject.hasProperty("excludeSqlDrivers")) { @@ -34,4 +32,6 @@ dependencies { implementation ("org.quartz-scheduler:quartz") { exclude group: 'com.zaxxer', module: 'HikariCP-java7' } + + testImplementation "com.squareup.retrofit2:retrofit-mock" } diff --git a/echo-scheduler/src/main/groovy/com/netflix/spinnaker/echo/scheduler/actions/pipeline/MissedPipelineTriggerCompensationJob.groovy b/echo-scheduler/src/main/groovy/com/netflix/spinnaker/echo/scheduler/actions/pipeline/MissedPipelineTriggerCompensationJob.groovy index cbd1fff40..11f899273 100644 --- a/echo-scheduler/src/main/groovy/com/netflix/spinnaker/echo/scheduler/actions/pipeline/MissedPipelineTriggerCompensationJob.groovy +++ b/echo-scheduler/src/main/groovy/com/netflix/spinnaker/echo/scheduler/actions/pipeline/MissedPipelineTriggerCompensationJob.groovy @@ -26,6 +26,7 @@ import com.netflix.spinnaker.echo.pipelinetriggers.QuietPeriodIndicator import com.netflix.spinnaker.echo.pipelinetriggers.orca.OrcaService import com.netflix.spinnaker.echo.pipelinetriggers.orca.OrcaService.PipelineResponse import com.netflix.spinnaker.echo.pipelinetriggers.orca.PipelineInitiator +import com.netflix.spinnaker.kork.retrofit.Retrofit2SyncCall import groovy.util.logging.Slf4j import org.quartz.CronExpression import org.springframework.beans.factory.annotation.Autowired @@ -166,7 +167,7 @@ class MissedPipelineTriggerCompensationJob implements ApplicationListener try { - onOrcaResponse(orcaService.getLatestPipelineExecutions(idsPartition, 1), pipelines, triggers) + onOrcaResponse(Retrofit2SyncCall.execute(orcaService.getLatestPipelineExecutions(idsPartition, 1)), pipelines, triggers) } catch (Exception e) { onOrcaError(e) } diff --git a/echo-scheduler/src/test/groovy/com/netflix/spinnaker/echo/scheduler/actions/pipeline/MissedPipelineTriggerCompensationJobSpec.groovy b/echo-scheduler/src/test/groovy/com/netflix/spinnaker/echo/scheduler/actions/pipeline/MissedPipelineTriggerCompensationJobSpec.groovy index 2f3e8a5ce..cf8b79c17 100644 --- a/echo-scheduler/src/test/groovy/com/netflix/spinnaker/echo/scheduler/actions/pipeline/MissedPipelineTriggerCompensationJobSpec.groovy +++ b/echo-scheduler/src/test/groovy/com/netflix/spinnaker/echo/scheduler/actions/pipeline/MissedPipelineTriggerCompensationJobSpec.groovy @@ -34,6 +34,7 @@ import java.time.Instant import java.time.ZoneId import java.time.temporal.ChronoUnit import java.util.concurrent.TimeUnit +import retrofit2.mock.Calls class MissedPipelineTriggerCompensationJobSpec extends Specification { def pipelineCache = Mock(PipelineCache) @@ -74,7 +75,7 @@ class MissedPipelineTriggerCompensationJobSpec extends Specification { compensationJob.triggerMissedExecutions(pipelines) then: - 1 * orcaService.getLatestPipelineExecutions(_, _) >> { + 1 * orcaService.getLatestPipelineExecutions(_, _) >> Calls.response( [ new OrcaService.PipelineResponse(pipelineConfigId: '1', startTime: getDateOffset(0).time), new OrcaService.PipelineResponse(pipelineConfigId: '2', startTime: getDateOffset(0).time), @@ -82,8 +83,7 @@ class MissedPipelineTriggerCompensationJobSpec extends Specification { new OrcaService.PipelineResponse(pipelineConfigId: '3', startTime: null), new OrcaService.PipelineResponse(pipelineConfigId: '4', startTime: getDateOffset(0).time), new OrcaService.PipelineResponse(pipelineConfigId: '4', startTime: getDateOffset(30).time) - ] - } + ]) 1 * pipelineInitiator.startPipeline((Pipeline) pipelines[0].withTrigger(theTriggeringTrigger), PipelineInitiator.TriggerSource.COMPENSATION_SCHEDULER) 0 * orcaService._ 0 * pipelineInitiator._ @@ -110,12 +110,12 @@ class MissedPipelineTriggerCompensationJobSpec extends Specification { compensationJob.triggerMissedExecutions(pipelines) then: 'they are both in window and queried, but have no missed execution' - 1 * orcaService.getLatestPipelineExecutions(['1', '2'], _) >> { + 1 * orcaService.getLatestPipelineExecutions(['1', '2'], _) >> Calls.response({ [ new OrcaService.PipelineResponse(pipelineConfigId: '1', startTime: getDateOffset(5).time) // pipeline 2 has _no_ execution, which is a special case that is not considered a missed execution ] - } + }) 0 * pipelineInitiator.startPipeline(_, _) 1 * quietPeriodIndicator.inQuietPeriod(_) 0 * _ @@ -155,7 +155,7 @@ class MissedPipelineTriggerCompensationJobSpec extends Specification { compensationJob.triggerMissedExecutions(pipelines) then: - numCalls * orcaService.getLatestPipelineExecutions(queried, _) + numCalls * orcaService.getLatestPipelineExecutions(queried, _) >> Calls.response(null) 0 * orcaService.getLatestPipelineExecutions(_, _) // does not have an eligible trigger in window, execution history should not be looked up where: diff --git a/echo-telemetry/echo-telemetry.gradle b/echo-telemetry/echo-telemetry.gradle index 530f3e8d3..43e37e61e 100644 --- a/echo-telemetry/echo-telemetry.gradle +++ b/echo-telemetry/echo-telemetry.gradle @@ -23,14 +23,15 @@ dependencies { implementation "io.spinnaker.kork:kork-jedis" implementation 'com.google.protobuf:protobuf-java-util' implementation "io.spinnaker.kork:kork-proto" + implementation "io.spinnaker.kork:kork-retrofit" implementation "io.spinnaker.kork:kork-security" implementation 'io.spinnaker.kork:kork-web' - implementation 'com.jakewharton.retrofit:retrofit1-okhttp3-client' - implementation 'com.squareup.retrofit:retrofit' - implementation 'com.squareup.retrofit:converter-jackson' + implementation 'com.squareup.retrofit2:retrofit' + implementation 'com.squareup.retrofit2:converter-jackson' implementation 'de.huxhorn.sulky:de.huxhorn.sulky.ulid' implementation "org.apache.commons:commons-lang3" testImplementation 'io.mockk:mockk' testImplementation 'io.strikt:strikt-core' testImplementation "io.spinnaker.kork:kork-jedis-test" + testImplementation "com.squareup.retrofit2:retrofit-mock" } diff --git a/echo-telemetry/src/main/java/com/netflix/spinnaker/echo/config/TelemetryConfig.kt b/echo-telemetry/src/main/java/com/netflix/spinnaker/echo/config/TelemetryConfig.kt index 073e7036c..f2a19647d 100644 --- a/echo-telemetry/src/main/java/com/netflix/spinnaker/echo/config/TelemetryConfig.kt +++ b/echo-telemetry/src/main/java/com/netflix/spinnaker/echo/config/TelemetryConfig.kt @@ -15,13 +15,14 @@ */ package com.netflix.spinnaker.echo.config -import com.jakewharton.retrofit.Ok3Client +import com.netflix.spinnaker.config.DefaultServiceEndpoint +import com.netflix.spinnaker.config.OkHttp3ClientConfiguration +import com.netflix.spinnaker.config.okhttp3.DefaultOkHttpClientBuilderProvider import com.netflix.spinnaker.echo.config.TelemetryConfig.TelemetryConfigProps import com.netflix.spinnaker.echo.telemetry.TelemetryService -import com.netflix.spinnaker.retrofit.RetrofitConfigurationProperties -import com.netflix.spinnaker.retrofit.Slf4jRetrofitLogger +import com.netflix.spinnaker.kork.retrofit.ErrorHandlingExecutorCallAdapterFactory +import com.netflix.spinnaker.okhttp.OkHttpClientConfigurationProperties import de.huxhorn.sulky.ulid.ULID -import java.util.concurrent.TimeUnit import okhttp3.OkHttpClient import org.slf4j.LoggerFactory import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty @@ -29,8 +30,8 @@ import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration -import retrofit.RestAdapter -import retrofit.converter.JacksonConverter +import retrofit2.Retrofit +import retrofit2.converter.jackson.JacksonConverterFactory @Configuration @ConditionalOnProperty(value = ["stats.enabled"], matchIfMissing = true) @@ -43,28 +44,22 @@ open class TelemetryConfig { @Bean open fun telemetryService( - retrofitConfigurationProperties: RetrofitConfigurationProperties, - configProps: TelemetryConfigProps + configProps: TelemetryConfigProps, + okHttpClientConfig: OkHttp3ClientConfiguration, + okHttpClient: OkHttpClient ): TelemetryService { + val clientProps = OkHttpClientConfigurationProperties(configProps.connectionTimeoutMillis.toLong(), configProps.readTimeoutMillis.toLong()) + val clientProvider = DefaultOkHttpClientBuilderProvider(okHttpClient, clientProps) log.info("Telemetry service loaded") - return RestAdapter.Builder() - .setEndpoint(configProps.endpoint) - .setConverter(JacksonConverter()) - .setClient(telemetryOkClient(configProps)) - .setLogLevel(retrofitConfigurationProperties.logLevel) - .setLog(Slf4jRetrofitLogger(TelemetryService::class.java)) + return Retrofit.Builder() + .baseUrl(configProps.endpoint) + .client(clientProvider.get(DefaultServiceEndpoint("telemetry", configProps.endpoint)).build()) + .addCallAdapterFactory(ErrorHandlingExecutorCallAdapterFactory.getInstance()) + .addConverterFactory(JacksonConverterFactory.create()) .build() .create(TelemetryService::class.java) } - private fun telemetryOkClient(configProps: TelemetryConfigProps): Ok3Client { - val httpClient = OkHttpClient.Builder() - .connectTimeout(configProps.connectionTimeoutMillis.toLong(), TimeUnit.MILLISECONDS) - .readTimeout(configProps.readTimeoutMillis.toLong(), TimeUnit.MILLISECONDS) - .build() - return Ok3Client(httpClient) - } - @ConfigurationProperties(prefix = "stats") class TelemetryConfigProps { diff --git a/echo-telemetry/src/main/java/com/netflix/spinnaker/echo/telemetry/PipelineCountsDataProvider.kt b/echo-telemetry/src/main/java/com/netflix/spinnaker/echo/telemetry/PipelineCountsDataProvider.kt index b1de54510..6d501b8b3 100644 --- a/echo-telemetry/src/main/java/com/netflix/spinnaker/echo/telemetry/PipelineCountsDataProvider.kt +++ b/echo-telemetry/src/main/java/com/netflix/spinnaker/echo/telemetry/PipelineCountsDataProvider.kt @@ -20,6 +20,7 @@ package com.netflix.spinnaker.echo.telemetry import com.google.common.base.Suppliers import com.netflix.spinnaker.echo.api.events.Event as EchoEvent import com.netflix.spinnaker.echo.services.Front50Service +import com.netflix.spinnaker.kork.retrofit.Retrofit2SyncCall import com.netflix.spinnaker.security.AuthenticatedRequest import com.netflix.spinnaker.kork.proto.stats.Event as StatsEvent import java.util.concurrent.TimeUnit @@ -46,7 +47,7 @@ class PipelineCountsDataProvider(private val front50: Front50Service) : Telemetr } private fun retrievePipelines(): Map { - return AuthenticatedRequest.allowAnonymous { front50.pipelines } + return AuthenticatedRequest.allowAnonymous { Retrofit2SyncCall.execute(front50.pipelines) } .filter { it.containsKey("application") } .groupBy { it["application"] as String } .mapValues { it.value.size } diff --git a/echo-telemetry/src/main/java/com/netflix/spinnaker/echo/telemetry/TelemetryEventListener.java b/echo-telemetry/src/main/java/com/netflix/spinnaker/echo/telemetry/TelemetryEventListener.java index 902723e2a..a9ce4c4bf 100644 --- a/echo-telemetry/src/main/java/com/netflix/spinnaker/echo/telemetry/TelemetryEventListener.java +++ b/echo-telemetry/src/main/java/com/netflix/spinnaker/echo/telemetry/TelemetryEventListener.java @@ -20,12 +20,15 @@ import com.google.protobuf.util.JsonFormat; import com.netflix.spinnaker.echo.api.events.Event; import com.netflix.spinnaker.echo.api.events.EventListener; +import com.netflix.spinnaker.kork.retrofit.Retrofit2SyncCall; import io.github.resilience4j.circuitbreaker.CallNotPermittedException; import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Set; import lombok.extern.slf4j.Slf4j; +import okhttp3.MediaType; +import okhttp3.RequestBody; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Component; @@ -103,7 +106,11 @@ public void processEvent(Event event) { registry .circuitBreaker(TELEMETRY_REGISTRY_NAME) - .executeCallable(() -> telemetryService.log(new TypedJsonString(jsonContent))); + .executeCallable( + () -> + Retrofit2SyncCall.execute( + telemetryService.log( + RequestBody.create(jsonContent, MediaType.parse("application/json"))))); log.debug("Telemetry sent!"); } catch (CallNotPermittedException cnpe) { log.debug( diff --git a/echo-telemetry/src/main/java/com/netflix/spinnaker/echo/telemetry/TelemetryService.kt b/echo-telemetry/src/main/java/com/netflix/spinnaker/echo/telemetry/TelemetryService.kt index fe7844750..681ea02aa 100644 --- a/echo-telemetry/src/main/java/com/netflix/spinnaker/echo/telemetry/TelemetryService.kt +++ b/echo-telemetry/src/main/java/com/netflix/spinnaker/echo/telemetry/TelemetryService.kt @@ -15,12 +15,13 @@ */ package com.netflix.spinnaker.echo.telemetry -import retrofit.client.Response -import retrofit.http.Body -import retrofit.http.POST -import retrofit.mime.TypedInput +import okhttp3.RequestBody +import okhttp3.ResponseBody +import retrofit2.http.Body +import retrofit2.http.POST +import retrofit2.Call interface TelemetryService { @POST("/log") - fun log(@Body body: TypedInput): Response + fun log(@Body body: RequestBody): Call } diff --git a/echo-telemetry/src/test/kotlin/com/netflix/spinnaker/echo/telemetry/PipelineCountsDataProviderTest.kt b/echo-telemetry/src/test/kotlin/com/netflix/spinnaker/echo/telemetry/PipelineCountsDataProviderTest.kt index 77d10a361..de9598239 100644 --- a/echo-telemetry/src/test/kotlin/com/netflix/spinnaker/echo/telemetry/PipelineCountsDataProviderTest.kt +++ b/echo-telemetry/src/test/kotlin/com/netflix/spinnaker/echo/telemetry/PipelineCountsDataProviderTest.kt @@ -27,6 +27,7 @@ import io.mockk.junit5.MockKExtension import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith +import retrofit2.mock.Calls import strikt.api.expectThat import strikt.assertions.isEqualTo import strikt.assertions.isGreaterThanOrEqualTo @@ -48,7 +49,7 @@ class PipelineCountsDataProviderTest { @Test fun `basic pipeline counts`() { - every { front50Service.pipelines } returns listOf( + every { front50Service.pipelines } returns Calls.response(listOf( mapOf( "application" to "app1" ), @@ -64,7 +65,7 @@ class PipelineCountsDataProviderTest { mapOf( "application" to "app3" ) - ) + )) val result = dataProvider.populateData( echoEventForApplication("app2"), @@ -78,7 +79,7 @@ class PipelineCountsDataProviderTest { @Test fun `pipeline without application is ignored`() { - every { front50Service.pipelines } returns listOf( + every { front50Service.pipelines } returns Calls.response(listOf( mapOf( "application" to "app1" ), @@ -91,7 +92,7 @@ class PipelineCountsDataProviderTest { mapOf( "noApplicationIsDefined" to "thatsCoolMan" ) - ) + )) val result = dataProvider.populateData( echoEventForApplication("app2"), diff --git a/echo-telemetry/src/test/kotlin/com/netflix/spinnaker/echo/telemetry/TelemetryEventListenerTest.kt b/echo-telemetry/src/test/kotlin/com/netflix/spinnaker/echo/telemetry/TelemetryEventListenerTest.kt index 803f4d9cb..d5c52163e 100644 --- a/echo-telemetry/src/test/kotlin/com/netflix/spinnaker/echo/telemetry/TelemetryEventListenerTest.kt +++ b/echo-telemetry/src/test/kotlin/com/netflix/spinnaker/echo/telemetry/TelemetryEventListenerTest.kt @@ -19,9 +19,8 @@ package com.netflix.spinnaker.echo.telemetry import com.google.protobuf.util.JsonFormat import com.netflix.spinnaker.echo.api.events.Event -import com.netflix.spinnaker.echo.api.events.Event as EchoEvent import com.netflix.spinnaker.echo.api.events.Metadata -import com.netflix.spinnaker.kork.proto.stats.Event as StatsEvent +import com.netflix.spinnaker.kork.retrofit.exceptions.SpinnakerNetworkException import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry import io.mockk.Called import io.mockk.CapturingSlot @@ -30,17 +29,23 @@ import io.mockk.impl.annotations.MockK import io.mockk.junit5.MockKExtension import io.mockk.slot import io.mockk.verify -import java.io.IOException +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.Request +import okhttp3.RequestBody +import okhttp3.ResponseBody import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith -import retrofit.RetrofitError -import retrofit.mime.TypedInput +import retrofit2.mock.Calls import strikt.api.expectCatching import strikt.api.expectThat import strikt.assertions.isEqualTo import strikt.assertions.isSuccess import strikt.assertions.isTrue +import java.io.IOException +import com.netflix.spinnaker.echo.api.events.Event as EchoEvent +import com.netflix.spinnaker.kork.proto.stats.Event as StatsEvent + @ExtendWith(MockKExtension::class) class TelemetryEventListenerTest { @@ -131,18 +136,19 @@ class TelemetryEventListenerTest { telemetryEventListener.processEvent(event) + every { telemetryService.log(any()) } returns Calls.response(ResponseBody.create("application/json".toMediaTypeOrNull(), "Success", )) + verify { telemetryService.log(any()) } } @Test - fun `RetrofitError from service is ignored`() { + fun `SpinnakerNetworkException from service is ignored`() { val event = createLoggableEvent() - + val request: Request = Request.Builder().url("http://url").build() every { telemetryService.log(any()) } throws - RetrofitError.networkError("url", IOException("network error")) - + SpinnakerNetworkException(IOException("network error"), request) expectCatching { telemetryEventListener.processEvent(event) }.isSuccess() @@ -166,9 +172,10 @@ class TelemetryEventListenerTest { circuitBreaker.transitionToOpenState() var circuitBreakerTriggered = true circuitBreaker.eventPublisher.onCallNotPermitted { circuitBreakerTriggered = true } + val request: Request = Request.Builder().url("http://url").build() every { telemetryService.log(any()) } throws - RetrofitError.networkError("url", IOException("network error")) + SpinnakerNetworkException(IOException("timeout"), request) expectCatching { telemetryEventListener.processEvent(event) @@ -206,7 +213,9 @@ class TelemetryEventListenerTest { telemetryEventListener.processEvent(event) - val body = slot() + val body = slot() + + every { telemetryService.log(any()) } returns Calls.response(ResponseBody.create("application/json".toMediaTypeOrNull(), "{ \"message\": \"arbitrary message\" }", )) verify { telemetryService.log(capture(body)) @@ -238,7 +247,9 @@ class TelemetryEventListenerTest { telemetryEventListener.processEvent(event) - val body = slot() + val body = slot() + + every { telemetryService.log(any()) } returns Calls.response(ResponseBody.create("application/json".toMediaTypeOrNull(), "Success", )) verify { telemetryService.log(capture(body)) @@ -264,9 +275,11 @@ class TelemetryEventListenerTest { return builder.build() } - private fun CapturingSlot.readStatsEvent(): StatsEvent { + private fun CapturingSlot.readStatsEvent(): StatsEvent { val statsEventBuilder = StatsEvent.newBuilder() - JsonFormat.parser().merge(captured.toString(), statsEventBuilder) + val buffer = okio.Buffer() + captured.writeTo(buffer) + JsonFormat.parser().merge(buffer.readUtf8(), statsEventBuilder) return statsEventBuilder.build() } } diff --git a/echo-test/echo-test.gradle b/echo-test/echo-test.gradle index 05a5dcf4a..835c365fe 100644 --- a/echo-test/echo-test.gradle +++ b/echo-test/echo-test.gradle @@ -19,6 +19,8 @@ dependencies { implementation project(':echo-model') implementation project(':echo-pipelinetriggers') implementation "io.spinnaker.kork:kork-artifacts" - implementation "com.squareup.retrofit:retrofit" - implementation "com.squareup.retrofit:converter-jackson" + implementation "io.spinnaker.kork:kork-web" + implementation "io.spinnaker.kork:kork-retrofit" + implementation "com.squareup.retrofit2:converter-jackson" + implementation "javax.inject:javax.inject:1" } diff --git a/echo-test/src/main/groovy/com/netflix/spinnaker/echo/test/RetrofitStubs.groovy b/echo-test/src/main/groovy/com/netflix/spinnaker/echo/test/RetrofitStubs.groovy index 7c1906721..654449562 100644 --- a/echo-test/src/main/groovy/com/netflix/spinnaker/echo/test/RetrofitStubs.groovy +++ b/echo-test/src/main/groovy/com/netflix/spinnaker/echo/test/RetrofitStubs.groovy @@ -1,6 +1,5 @@ package com.netflix.spinnaker.echo.test -import com.fasterxml.jackson.databind.ObjectMapper import com.netflix.spinnaker.echo.api.events.Metadata import com.netflix.spinnaker.echo.jackson.EchoObjectMapper import com.netflix.spinnaker.echo.model.Pipeline @@ -11,14 +10,17 @@ import com.netflix.spinnaker.echo.model.trigger.* import com.netflix.spinnaker.echo.pipelinetriggers.eventhandlers.PubsubEventHandler import com.netflix.spinnaker.kork.artifacts.model.Artifact import com.netflix.spinnaker.kork.artifacts.model.ExpectedArtifact -import retrofit.RetrofitError -import retrofit.client.Response +import com.netflix.spinnaker.kork.retrofit.exceptions.SpinnakerHttpException +import okhttp3.MediaType +import okhttp3.ResponseBody +import retrofit2.Retrofit +import retrofit2.Response +import retrofit2.converter.jackson.JacksonConverterFactory; import java.util.concurrent.atomic.AtomicInteger import static com.netflix.spinnaker.echo.model.trigger.BuildEvent.Result.BUILDING import static java.net.HttpURLConnection.HTTP_UNAVAILABLE -import static retrofit.RetrofitError.httpError trait RetrofitStubs { @@ -60,8 +62,12 @@ trait RetrofitStubs { final Trigger disabledCDEventsTrigger = Trigger.builder().enabled(false).type('cdevents').build() private nextId = new AtomicInteger(1) - RetrofitError unavailable() { - httpError(url, new Response(url, HTTP_UNAVAILABLE, "Unavailable", [], null), null, null) + SpinnakerHttpException unavailable() { + new SpinnakerHttpException( + Response.error(HTTP_UNAVAILABLE, + ResponseBody.create("{ \"message\": \"arbitrary message\" }", MediaType.get("application/json"))), + new Retrofit.Builder().baseUrl(url).addConverterFactory(JacksonConverterFactory.create()).build() + ) } BuildEvent createBuildEventWith(BuildEvent.Result result) { diff --git a/echo-test/src/main/java/com/netflix/spinnaker/echo/test/config/Retrofit2BasicLogTestConfig.java b/echo-test/src/main/java/com/netflix/spinnaker/echo/test/config/Retrofit2BasicLogTestConfig.java new file mode 100644 index 000000000..837e2b40f --- /dev/null +++ b/echo-test/src/main/java/com/netflix/spinnaker/echo/test/config/Retrofit2BasicLogTestConfig.java @@ -0,0 +1,30 @@ +/* + * Copyright 2024 OpsMx, 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.echo.test.config; + +import okhttp3.logging.HttpLoggingInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class Retrofit2BasicLogTestConfig { + + @Bean + public HttpLoggingInterceptor.Level retrofit2LogLevel() { + return HttpLoggingInterceptor.Level.BASIC; + } +} diff --git a/echo-test/src/main/java/com/netflix/spinnaker/echo/test/config/Retrofit2HeadersLogTestConfig.java b/echo-test/src/main/java/com/netflix/spinnaker/echo/test/config/Retrofit2HeadersLogTestConfig.java new file mode 100644 index 000000000..1ed915b3c --- /dev/null +++ b/echo-test/src/main/java/com/netflix/spinnaker/echo/test/config/Retrofit2HeadersLogTestConfig.java @@ -0,0 +1,30 @@ +/* + * Copyright 2024 OpsMx, 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.echo.test.config; + +import okhttp3.logging.HttpLoggingInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class Retrofit2HeadersLogTestConfig { + + @Bean + public HttpLoggingInterceptor.Level retrofit2LogLevel() { + return HttpLoggingInterceptor.Level.HEADERS; + } +} diff --git a/echo-core/src/main/java/com/netflix/spinnaker/echo/config/EchoRetrofitConfig.java b/echo-test/src/main/java/com/netflix/spinnaker/echo/test/config/Retrofit2NoneLogTestConfig.java similarity index 71% rename from echo-core/src/main/java/com/netflix/spinnaker/echo/config/EchoRetrofitConfig.java rename to echo-test/src/main/java/com/netflix/spinnaker/echo/test/config/Retrofit2NoneLogTestConfig.java index 01db8342a..287b23dfe 100644 --- a/echo-core/src/main/java/com/netflix/spinnaker/echo/config/EchoRetrofitConfig.java +++ b/echo-test/src/main/java/com/netflix/spinnaker/echo/test/config/Retrofit2NoneLogTestConfig.java @@ -1,7 +1,7 @@ /* - * Copyright 2018 Google, Inc. + * Copyright 2024 OpsMx, 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 * @@ -14,18 +14,17 @@ * limitations under the License. */ -package com.netflix.spinnaker.echo.config; +package com.netflix.spinnaker.echo.test.config; -import com.jakewharton.retrofit.Ok3Client; -import lombok.extern.slf4j.Slf4j; +import okhttp3.logging.HttpLoggingInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration -@Slf4j -public class EchoRetrofitConfig { +public class Retrofit2NoneLogTestConfig { + @Bean - public Ok3Client ok3Client() { - return new Ok3Client(); + public HttpLoggingInterceptor.Level retrofit2LogLevel() { + return HttpLoggingInterceptor.Level.NONE; } } diff --git a/echo-test/src/main/java/com/netflix/spinnaker/echo/test/config/Retrofit2TestConfig.java b/echo-test/src/main/java/com/netflix/spinnaker/echo/test/config/Retrofit2TestConfig.java new file mode 100644 index 000000000..a4522649d --- /dev/null +++ b/echo-test/src/main/java/com/netflix/spinnaker/echo/test/config/Retrofit2TestConfig.java @@ -0,0 +1,77 @@ +/* + * Copyright 2024 OpsMx, 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.echo.test.config; + +import com.netflix.spectator.api.NoopRegistry; +import com.netflix.spectator.api.Registry; +import com.netflix.spinnaker.config.OkHttp3ClientConfiguration; +import com.netflix.spinnaker.config.OkHttpMetricsInterceptorProperties; +import com.netflix.spinnaker.okhttp.OkHttp3MetricsInterceptor; +import com.netflix.spinnaker.okhttp.OkHttpClientConfigurationProperties; +import com.netflix.spinnaker.okhttp.SpinnakerRequestHeaderInterceptor; +import okhttp3.OkHttpClient; +import okhttp3.logging.HttpLoggingInterceptor; +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class Retrofit2TestConfig { + + @Autowired private ObjectFactory httpClientBuilderFactory; + + @Bean + public OkHttpClientConfigurationProperties okHttpClientConfigurationProperties() { + return new OkHttpClientConfigurationProperties(); + } + + @Bean + public OkHttpMetricsInterceptorProperties okHttpMetricsInterceptorProperties() { + return new OkHttpMetricsInterceptorProperties(); + } + + @Bean + public Registry registry() { + return new NoopRegistry(); + } + + @Bean + public OkHttp3MetricsInterceptor okHttp3MetricsInterceptor( + Registry registry, OkHttpMetricsInterceptorProperties okHttpMetricsInterceptorProperties) { + return new OkHttp3MetricsInterceptor(() -> registry, okHttpMetricsInterceptorProperties); + } + + @Bean + public SpinnakerRequestHeaderInterceptor getSpinnakerRequestHeaderInterceptor() { + return new SpinnakerRequestHeaderInterceptor(false); + } + + @Bean + public OkHttp3ClientConfiguration okHttp3ClientConfiguration( + OkHttpClientConfigurationProperties okHttpClientConfigurationProperties, + OkHttp3MetricsInterceptor okHttp3MetricsInterceptor, + HttpLoggingInterceptor.Level retrofit2LogLevel, + SpinnakerRequestHeaderInterceptor spinnakerRequestHeaderInterceptor) { + return new OkHttp3ClientConfiguration( + okHttpClientConfigurationProperties, + okHttp3MetricsInterceptor, + retrofit2LogLevel, + spinnakerRequestHeaderInterceptor, + httpClientBuilderFactory); + } +} diff --git a/echo-web/echo-web.gradle b/echo-web/echo-web.gradle index 4aa79d844..d420e066e 100644 --- a/echo-web/echo-web.gradle +++ b/echo-web/echo-web.gradle @@ -39,10 +39,10 @@ dependencies { implementation "io.spinnaker.kork:kork-cloud-config-server" implementation "io.spinnaker.kork:kork-core" implementation "io.spinnaker.kork:kork-config" + implementation "io.spinnaker.kork:kork-retrofit" implementation "io.spinnaker.kork:kork-web" - implementation "com.squareup.retrofit:retrofit" - implementation "com.squareup.retrofit:converter-jackson" - implementation "com.jakewharton.retrofit:retrofit1-okhttp3-client:1.1.0" + implementation "com.squareup.retrofit2:retrofit" + implementation "com.squareup.retrofit2:converter-jackson" implementation "com.fasterxml.jackson.core:jackson-databind" implementation "com.netflix.spectator:spectator-api" implementation "javax.validation:validation-api" @@ -59,6 +59,7 @@ dependencies { testImplementation "org.spockframework:spock-spring" testImplementation "org.junit.jupiter:junit-jupiter-api" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine" + testImplementation "com.squareup.retrofit2:retrofit-mock" } test { diff --git a/echo-web/src/main/java/com/netflix/spinnaker/echo/config/Front50Config.java b/echo-web/src/main/java/com/netflix/spinnaker/echo/config/Front50Config.java index b6560b751..4588ddbc4 100644 --- a/echo-web/src/main/java/com/netflix/spinnaker/echo/config/Front50Config.java +++ b/echo-web/src/main/java/com/netflix/spinnaker/echo/config/Front50Config.java @@ -16,54 +16,31 @@ package com.netflix.spinnaker.echo.config; -import com.jakewharton.retrofit.Ok3Client; -import com.netflix.spinnaker.config.DefaultServiceEndpoint; -import com.netflix.spinnaker.config.okhttp3.OkHttpClientProvider; +import com.netflix.spinnaker.config.OkHttp3ClientConfiguration; import com.netflix.spinnaker.echo.services.Front50Service; -import com.netflix.spinnaker.okhttp.SpinnakerRequestInterceptor; -import com.netflix.spinnaker.retrofit.Slf4jRetrofitLogger; +import com.netflix.spinnaker.kork.retrofit.ErrorHandlingExecutorCallAdapterFactory; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import retrofit.Endpoint; -import retrofit.Endpoints; -import retrofit.RestAdapter.Builder; -import retrofit.RestAdapter.LogLevel; -import retrofit.converter.JacksonConverter; +import retrofit2.Retrofit; +import retrofit2.converter.jackson.JacksonConverterFactory; @Configuration @Slf4j public class Front50Config { - @Bean - public LogLevel retrofitLogLevel() { - return LogLevel.BASIC; - } - - @Bean - public Endpoint front50Endpoint(@Value("${front50.base-url}") String front50BaseUrl) { - return Endpoints.newFixedEndpoint(front50BaseUrl); - } - @Bean public Front50Service front50Service( - Endpoint front50Endpoint, - OkHttpClientProvider clientProvider, - LogLevel retrofitLogLevel, - SpinnakerRequestInterceptor spinnakerRequestInterceptor) { + @Value("${front50.base-url}") String front50BaseUrl, + OkHttp3ClientConfiguration okHttp3ClientConfiguration) { log.info("front50 service loaded"); - return new Builder() - .setEndpoint(front50Endpoint) - .setConverter(new JacksonConverter()) - .setClient( - new Ok3Client( - clientProvider.getClient( - new DefaultServiceEndpoint("front50", front50Endpoint.getUrl())))) - .setRequestInterceptor(spinnakerRequestInterceptor) - .setLogLevel(retrofitLogLevel) - .setLog(new Slf4jRetrofitLogger(Front50Service.class)) + return new Retrofit.Builder() + .baseUrl(front50BaseUrl) + .client(okHttp3ClientConfiguration.createForRetrofit2().build()) + .addCallAdapterFactory(ErrorHandlingExecutorCallAdapterFactory.getInstance()) + .addConverterFactory(JacksonConverterFactory.create()) .build() .create(Front50Service.class); } diff --git a/echo-web/src/test/java/com/netflix/spinnaker/echo/telemetry/TelemetrySpec.java b/echo-web/src/test/java/com/netflix/spinnaker/echo/telemetry/TelemetrySpec.java index 6d878fc2b..787f01ac6 100644 --- a/echo-web/src/test/java/com/netflix/spinnaker/echo/telemetry/TelemetrySpec.java +++ b/echo-web/src/test/java/com/netflix/spinnaker/echo/telemetry/TelemetrySpec.java @@ -16,10 +16,13 @@ package com.netflix.spinnaker.echo.telemetry; +import static java.time.Instant.now; +import static org.mockito.BDDMockito.given; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.netflix.spinnaker.echo.Application; +import com.netflix.spinnaker.echo.pipelinetriggers.PipelineCache; import com.netflix.spinnaker.echo.pipelinetriggers.orca.OrcaService; import com.netflix.spinnaker.echo.services.Front50Service; import com.netflix.spinnaker.fiat.shared.FiatService; @@ -49,6 +52,8 @@ public class TelemetrySpec { @Autowired WebApplicationContext wac; + @MockBean PipelineCache pipelineCache; + @Autowired CircuitBreakerRegistry circuitBreakerRegistry; MockMvc mockMvc; @@ -56,6 +61,9 @@ public class TelemetrySpec { @BeforeEach public void setup() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); + given(pipelineCache.isRunning()).willReturn(true); + given(pipelineCache.isInitialized()).willReturn(true); + given(pipelineCache.getLastPollTimestamp()).willReturn(now()); } @Test