/**
 * Copyright 2019 Google LLC
 *
 * 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 * as fs from 'fs' ; import { existsSync , mkdirSync } from 'fs' ; import * as glob from 'glob' ; import { DumpOptions , safeDump } from 'js-yaml' ; import * as kpt from 'kpt-functions' ; import * as path from 'path' ; import { isNamespace , Namespace } from './gen/io.k8s.api.core.v1' ; export const SINK_DIR = 'sink_dir' ; export const OVERWRITE = 'overwrite' ; const DEFAULT_NAMESPACE = 'default' ; const YAML_STYLE = { indent : 2 , noArrayIndent : true , } ; export async function writeYaml ( configs ) { const sinkDir = configs . getFunctionConfigValueOrThrow ( SINK_DIR ) ; const overwrite = configs . getFunctionConfigValue ( OVERWRITE ) === 'true' ; const yamls = listYamlFiles ( sinkDir ) ; if ( ! overwrite && yamls . length > 0 ) { throw new Error ( `sink dir contains YAML files and overwrite is not set to string 'true'.` ) ; } const filesToDelete = new Set ( yamls ) ; configs . groupBy ( buildSourcePath ) . forEach ( ( [ p , configsAtPath ] ) => { const documents = configsAtPath . sort ( compareSourceIndex ) . map ( ( config ) => kpt . removeAnnotation ( config , kpt . SOURCE_INDEX_ANNOTATION ) ) . map ( ( config ) => kpt . removeAnnotation ( config , kpt . SOURCE_PATH_ANNOTATION ) ) . map ( toYaml ) ; const file = path . join ( sinkDir , p ) ; const dir = path . dirname ( file ) ; if ( ! fs . existsSync ( dir ) ) { fs . mkdirSync ( path . dirname ( file ) , { recursive : true } ) ; } const contents = documents . join ( '---\n' ) ; if ( fs . existsSync ( file ) ) { filesToDelete . delete ( file ) ; const currentContents = fs . readFileSync ( file ) . toString ( ) ; if ( contents === currentContents ) { return ; } } fs . writeFileSync ( file , contents , 'utf8' ) ; } ) ; filesToDelete . forEach ( ( file ) => { fs . unlinkSync ( file ) ; } ) ; } writeYaml . usage = `
Creates a directory of YAML files.

If an object has the '${ kpt . SOURCE_PATH_ANNOTATION }' annotation, the file is created at that path.
Otherwise, this convention is used for the file path:

[<namespace>]/<kind>_<name>.yaml

default/<kind>_<name>.yaml

e.g.:

my-namespace/rolebinding_alice.yaml
default/role_pod-creator.yaml
clusterrole_sre.yaml

If two objects have the same path annotation, a multi-document file is
created. Ordering within this file is based on the '${ kpt . SOURCE_INDEX_ANNOTATION }' annotation.

Configured using a ConfigMap with the following keys:

${ SINK_DIR }: Path to the config directory to write to.
${ OVERWRITE }: [Optional] If 'true', overwrite existing YAML files. Otherwise, fail if any YAML files exist.

Example:

apiVersion: v1
kind: ConfigMap
data:
  ${ SINK_DIR }: /path/to/sink/dir
  ${ OVERWRITE }: 'true'
metadata:
  name: my-config
` ; function listYamlFiles ( dir ) { if ( ! existsSync ( dir ) ) { mkdirSync ( dir , { recursive : true } ) ; } return glob . sync ( dir + '/**/*.+(yaml|yml)' ) ; } function toYaml ( o ) { try { return safeDump ( o , YAML_STYLE ) ; } catch ( err ) { throw new Error ( `Failed to write YAML for object: ${ JSON . stringify ( o ) }: ${ err }` ) ; } } param o returns export function buildSourcePath ( o ) { const annotationPath = kpt . getAnnotation ( o , kpt . SOURCE_PATH_ANNOTATION ) ; if ( annotationPath ) { return annotationPath ; } if ( isNamespace ( o ) ) { return path . join ( o . metadata . name , `${ Namespace . kind . toLowerCase ( ) }.yaml` ) ; } const basePath = `${ o . kind . toLowerCase ( ) }_${ o . metadata . name }.yaml` ; if ( o . metadata . namespace !== undefined ) { let dir = o . metadata . namespace ; if ( dir === '' ) { dir = DEFAULT_NAMESPACE ; } return path . join ( dir , basePath ) ; } return basePath ; } /**
 * Sorts the array of objects using 'index' annotation.
 *
 * If an object is missing index annotation, default to using zero.
 */ function compareSourceIndex ( o1 , o2 ) { const i1 = Number ( kpt . getAnnotation ( o1 , kpt . SOURCE_INDEX_ANNOTATION ) ) || 0 ; const i2 = Number ( kpt . getAnnotation ( o2 , kpt . SOURCE_INDEX_ANNOTATION ) ) || 0 ; return i1 - i2 ; }