packages/constructs/L3/dataops/dataops-nifi-l3-construct/lib/cdk8s/nifi-registry-chart.ts (694 lines of code) (raw):
/*!
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
import * as cdk8s from 'cdk8s';
import { Construct } from 'constructs';
import * as fs from 'fs';
import { NifiRegistryBucketProps } from '../dataops-nifi-l3-construct';
import { NifiAuthorization, NifiIdentityAuthorizationOptions } from '../nifi-options';
import { ExternalSecretStore } from './external-secret-store';
import * as k8s from './imports/k8s';
import { NifiClusterChart } from './nifi-cluster-chart';
// nosemgrep
const { XMLParser, XMLBuilder } = require('fast-xml-parser');
export interface NodeResources {
readonly memory: string;
readonly cpu: string;
}
export interface EfsPersistentVolume {
readonly efsFsId: string;
readonly efsApId: string;
}
export interface NifiRegistryChartSamlProps {
readonly idpMetadataUrl: string;
readonly entityId: string;
}
export interface NifiRegistryClusterProps {
readonly adminIdentities: string[];
readonly nodeList: string[];
}
export interface NifiRegistryChartProps extends cdk8s.ChartProps, NifiIdentityAuthorizationOptions {
readonly nifiRegistryImageTag?: string;
readonly awsRegion: string;
readonly adminCredsSecretName: string;
readonly keystorePasswordSecretName: string;
readonly externalSecretsRoleArn: string;
readonly efsPersistentVolume: EfsPersistentVolume;
readonly efsStorageClassName: string;
readonly caIssuerName: string;
readonly hostname: string;
readonly hostedZoneName: string;
readonly httpsPort: number;
readonly nifiRegistryServiceRoleArn: string;
readonly nifiRegistryServiceRoleName: string;
readonly nifiRegistryCertDuration: string;
readonly nifiRegistryCertRenewBefore: string;
readonly nifiClusters: { [clusterName: string]: NifiRegistryClusterProps };
readonly certKeyAlg: string;
readonly certKeySize: number;
readonly nifiManagerImageUri: string;
readonly buckets?: { [bucketName: string]: NifiRegistryBucketProps };
}
export class NifiRegistryChart extends cdk8s.Chart {
private readonly props: NifiRegistryChartProps;
private static DEFAULT_NIFI_IMAGE_TAG = '1.25.0';
private readonly nifiNodes: string[];
constructor(scope: Construct, id: string, props: NifiRegistryChartProps) {
super(scope, id, props);
this.props = props;
this.nifiNodes = Object.entries(this.props.nifiClusters || {}).flatMap(clusterEntry => clusterEntry[1].nodeList);
const nifiRegistryService = this.createNifiRegistryService();
const nifiRegistrySecretName = this.createExternalSecrets(props);
this.createNifiRegistryDeployment(nifiRegistryService, nifiRegistrySecretName);
this.createSslResources(nifiRegistrySecretName);
}
public hash(): string {
const json = JSON.stringify(this.toJson(), undefined, 2);
const stableJson = json.replace(/Token\[.*?\]/g, 'Token');
// nosemgrep
const crypto = require('crypto');
// nosemgrep
const hash = crypto //NOSONAR not used in senstive context
.createHash('sha1') //NOSONAR not used in senstive context
.update(stableJson)
.digest('hex');
return hash;
}
private createExternalSecrets(props: NifiRegistryChartProps): string {
const secretStoreChart = new ExternalSecretStore(this, 'secret-store', {
storeName: 'external-secret-store',
...props,
});
const targetSecretName = 'nifi-registry-secret';
new cdk8s.ApiObject(this, 'nifi-registry-external-secret', {
apiVersion: 'external-secrets.io/v1beta1',
kind: 'ExternalSecret',
metadata: {
name: 'external-secret',
},
spec: {
refreshInterval: '1h',
secretStoreRef: {
name: secretStoreChart.secretStoreName,
kind: 'SecretStore',
},
target: {
name: targetSecretName,
creationPolicy: 'Owner',
},
data: [
{
secretKey: 'admin-creds',
remoteRef: {
key: props.adminCredsSecretName,
},
},
{
secretKey: 'keystore-password',
remoteRef: {
key: props.keystorePasswordSecretName,
},
},
],
},
});
return targetSecretName;
}
private createSslResources(nifiRegistrySecretName: string) {
const registryCert = new cdk8s.ApiObject(this, `nifi-registry-cert`, {
apiVersion: 'cert-manager.io/v1',
kind: 'Certificate',
metadata: {
name: `nifi-registry-cert`,
},
spec: {
isCA: false,
commonName: 'nifi-registry',
dnsNames: ['localhost', this.props.hostname],
secretName: `nifi-registry-ssl`,
privateKey: {
algorithm: this.props.certKeyAlg,
encoding: 'PKCS1',
size: this.props.certKeySize,
},
usages: ['server auth', 'client auth'],
issuerRef: {
name: this.props.caIssuerName,
kind: 'ClusterIssuer',
},
keystores: {
jks: {
create: true,
passwordSecretRef: {
name: nifiRegistrySecretName,
key: 'keystore-password',
},
},
},
duration: this.props.nifiRegistryCertDuration,
renewBefore: this.props.nifiRegistryCertRenewBefore,
},
});
const registryManagerCert = new cdk8s.ApiObject(this, `nifi-registry-manager-cert`, {
apiVersion: 'cert-manager.io/v1',
kind: 'Certificate',
metadata: {
name: `nifi-registry-manager-cert`,
},
spec: {
isCA: false,
commonName: `nifi-registry-manager`,
secretName: `nifi-registry-manager-ssl`,
privateKey: {
algorithm: this.props.certKeyAlg,
encoding: 'PKCS1',
size: this.props.certKeySize,
},
usages: ['client auth'],
issuerRef: {
name: this.props.caIssuerName,
kind: 'ClusterIssuer',
},
keystores: {
jks: {
create: true,
passwordSecretRef: {
name: nifiRegistrySecretName,
key: 'keystore-password',
},
},
},
duration: this.props.nifiRegistryCertDuration,
renewBefore: this.props.nifiRegistryCertRenewBefore,
},
});
return [registryManagerCert, registryCert];
}
private createNifiRegistryService(): k8s.KubeService {
const nifiRegistryServiceProps: k8s.KubeServiceProps = {
metadata: {
name: 'nifi-registry-svc',
labels: {
app: 'nifi-registry',
},
annotations: {
'external-dns.alpha.kubernetes.io/hostname': this.props.hostname,
'external-dns.alpha.kubernetes.io/ttl': '60',
},
},
spec: {
ports: [
{
port: this.props.httpsPort,
name: 'nifi-registry-ui',
},
],
clusterIp: 'None',
selector: {
app: 'nifi-registry',
},
},
};
return new k8s.KubeService(this, 'nifi-registry-svc', nifiRegistryServiceProps);
}
private createNifiRegistryDeployment(
nifiRegistryService: k8s.KubeService,
nifiRegistrySecretName: string,
): k8s.KubeStatefulSet {
// nosemgrep
const nifiRegistryInitScriptsConfigMapData = Object.fromEntries(
fs.readdirSync(`${__dirname}/../../scripts/nifi/`).map(fileName => {
// nosemgrep
return [fileName, fs.readFileSync(`${__dirname}/../../scripts/nifi/${fileName}`, 'utf-8')];
}),
);
const nifiRegistryInitConfigMap = new k8s.KubeConfigMap(this, 'nifi-registry-init-configmap', {
metadata: {
name: 'nifi-registry-init-scripts',
},
data: nifiRegistryInitScriptsConfigMapData,
});
const persistentVolumeClaim = new k8s.KubePersistentVolumeClaim(this, `nifi-registry-persistent-volume-claim`, {
metadata: {
name: 'nifi-registry-data-pvc',
},
spec: {
storageClassName: this.props.efsStorageClassName,
accessModes: ['ReadWriteOnce'],
resources: {
requests: {
storage: k8s.Quantity.fromString('3Gi'),
},
},
},
});
const persistentVolume = new k8s.KubePersistentVolume(this, `nifi-registry-persistent-volume`, {
metadata: {
name: `nifi-registry-vol-${this.props.efsPersistentVolume.efsFsId}-${this.props.efsPersistentVolume.efsApId}`,
labels: {
app: 'nifi-registry',
},
},
spec: {
volumeMode: 'Filesystem',
capacity: {
storage: k8s.Quantity.fromString('60Gi'),
},
accessModes: ['ReadWriteOnce'],
persistentVolumeReclaimPolicy: 'Retain',
storageClassName: this.props.efsStorageClassName,
csi: {
driver: 'efs.csi.aws.com',
volumeHandle: `${this.props.efsPersistentVolume.efsFsId}::${this.props.efsPersistentVolume.efsApId}`,
},
claimRef: {
name: persistentVolumeClaim.name,
namespace: this.namespace,
},
},
});
const sslBasePath = '/opt/nifi-registry/ssl';
const nifiRegistryDataDir = '/opt/nifi-registry/data';
const nifiRegistryInitBaseDir = '/opt/nifi-registry/init';
const nifiRegistryConfigMap = this.createRegistryConfigMap(sslBasePath, nifiRegistryDataDir);
const serviceAccount = new k8s.KubeServiceAccount(this, 'nifi-registry-service-account', {
metadata: {
name: 'nifi-registry',
annotations: {
'eks.amazonaws.com/role-arn': this.props.nifiRegistryServiceRoleArn,
},
},
});
const nifiRegistryDeploymentProps: k8s.KubeDeploymentProps = {
metadata: {
name: 'nifi-registry',
},
spec: {
selector: {
matchLabels: {
app: 'nifi-registry',
},
},
replicas: 1,
template: {
metadata: {
labels: {
app: 'nifi-registry',
},
},
spec: {
serviceAccountName: serviceAccount.name,
tolerations: [
{
key: 'eks.amazonaws.com/compute-type',
value: 'fargate',
},
],
dnsConfig: {
searches: [`${nifiRegistryService.name}.${this.namespace}.svc.cluster.local`],
},
securityContext: {
runAsUser: 1000,
runAsGroup: 1000,
fsGroup: 1000,
},
volumes: [
{
name: 'nifi-registry-init-scripts',
configMap: {
name: nifiRegistryInitConfigMap.name,
defaultMode: 0o755,
},
},
{
name: 'nifi-registry-config',
configMap: {
name: nifiRegistryConfigMap.name,
defaultMode: 0o755,
},
},
{
name: 'aws-creds',
emptyDir: {},
},
{
name: 'pip-local',
emptyDir: {},
},
{
name: `nifi-registry-ssl`,
secret: {
secretName: `nifi-registry-ssl`,
},
},
{
name: `nifi-registry-manager-ssl`,
secret: {
secretName: `nifi-registry-manager-ssl`,
},
},
{
name: 'nifi-registry-data',
persistentVolumeClaim: {
claimName: persistentVolumeClaim.name,
},
},
],
shareProcessNamespace: true,
containers: [
{
name: 'nifi-registry-manager',
image: this.props.nifiManagerImageUri,
command: ['sh', `/opt/nifi/scripts/nifi_registry_manager.sh`],
resources: {
requests: {
memory: k8s.Quantity.fromString('0.5Gi'),
cpu: k8s.Quantity.fromString('250m'),
},
limits: {
memory: k8s.Quantity.fromString('0.5Gi'),
cpu: k8s.Quantity.fromString('250m'),
},
},
env: [
{
name: 'NIFI_APP',
value: 'registry',
},
{
name: 'MANAGER_CONFIG',
value: `${nifiRegistryInitBaseDir}/conf/registry_manager.json`,
},
{
name: 'NIFI_INIT_DIR',
value: nifiRegistryInitBaseDir,
},
{
name: 'PYTHONUNBUFFERED',
value: '1',
},
{
name: 'NIFI_DATA_DIR',
value: nifiRegistryDataDir,
},
{
name: 'NIFI_SSL_BASE_PATH',
value: `${sslBasePath}/registry`,
},
{
name: 'NIFI_CERT_NAME',
value: 'nifi-registry',
},
{
name: 'NIFI_NODES',
value: this.nifiNodes.map(x => `CN=${x}`).join(','),
},
{
name: 'NIFI_KEYSTORE_PASSWORD',
valueFrom: {
secretKeyRef: {
name: nifiRegistrySecretName,
key: 'keystore-password',
optional: false,
},
},
},
{
name: 'NIFI_TRUSTSTORE_PASSWORD',
valueFrom: {
secretKeyRef: {
name: nifiRegistrySecretName,
key: 'keystore-password',
optional: false,
},
},
},
],
volumeMounts: [
{
name: 'nifi-registry-config',
mountPath: `${nifiRegistryInitBaseDir}/conf`,
},
{
name: 'aws-creds',
mountPath: `/home/nifi/.aws`,
},
{
name: 'nifi-registry-init-scripts',
mountPath: `${nifiRegistryInitBaseDir}/scripts`,
},
{
name: 'nifi-registry-data',
mountPath: `${nifiRegistryDataDir}`,
},
{
mountPath: `${sslBasePath}/manager`,
name: `nifi-registry-manager-ssl`,
readOnly: true,
},
{
mountPath: `${sslBasePath}/registry/nifi-registry`,
name: 'nifi-registry-ssl',
readOnly: true,
},
],
},
{
name: 'nifi-registry',
image: `apache/nifi-registry:${
this.props.nifiRegistryImageTag ?? NifiRegistryChart.DEFAULT_NIFI_IMAGE_TAG
}`,
ports: [{ containerPort: this.props.httpsPort }],
command: ['bash', '-c', `${nifiRegistryInitBaseDir}/scripts/nifi_registry_start.sh`],
resources: {
requests: {
memory: k8s.Quantity.fromString('1Gi'),
cpu: k8s.Quantity.fromString('500m'),
},
limits: {
memory: k8s.Quantity.fromString('1Gi'),
cpu: k8s.Quantity.fromString('500m'),
},
},
env: [
{
name: 'NIFI_INIT_DIR',
value: nifiRegistryInitBaseDir,
},
{
name: 'NIFI_DATA_DIR',
value: nifiRegistryDataDir,
},
{
name: 'NIFI_HOME',
value: '/opt/nifi-registry/nifi-registry-current',
},
{
name: 'NIFI_SSL_BASE_PATH',
value: sslBasePath,
},
{
name: 'NIFI_KEYSTORE_PASSWORD',
valueFrom: {
secretKeyRef: {
name: nifiRegistrySecretName,
key: 'keystore-password',
optional: false,
},
},
},
{
name: 'NIFI_TRUSTSTORE_PASSWORD',
valueFrom: {
secretKeyRef: {
name: nifiRegistrySecretName,
key: 'keystore-password',
optional: false,
},
},
},
],
volumeMounts: [
{
name: 'nifi-registry-config',
mountPath: `${nifiRegistryInitBaseDir}/conf`,
},
{
name: 'nifi-registry-init-scripts',
mountPath: `${nifiRegistryInitBaseDir}/scripts`,
},
{
name: 'nifi-registry-data',
mountPath: `${nifiRegistryDataDir}`,
},
{
name: 'aws-creds',
mountPath: `/home/nifi/.aws`,
},
{
mountPath: `${sslBasePath}`,
name: 'nifi-registry-ssl',
readOnly: true,
},
],
},
],
},
},
},
};
const sts = new k8s.KubeDeployment(this, 'nifi-registry-sts', nifiRegistryDeploymentProps);
sts.addDependency(persistentVolume);
sts.addDependency(persistentVolumeClaim);
return sts;
}
private createRegistryConfigMap(sslBasePath: string, nifiRegistryDataDir: string): k8s.KubeConfigMap {
// nosemgrep
const nifiRegistryBaseConfigMapData = Object.fromEntries(
fs.readdirSync(`${__dirname}/../../base_conf/nifi-registry`).map(fileName => {
// nosemgrep
return [fileName, fs.readFileSync(`${__dirname}/../../base_conf/nifi-registry/${fileName}`, 'utf-8')];
}),
);
const nifiRegistryConfigMap = new k8s.KubeConfigMap(this, 'nifi-registry-configmap', {
metadata: {
name: 'nifi-registry-config',
},
data: {
...nifiRegistryBaseConfigMapData,
'nifi-registry.properties': this.updateNifiProperties(
nifiRegistryBaseConfigMapData['nifi-registry.properties'],
nifiRegistryDataDir,
),
'authorizers.xml': this.updateAuthorizers(
nifiRegistryBaseConfigMapData['authorizers.xml'],
nifiRegistryDataDir,
),
'registry_manager.json': JSON.stringify(this.createRegistryManagerConfig(), undefined, 2),
'nifi-reg-cli.config': NifiClusterChart.createNifiToolkitConfig(
sslBasePath,
this.props.hostname,
this.props.httpsPort,
),
},
});
return nifiRegistryConfigMap;
}
private createRegistryManagerConfig() {
const additionalGroups: { [name: string]: string[] } = {};
const additionalIdentities: string[] = [];
const additionalAuthorizations: NifiAuthorization[] = [];
additionalGroups['admins'] = this.props.adminIdentities;
additionalIdentities.push(...this.props.adminIdentities);
additionalAuthorizations.push({
policyResourcePattern: '/.*',
actions: ['READ', 'WRITE', 'DELETE'],
groups: ['admins'],
});
const allNifiNodeIdentities = this.nifiNodes.map(x => `CN=${x}`);
additionalIdentities.push(...allNifiNodeIdentities);
additionalGroups['all_nifi_nodes'] = allNifiNodeIdentities;
additionalAuthorizations.push({
policyResourcePattern: '/proxy',
actions: ['READ', 'WRITE', 'DELETE'],
groups: ['all_nifi_nodes'],
});
if (this.props.externalNodeIdentities) {
additionalGroups['external_nodes'] = this.props.externalNodeIdentities;
additionalIdentities.push(...this.props.externalNodeIdentities);
}
const nifiClusterBuckets: { [key: string]: NifiRegistryBucketProps } = Object.fromEntries(
Object.entries(this.props.nifiClusters).map(clusterEntry => {
const clusterName = clusterEntry[0];
const cluster = clusterEntry[1];
const clusterNodes = cluster.nodeList.map(x => `CN=${x}`);
additionalGroups[`${clusterName}_admins`] = cluster.adminIdentities;
additionalGroups[`${clusterName}_nodes`] = clusterNodes;
const bucketProps: NifiRegistryBucketProps = {
WRITE: {
groups: [`${clusterName}_admins`],
},
READ: {
groups: [`${clusterName}_admins`, `${clusterName}_nodes`],
},
};
additionalIdentities.push(...cluster.adminIdentities);
return [clusterName, bucketProps];
}),
);
return {
buckets: { ...(this.props.buckets || {}), ...nifiClusterBuckets },
identities: [...(this.props.identities || []), ...additionalIdentities],
groups: { ...(this.props.groups || {}), ...additionalGroups },
policies: [...(this.props.policies || [])],
authorizations: [...(this.props.authorizations || []), ...additionalAuthorizations],
};
}
private updateAuthorizers(authorizersData: string, nifiRegistryDataDir: string): string {
const authorizersXmlObj = new XMLParser({
ignoreAttributes: false,
alwaysCreateTextNode: true,
}).parse(authorizersData);
interface XmlProp {
'@_name': string;
'#text': string;
}
const userGroupProviderProps: XmlProp[] = authorizersXmlObj['authorizers']['userGroupProvider']['property'];
userGroupProviderProps.forEach(prop => {
if (prop['@_name'] == 'Users File') {
prop['#text'] = `${nifiRegistryDataDir}/users.xml`;
} else if (prop['@_name'] == 'Initial User Identity 1') {
prop['#text'] = `CN=nifi-registry-manager`;
}
});
const accessPolicyProviderProps: XmlProp[] = authorizersXmlObj['authorizers']['accessPolicyProvider']['property'];
accessPolicyProviderProps.forEach(prop => {
if (prop['@_name'] == 'Authorizations File') {
prop['#text'] = `${nifiRegistryDataDir}/authorizations.xml`;
} else if (prop['@_name'] == 'Initial Admin Identity') {
prop['#text'] = `CN=nifi-registry-manager`;
}
});
this.nifiNodes.forEach(node => {
userGroupProviderProps.push({
'@_name': `Initial User Identity ${node}`,
'#text': `CN=${node}`,
});
accessPolicyProviderProps.push({
'@_name': `Nifi Identity ${node}`,
'#text': `CN=${node}`,
});
});
const authorizersXml: string = new XMLBuilder({
ignoreAttributes: false,
format: true,
}).build(authorizersXmlObj);
return authorizersXml;
}
private updateNifiProperties(nifiRegistryPropertiesData: string, nifiRegistryDataDir: string): string {
const nifiRegistryPropertiesMap: { [key: string]: string } = Object.fromEntries(
nifiRegistryPropertiesData
.split('\n')
.filter(line => {
return !/^\s*#/.test(line) && !/^\s*$/.test(line);
})
.map(line => {
return [line.split(/=(.*)/s)[0], line.split(/=(.*)/s)[1]];
}),
);
//perform config-driven overrides here
nifiRegistryPropertiesMap['nifi.registry.web.https.port'] = this.props.httpsPort.toString();
nifiRegistryPropertiesMap['nifi.registry.web.https.host'] = this.props.hostname;
nifiRegistryPropertiesMap['nifi.registry.web.http.port'] = '';
nifiRegistryPropertiesMap['nifi.registry.web.http.host'] = '';
nifiRegistryPropertiesMap['nifi.registry.security.keystore'] = `${nifiRegistryDataDir}/ssl/keystore/keystore.jks`;
nifiRegistryPropertiesMap['nifi.registry.security.keystoreType'] = 'JKS';
nifiRegistryPropertiesMap['nifi.registry.security.keystorePasswd'] = 'INIT_KEYSTORE_PASSWORD'; //NOSONAR placeholder value replaced at runtime
nifiRegistryPropertiesMap['nifi.registry.security.keyPasswd'] = 'INIT_KEYSTORE_PASSWORD'; //NOSONAR placeholder value replaced at runtime
nifiRegistryPropertiesMap[
'nifi.registry.security.truststore'
] = `${nifiRegistryDataDir}/ssl/truststore/truststore.jks`;
nifiRegistryPropertiesMap['nifi.registry.security.truststoreType'] = 'JKS';
nifiRegistryPropertiesMap['nifi.registry.security.truststorePasswd'] = 'INIT_TRUSTSTORE_PASSWORD'; //NOSONAR placeholder value replaced at runtime
nifiRegistryPropertiesMap['nifi.registry.security.user.authorizer'] = 'managed-authorizer';
nifiRegistryPropertiesMap['nifi.registry.security.autoreload.enabled'] = 'true';
nifiRegistryPropertiesMap['nifi.registry.security.autoreload.interval'] = '10 secs';
nifiRegistryPropertiesMap['nifi.registry.security.needClientAuth'] = 'true';
nifiRegistryPropertiesMap['nifi.registry.security.user.login.identity.provider'] = 'single-user-provider';
const nifiRegistryProperties = Object.entries(nifiRegistryPropertiesMap)
.map(entry => {
return `${entry[0]}=${entry[1]}`;
})
.join('\n');
return nifiRegistryProperties;
}
}