github.com/zntrio/harp/v2@v2.0.9/pkg/tasks/template/render.go (about) 1 // Licensed to Elasticsearch B.V. under one or more contributor 2 // license agreements. See the NOTICE file distributed with 3 // this work for additional information regarding copyright 4 // ownership. Elasticsearch B.V. licenses this file to you under 5 // the Apache License, Version 2.0 (the "License"); you may 6 // not use this file except in compliance with the License. 7 // You may obtain a copy of the License at 8 // 9 // http://www.apache.org/licenses/LICENSE-2.0 10 // 11 // Unless required by applicable law or agreed to in writing, 12 // software distributed under the License is distributed on an 13 // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 // KIND, either express or implied. See the License for the 15 // specific language governing permissions and limitations 16 // under the License. 17 18 package template 19 20 import ( 21 "context" 22 "fmt" 23 "io" 24 "io/fs" 25 26 "github.com/hashicorp/vault/api" 27 28 "github.com/zntrio/harp/v2/pkg/bundle" 29 "github.com/zntrio/harp/v2/pkg/sdk/cmdutil" 30 "github.com/zntrio/harp/v2/pkg/sdk/fsutil" 31 "github.com/zntrio/harp/v2/pkg/sdk/types" 32 "github.com/zntrio/harp/v2/pkg/tasks" 33 tplcmdutil "github.com/zntrio/harp/v2/pkg/template/cmdutil" 34 "github.com/zntrio/harp/v2/pkg/template/engine" 35 "github.com/zntrio/harp/v2/pkg/vault/kv" 36 ) 37 38 // RenderTask implements single template rendering task. 39 type RenderTask struct { 40 InputReader tasks.ReaderProvider 41 OutputWriter tasks.WriterProvider 42 ValueFiles []string 43 SecretLoaders []string 44 Values []string 45 StringValues []string 46 FileValues []string 47 LeftDelims string 48 RightDelims string 49 AltDelims bool 50 RootPath string 51 } 52 53 // Run the task. 54 func (t *RenderTask) Run(ctx context.Context) error { 55 var ( 56 reader io.Reader 57 err error 58 ) 59 60 // Create input reader 61 reader, err = t.InputReader(ctx) 62 if err != nil { 63 return fmt.Errorf("unable to create input reader: %w", err) 64 } 65 66 // Drain reader 67 body, err := io.ReadAll(reader) 68 if err != nil { 69 return fmt.Errorf("unable to drain input template reader: %w", err) 70 } 71 72 var fileRootFS fs.FS 73 if t.RootPath != "" { 74 var errRootFS error 75 fileRootFS, errRootFS = fsutil.From(t.RootPath) 76 if errRootFS != nil { 77 return fmt.Errorf("unable load files filesystem: %w", errRootFS) 78 } 79 } 80 81 // Prepare render context 82 renderCtx, err := prepareRenderContext(ctx, &renderContextConfig{ 83 ValueFiles: t.ValueFiles, 84 SecretLoaders: t.SecretLoaders, 85 Values: t.Values, 86 StringValues: t.StringValues, 87 FileValues: t.FileValues, 88 LeftDelims: t.LeftDelims, 89 RightDelims: t.RightDelims, 90 AltDelims: t.AltDelims, 91 FileRootPath: fileRootFS, 92 }) 93 if err != nil { 94 return fmt.Errorf("unable to prepare rendering context: %w", err) 95 } 96 97 // Compile and execute template 98 out, err := engine.RenderContext(renderCtx, string(body)) 99 if err != nil { 100 return fmt.Errorf("unable to produce output content: %w", err) 101 } 102 103 // Create output writer 104 writer, err := t.OutputWriter(ctx) 105 if err != nil { 106 return fmt.Errorf("unable to create output writer: %w", err) 107 } 108 109 // Write rendered content 110 fmt.Fprintf(writer, "%s", out) 111 112 // No error 113 return nil 114 } 115 116 // ----------------------------------------------------------------------------- 117 118 type renderContextConfig struct { 119 ValueFiles []string 120 SecretLoaders []string 121 Values []string 122 StringValues []string 123 FileValues []string 124 LeftDelims string 125 RightDelims string 126 AltDelims bool 127 FileRootPath fs.FS 128 } 129 130 func prepareRenderContext(ctx context.Context, cfg *renderContextConfig) (engine.Context, error) { 131 // Load values 132 valueOpts := tplcmdutil.ValueOptions{ 133 ValueFiles: cfg.ValueFiles, 134 Values: cfg.Values, 135 StringValues: cfg.StringValues, 136 FileValues: cfg.FileValues, 137 } 138 values, err := valueOpts.MergeValues() 139 if err != nil { 140 return nil, fmt.Errorf("unable to process input values: %w", err) 141 } 142 143 // Load files 144 var files engine.Files 145 if !types.IsNil(cfg.FileRootPath) { 146 var errFs error 147 files, errFs = tplcmdutil.Files(cfg.FileRootPath, ".") 148 if errFs != nil { 149 return nil, fmt.Errorf("unable to process files: %w", errFs) 150 } 151 } 152 153 // If alternative delimiters is used 154 if cfg.AltDelims { 155 cfg.LeftDelims = "[[" 156 cfg.RightDelims = "]]" 157 } 158 159 // Process secret readers 160 secretReaders := []engine.SecretReaderFunc{} 161 for _, sr := range cfg.SecretLoaders { 162 if sr == "vault" { 163 // Initialize Vault connection 164 vaultClient, errVault := api.NewClient(api.DefaultConfig()) 165 if errVault != nil { 166 return nil, fmt.Errorf("unable to initialize vault secret loader: %w", errVault) 167 } 168 169 secretReaders = append(secretReaders, kv.SecretGetter(ctx, vaultClient)) 170 continue 171 } 172 173 // Read container 174 containerReader, errLoader := cmdutil.Reader(sr) 175 if errLoader != nil { 176 return nil, fmt.Errorf("unable to read secret container: %w", errLoader) 177 } 178 179 // Load container 180 b, errBundle := bundle.FromContainerReader(containerReader) 181 if errBundle != nil { 182 return nil, fmt.Errorf("unable to decode secret container: %w", err) 183 } 184 185 // Append secret loader 186 secretReaders = append(secretReaders, bundle.SecretReader(b)) 187 } 188 189 // Create rendering context 190 renderCtx := engine.NewContext( 191 engine.WithName("template"), 192 engine.WithDelims(cfg.LeftDelims, cfg.RightDelims), 193 engine.WithValues(values), 194 engine.WithFiles(files), 195 engine.WithSecretReaders(secretReaders...), 196 ) 197 198 // No error 199 return renderCtx, nil 200 }