/**
 * Copyright 2021 Opstrace, 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.
 */ import { KubeConfig , V1EnvVar , V1PodSpec } from "@kubernetes/client-node" ; import { DockerImages , getImagePullSecrets } from "@opstrace/controller-config" ; import { ConfigMap , Deployment , K8sResource , ResourceCollection , Secret , Service , V1ServicemonitorResource } from "@opstrace/kubernetes" ; import { Integration } from "../../reducers/graphql/integrations" ; import { toTenantName , toTenantNamespace } from "../../helpers" ; import { State } from "../../reducer" ; import { log } from "@opstrace/utils" ; export function IntegrationResources ( state , kubeConfig ) { const collection = new ResourceCollection ( ) ; state . graphql . Integrations . resources . forEach ( integration => { toKubeResources ( state , integration , kubeConfig ) . forEach ( resource => collection . add ( resource ) ) ; } ) ; return collection ; } type BlackboxConfig = { probes : { [ key ] : string ; } [ ] ; configFile : string | null ; } ; type BlackboxIntegrationData = { config : BlackboxConfig ; } ; type CloudwatchIntegrationData = { config : string ; credentials : { [ key ] : string ; } ; } ; type StackdriverIntegrationData = { config : Record < string , unknown > ; credentials : string ; } ; type AzureIntegrationData = { config : string ; credentials : { [ key ] : string ; } | null ; } ; const toKubeResources = ( state , integration , kubeConfig ) : K8sResource [ ] => { const k8sName = `integration-${ integration . key }` ; const tenantName = toTenantName ( integration . tenant_id , state . tenants . list . tenants ) ; if ( tenantName == null ) { log . warning ( "Skipping deployment of %s integration '%s': missing tenant_id=%s in local state: %s" , integration . kind , integration . name , integration . tenant_id , state . tenants . list . tenants ) ; return [ ] ; } const k8sLabels = { app : k8sName , tenant : tenantName , "opstrace.com/integration-key" : integration . key , "opstrace.com/integration-kind" : integration . kind } ; const k8sMetadata = { name : k8sName , namespace : toTenantNamespace ( tenantName ) , labels : k8sLabels } ; const resources = [ ] ; let podSpec ; const customMonitorEndpoints = [ ] ; let configMapData = null ; let secretData = null ; if ( integration . kind == "exporter-cloudwatch" ) { const integrationData = integration . data as CloudwatchIntegrationData ; configMapData = { "config.yml" : integrationData . config } ; secretData = integrationData . credentials ; podSpec = { containers : [ { name : "exporter" , image : DockerImages . exporterCloudwatch , envFrom : [ { secretRef : { name : k8sName } } ] , ports : [ { name : "metrics" , containerPort : 9106 } ] , volumeMounts : [ { name : "config" , mountPath : "/config" } ] , startupProbe : { httpGet : { path : "/-/healthy" , port : "metrics" as any } , failureThreshold : 3 , initialDelaySeconds : 10 , periodSeconds : 30 , successThreshold : 1 , timeoutSeconds : 5 } } ] , volumes : [ { name : "config" , configMap : { name : k8sName } } ] } ; } else if ( integration . kind == "exporter-cloud-monitoring" ) { const integrationData = integration . data as StackdriverIntegrationData ; secretData = { "secret.json" : integrationData . credentials } ; const env = [ ] ; env . push ( { name : "GOOGLE_APPLICATION_CREDENTIALS" , value : "/credential/secret.json" } ) ; env . push ( { name : "STACKDRIVER_EXPORTER_WEB_LISTEN_ADDRESS" , value : ":9255" } ) ; env . push ( { name : "STACKDRIVER_EXPORTER_WEB_TELEMETRY_PATH" , value : "/metrics" } ) ; const bannedEnv = new Set < string > ( ) ; env . forEach ( e => bannedEnv . add ( e . name ) ) ; Object . entries ( integrationData . config ) . forEach ( ( [ k , v ] ) => { const name = "STACKDRIVER_EXPORTER_" + k . toUpperCase ( ) . replace ( /\W/g , "_" ) ; if ( bannedEnv . has ( name ) ) { return ; } const value = v instanceof Array ? v . toString ( ) : ( v as string ) ; env . push ( { name , value } ) ; } ) ; podSpec = { containers : [ { name : "exporter" , image : DockerImages . exporterStackdriver , env , ports : [ { name : "metrics" , containerPort : 9255 } ] , volumeMounts : [ { name : "credential" , mountPath : "/credential" } ] , startupProbe : { httpGet : { path : "/" , port : "metrics" as any } , failureThreshold : 3 , initialDelaySeconds : 10 , periodSeconds : 30 , successThreshold : 1 , timeoutSeconds : 5 } } ] , volumes : [ { name : "credential" , secret : { secretName : k8sName } } ] } ; } else if ( integration . kind == "exporter-azure" ) { const integrationData = integration . data as AzureIntegrationData ; configMapData = { "azure.yml" : integrationData . config } ; secretData = integrationData . credentials ; podSpec = { containers : [ { name : "exporter" , image : DockerImages . exporterAzure , command : [ "/bin/azure_metrics_exporter" , "--config.file=/config/azure.yml" ] , envFrom : [ { secretRef : { name : k8sName } } ] , ports : [ { name : "metrics" , containerPort : 9276 } ] , volumeMounts : [ { name : "config" , mountPath : "/config" } ] , startupProbe : { httpGet : { path : "/" , port : "metrics" as any } , failureThreshold : 3 , initialDelaySeconds : 10 , periodSeconds : 30 , successThreshold : 1 , timeoutSeconds : 5 } } ] , volumes : [ { name : "config" , configMap : { name : k8sName } } ] } ; } else if ( integration . kind == "exporter-blackbox" ) { const integrationData = integration . data as BlackboxIntegrationData ; const exporterConfig = integrationData . config ; if ( exporterConfig . configFile ) { configMapData = { "config.yml" : exporterConfig . configFile } ; } podSpec = { imagePullSecrets : getImagePullSecrets ( ) , containers : [ { name : "exporter" , image : DockerImages . exporterBlackbox , ports : [ { name : "metrics" , containerPort : 9115 } ] , volumeMounts : exporterConfig . configFile ? [ { name : "config" , mountPath : "/etc/blackbox_exporter" } ] : [ ] , startupProbe : { httpGet : { path : "/-/healthy" , port : "metrics" as any } , failureThreshold : 3 , initialDelaySeconds : 10 , periodSeconds : 30 , successThreshold : 1 , timeoutSeconds : 5 } } ] , volumes : exporterConfig . configFile ? [ { name : "config" , configMap : { name : k8sName } } ] : [ ] } ; if ( ! exporterConfig . probes ) { log . warning ( "Skipping deployment of blackbox integration %s/'%s': missing required 'probes' in config" , integration . tenant_id , integration . name ) ; return [ ] ; } for ( const probeIdx in exporterConfig . probes ) { const params = Object . fromEntries ( Object . entries ( exporterConfig . probes [ probeIdx ] ) . map ( ( [ k , v ] ) => [ k , [ v ] ] ) ) ; const relabelings = Object . entries ( exporterConfig . probes [ probeIdx ] ) . map ( ( [ k , v ] ) => ( { sourceLabels : [ ] , targetLabel : k , replacement : v } ) ) ; relabelings . push ( { sourceLabels : [ ] , targetLabel : "integration_id" , replacement : integration . id } ) ; customMonitorEndpoints . push ( { interval : "30s" , port : "metrics" , path : "/probe" , params , relabelings } ) ; } } else { log . debug ( "Ignoring integration %s/%s with kind: %s" , integration . tenant_id , integration . id , integration . kind ) ; return [ ] ; } resources . push ( new Deployment ( { apiVersion : "apps/v1" , kind : "Deployment" , metadata : k8sMetadata , spec : { replicas : 1 , selector : { matchLabels : { app : k8sName } } , template : { metadata : { labels : k8sLabels } , spec : podSpec } } } , kubeConfig ) ) ; resources . push ( new Service ( { apiVersion : "v1" , kind : "Service" , metadata : k8sMetadata , spec : { ports : [ { name : "metrics" , port : 9000 , targetPort : "metrics" as any } ] , selector : { app : k8sName } } } , kubeConfig ) ) ; resources . push ( new V1ServicemonitorResource ( { apiVersion : "monitoring.coreos.com/v1" , kind : "ServiceMonitor" , metadata : k8sMetadata , spec : { jobLabel : "opstrace.com/integration-key" , endpoints : customMonitorEndpoints . length != 0 ? customMonitorEndpoints : [ { scrapeTimeout : "45s" , interval : "60s" , port : "metrics" , path : "/metrics" , relabelings : [ { sourceLabels : [ ] , targetLabel : "integration_id" , replacement : integration . id } ] } ] , selector : { matchLabels : { app : k8sName } } } } , kubeConfig ) ) ; if ( configMapData ) { resources . push ( new ConfigMap ( { apiVersion : "v1" , kind : "ConfigMap" , metadata : k8sMetadata , data : configMapData } , kubeConfig ) ) ; } if ( secretData ) { const datab64 = Object . fromEntries ( Object . entries ( secretData ) . map ( ( [ k , v ] ) => [ k , Buffer . from ( v ) . toString ( "base64" ) ] ) ) ; resources . push ( new Secret ( { apiVersion : "v1" , kind : "Secret" , metadata : k8sMetadata , data : datab64 } , kubeConfig ) ) ; } return resources ; } ;