All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.netflix.spinnaker.gate.config.GateConfig.groovy Maven / Gradle / Ivy

There is a newer version: 6.64.0
Show newest version
/*
 * Copyright 2014 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.netflix.spinnaker.gate.config

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
import com.netflix.spectator.api.Registry
import com.netflix.spinnaker.config.DefaultServiceEndpoint
import com.netflix.spinnaker.config.OkHttp3ClientConfiguration
import com.netflix.spinnaker.config.PluginsAutoConfiguration
import com.netflix.spinnaker.fiat.shared.FiatClientConfigurationProperties
import com.netflix.spinnaker.fiat.shared.FiatPermissionEvaluator
import com.netflix.spinnaker.fiat.shared.FiatService
import com.netflix.spinnaker.fiat.shared.FiatStatus
import com.netflix.spinnaker.filters.AuthenticatedRequestFilter
import com.netflix.spinnaker.gate.converters.JsonHttpMessageConverter
import com.netflix.spinnaker.gate.converters.YamlHttpMessageConverter
import com.netflix.spinnaker.gate.filters.RequestLoggingFilter
import com.netflix.spinnaker.gate.filters.RequestSheddingFilter
import com.netflix.spinnaker.gate.filters.ResetAuthenticatedRequestFilter
import com.netflix.spinnaker.gate.plugins.deck.DeckPluginConfiguration
import com.netflix.spinnaker.gate.plugins.web.PluginWebConfiguration
import com.netflix.spinnaker.gate.services.internal.*
import com.netflix.spinnaker.kork.client.ServiceClientProvider
import com.netflix.spinnaker.kork.dynamicconfig.DynamicConfigService
import com.netflix.spinnaker.kork.web.context.AuthenticatedRequestContextProvider
import com.netflix.spinnaker.kork.web.context.RequestContextProvider
import com.netflix.spinnaker.kork.web.selector.DefaultServiceSelector
import com.netflix.spinnaker.kork.web.selector.SelectableService
import com.netflix.spinnaker.kork.web.selector.ServiceSelector
import com.netflix.spinnaker.okhttp.OkHttp3MetricsInterceptor
import com.netflix.spinnaker.okhttp.OkHttpClientConfigurationProperties
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.boot.web.servlet.FilterRegistrationBean
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Import
import org.springframework.context.annotation.Primary
import org.springframework.core.Ordered
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder
import org.springframework.util.CollectionUtils
import org.springframework.web.client.RestTemplate
import retrofit.Endpoint

import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors

import static retrofit.Endpoints.newFixedEndpoint

@CompileStatic
@Configuration
@Slf4j
@Import([PluginsAutoConfiguration, DeckPluginConfiguration, PluginWebConfiguration])
class GateConfig {

  private ServiceClientProvider serviceClientProvider

  @Autowired
  void setServiceClientProvider(ServiceClientProvider serviceClientProvider) {
    this.serviceClientProvider = serviceClientProvider
  }

  @Bean
  @ConditionalOnMissingBean(RestTemplate)
  RestTemplate restTemplate() {
    new RestTemplate()
  }

  @Bean
  ExecutorService executorService() {
    Executors.newCachedThreadPool()
  }

  @Autowired
  Registry registry

  @Autowired
  ServiceConfiguration serviceConfiguration

  @Autowired
  Jackson2ObjectMapperBuilder objectMapperBuilder

  /**
   * This needs to be before the yaml converter in order for json to be the default
   * response type.
   */
  @Bean
  AbstractJackson2HttpMessageConverter jsonHttpMessageConverter() {
    return new JsonHttpMessageConverter(objectMapperBuilder.build())
  }

  @Bean
  AbstractJackson2HttpMessageConverter yamlHttpMessageConverter() {
    return new YamlHttpMessageConverter(objectMapperBuilder.factory(new YAMLFactory()).build())
  }

  @Bean
  RequestContextProvider requestContextProvider() {
    return new AuthenticatedRequestContextProvider();
  }

  @Bean
  OrcaServiceSelector orcaServiceSelector(RequestContextProvider contextProvider) {
    return new OrcaServiceSelector(createClientSelector("orca", OrcaService), contextProvider)
  }

  @Bean
  @Primary
  FiatService fiatService() {
    // always create the fiat service even if 'services.fiat.enabled' is 'false' (it can be enabled dynamically)
    createClient "fiat", FiatService, null, true
  }

  @Bean
  ExtendedFiatService extendedFiatService() {
    // always create the fiat service even if 'services.fiat.enabled' is 'false' (it can be enabled dynamically)
    createClient "fiat", ExtendedFiatService,  null, true
  }

  @Bean
  @ConditionalOnProperty("services.fiat.config.dynamic-endpoints.login")
  FiatService fiatLoginService() {
    // always create the fiat service even if 'services.fiat.enabled' is 'false' (it can be enabled dynamically)
    createClient "fiat", FiatService,  "login", true
  }


  @Bean
  Front50Service front50Service() {
    createClient "front50", Front50Service
  }

  @Bean
  ClouddriverService clouddriverService() {
    createClient "clouddriver", ClouddriverService
  }

  @Bean
  ClouddriverServiceSelector clouddriverServiceSelector(ClouddriverService defaultClouddriverService,
                                                        DynamicConfigService dynamicConfigService,
                                                        RequestContextProvider contextProvider
  ) {
    if (serviceConfiguration.getService("clouddriver").getConfig().containsKey("dynamicEndpoints")) {
      def endpoints = (Map) serviceConfiguration.getService("clouddriver").getConfig().get("dynamicEndpoints")
      // translates the following config:
      //   dynamicEndpoints:
      //     deck: url

      // into a SelectableService that would be produced by an equivalent config:
      //   baseUrl: url
      //   config:
      //     selectorClass: com.netflix.spinnaker.kork.web.selector.ByUserOriginSelector
      //     priority: 2
      //     origin: deck

      ServiceSelector defaultSelector = new DefaultServiceSelector(
        defaultClouddriverService,
        1,
        null)

      List selectors = []
      endpoints.each { sourceApp, url ->
        def service = buildService("clouddriver",  ClouddriverService, newFixedEndpoint(url))
        selectors << new ByUserOriginSelector(service, 2, ['origin': (Object) sourceApp])
      }

      return new ClouddriverServiceSelector(
        new SelectableService(selectors + defaultSelector), dynamicConfigService, contextProvider)
    }

    SelectableService selectableService = createClientSelector("clouddriver", ClouddriverService)
    return new ClouddriverServiceSelector(selectableService, dynamicConfigService, contextProvider)
  }

  //---- semi-optional components:
  @Bean
  @ConditionalOnProperty('services.rosco.enabled')
  RoscoService roscoService() {
    createClient "rosco", RoscoService
  }

  @Bean
  @ConditionalOnProperty('services.rosco.enabled')
  RoscoServiceSelector roscoServiceSelector(RoscoService defaultService) {
    return new RoscoServiceSelector(
      createClientSelector("rosco", RoscoService),
      defaultService
    )
  }

  //---- optional backend components:
  @Bean
  @ConditionalOnProperty('services.echo.enabled')
  EchoService echoService() {
    createClient "echo", EchoService
  }

  @Bean
  @ConditionalOnProperty('services.igor.enabled')
  IgorService igorService() {
    createClient "igor", IgorService
  }

  @Bean
  @ConditionalOnProperty('services.mine.enabled')
  MineService mineService() {
    createClient "mine", MineService
  }

  @Bean
  @ConditionalOnProperty("services.keel.enabled")
  KeelService keelService() {
    createClient "keel", KeelService
  }

  @Bean
  @ConditionalOnProperty('services.kayenta.enabled')
  KayentaService kayentaService(OkHttpClientConfigurationProperties props,
                                OkHttp3MetricsInterceptor interceptor,
                                @Value('${services.kayenta.externalhttps:false}') boolean kayentaExternalHttps) {
    if (kayentaExternalHttps) {
      def noSslCustomizationProps = props.clone()
      noSslCustomizationProps.keyStore = null
      noSslCustomizationProps.trustStore = null
      def okHttpClient = new OkHttp3ClientConfiguration(noSslCustomizationProps, interceptor).create().build()
      createClient "kayenta", KayentaService
    } else {
      createClient "kayenta", KayentaService
    }
  }

  @Bean
  @ConditionalOnProperty('services.swabbie.enabled')
  SwabbieService swabbieService() {
    createClient("swabbie", SwabbieService)
  }

  private  T createClient(String serviceName,
                             Class type,
                             String dynamicName = null,
                             boolean forceEnabled = false) {
    Service service = serviceConfiguration.getService(serviceName)
    if (service == null) {
      throw new IllegalArgumentException("Unknown service ${serviceName} requested of type ${type}")
    }

    if (!service.enabled && !forceEnabled) {
      return null
    }

    Endpoint endpoint = serviceConfiguration.getServiceEndpoint(serviceName, dynamicName)

    buildService(serviceName, type, endpoint)
  }

  private  T buildService(String serviceName, Class type, Endpoint endpoint) {
    ObjectMapper objectMapper = objectMapperBuilder.build() as ObjectMapper
    if(serviceName.equals("echo")) {
      objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)
      objectMapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, false)
    }
    serviceClientProvider.getService(type, new DefaultServiceEndpoint(serviceName, endpoint.url), objectMapper)
  }

  private  SelectableService createClientSelector(String serviceName, Class type) {
    Service service = serviceConfiguration.getService(serviceName)
    if (CollectionUtils.isEmpty(service?.getBaseUrls())) {
      throw new IllegalArgumentException("Unknown service ${serviceName} requested of type ${type}")
    }

    return new SelectableService(
      service.getBaseUrls().collect {
        def selector = new DefaultServiceSelector(
          buildService(
            serviceName,
            type,
            newFixedEndpoint(it.baseUrl)),
          it.priority,
          it.config)

        def selectorClass = it.config?.selectorClass as Class
        if (selectorClass) {
          log.debug("Initializing selector class {} with baseUrl={}, priority={}, config={}", selectorClass, it.baseUrl, it.priority, it.config)
          selector = selectorClass.getConstructors()[0].newInstance(
            selector.service, it.priority, it.config
          )
        }
        selector
      } as List
    )
  }

  @Bean
  FilterRegistrationBean resetAuthenticatedRequestFilter() {
    def frb = new FilterRegistrationBean<>(new ResetAuthenticatedRequestFilter())
    frb.order = Ordered.HIGHEST_PRECEDENCE
    return frb
  }

  /**
   * This AuthenticatedRequestFilter pulls the email and accounts out of the Spring
   * security context in order to enabling forwarding them to downstream components.
   *
   * Additionally forwards request origin metadata (deck vs api).
   */
  @Bean
  FilterRegistrationBean authenticatedRequestFilter() {
    // no need to force the `AuthenticatedRequestFilter` to create a request id as that is
    // handled by the `RequestTimingFilter`.
    def frb = new FilterRegistrationBean<>(new AuthenticatedRequestFilter(true, true, false, false))
    frb.order = Ordered.LOWEST_PRECEDENCE - 1
    return frb
  }

  @Bean
  @ConditionalOnProperty("request-logging.enabled")
  FilterRegistrationBean requestLoggingFilter() {
    def frb = new FilterRegistrationBean<>(new RequestLoggingFilter())
    // this filter should be placed very early in the request chain to ensure we track an accurate start time and
    // have a request id available to propagate across thread and service boundaries.
    frb.order = Ordered.HIGHEST_PRECEDENCE + 1
    return frb
  }

  @Bean
  FilterRegistrationBean requestSheddingFilter(DynamicConfigService dynamicConfigService) {
    def frb = new FilterRegistrationBean<>(new RequestSheddingFilter(dynamicConfigService, registry))

    /*
     * This filter should:
     * - be placed early in the request chain to allow for requests to be shed prior to the security filter chain.
     * - be placed after the RequestLoggingFilter such that shed requests are logged.
     */
    frb.order = Ordered.HIGHEST_PRECEDENCE + 2
    return frb
  }

  @Bean
  FiatStatus fiatStatus(DynamicConfigService dynamicConfigService,
                        Registry registry,
                        FiatClientConfigurationProperties fiatClientConfigurationProperties) {
    return new FiatStatus(registry, dynamicConfigService, fiatClientConfigurationProperties)
  }

  @Bean
  FiatPermissionEvaluator fiatPermissionEvaluator(FiatStatus fiatStatus,
                                                  Registry registry,
                                                  FiatService fiatService,
                                                  FiatClientConfigurationProperties fiatClientConfigurationProperties) {
    return new FiatPermissionEvaluator(registry, fiatService, fiatClientConfigurationProperties, fiatStatus)
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy