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  }