mirror of
https://github.com/absmach/magistrala.git
synced 2026-06-22 20:00:22 +00:00
683809dc6b
Property Based Tests / api-test (push) Has been cancelled
Continuous Delivery / lint-and-build (push) Has been cancelled
Deploy GitHub Pages / swagger-ui (push) Has been cancelled
CI Pipeline / Lint Proto (push) Has been cancelled
Continuous Delivery / Build and Push Docker Images (push) Has been cancelled
CI Pipeline / lint-and-build (push) Has been cancelled
CI Pipeline / Test ${{ matrix.module }} (push) Has been cancelled
CI Pipeline / Upload Coverage (push) Has been cancelled
CI Pipeline / Detect Changes (push) Has been cancelled
Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>
175 lines
4.9 KiB
Go
175 lines
4.9 KiB
Go
// Copyright (c) Abstract Machines
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package bootstrap
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"text/template"
|
|
|
|
"github.com/absmach/magistrala/pkg/errors"
|
|
"github.com/pelletier/go-toml/v2"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// Renderer renders a Profile's content template into a concrete device
|
|
// configuration. All input data must already be stored in Bootstrap — no
|
|
// external service calls are allowed inside Render.
|
|
type Renderer interface {
|
|
Render(profile Profile, enrollment Config, bindings []BindingSnapshot) ([]byte, error)
|
|
}
|
|
|
|
// ErrRenderFailed is returned when template execution or output validation fails.
|
|
var ErrRenderFailed = errors.New("failed to render profile template")
|
|
|
|
type renderer struct{}
|
|
|
|
// NewRenderer returns the default Renderer implementation using Go text/template.
|
|
func NewRenderer() Renderer {
|
|
return renderer{}
|
|
}
|
|
|
|
func (r renderer) Render(profile Profile, enrollment Config, bindings []BindingSnapshot) ([]byte, error) {
|
|
rctx := buildRenderContext(profile, enrollment, bindings)
|
|
|
|
switch profile.ContentFormat {
|
|
case ContentFormatRaw:
|
|
return []byte(profile.ContentTemplate), nil
|
|
case ContentFormatGoTemplate, ContentFormatJSON, ContentFormatYAML, ContentFormatTOML, "":
|
|
return r.renderTemplate(profile, rctx)
|
|
default:
|
|
return nil, fmt.Errorf("%w: unsupported template format %q", ErrRenderFailed, profile.ContentFormat)
|
|
}
|
|
}
|
|
|
|
func (r renderer) renderTemplate(profile Profile, rctx RenderContext) ([]byte, error) {
|
|
t, err := template.New("bootstrap").
|
|
Option("missingkey=error").
|
|
Funcs(allowlistedFuncs()).
|
|
Parse(profile.ContentTemplate)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: %w", ErrRenderFailed, err)
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
if err := t.Execute(&buf, rctx); err != nil {
|
|
return nil, fmt.Errorf("%w: %w", ErrRenderFailed, err)
|
|
}
|
|
|
|
return convertOutput(buf.Bytes(), profile.ContentFormat)
|
|
}
|
|
|
|
// convertOutput parses the rendered bytes as any structured format (JSON, YAML,
|
|
// or TOML) and re-marshals them into the declared target format. For go-template
|
|
// or empty format the raw bytes are returned unchanged.
|
|
func convertOutput(out []byte, format ContentFormat) ([]byte, error) {
|
|
switch format {
|
|
case ContentFormatGoTemplate, "":
|
|
return out, nil
|
|
case ContentFormatJSON, ContentFormatYAML, ContentFormatTOML:
|
|
var v any
|
|
if err := parseStructured(out, &v); err != nil {
|
|
return nil, fmt.Errorf("%w: %w", ErrRenderFailed, err)
|
|
}
|
|
result, err := marshalAs(v, format)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: %w", ErrRenderFailed, err)
|
|
}
|
|
return result, nil
|
|
default:
|
|
return nil, fmt.Errorf("%w: unsupported format %q", ErrRenderFailed, format)
|
|
}
|
|
}
|
|
|
|
// parseStructured tries JSON, then YAML, then TOML and unmarshals into v.
|
|
func parseStructured(out []byte, v any) error {
|
|
if err := json.Unmarshal(out, v); err == nil {
|
|
return nil
|
|
}
|
|
if err := yaml.Unmarshal(out, v); err == nil {
|
|
return nil
|
|
}
|
|
if err := toml.Unmarshal(out, v); err == nil {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("template output is not valid JSON, YAML, or TOML")
|
|
}
|
|
|
|
// marshalAs re-marshals v into the requested format.
|
|
func marshalAs(v any, format ContentFormat) ([]byte, error) {
|
|
switch format {
|
|
case ContentFormatJSON:
|
|
return json.MarshalIndent(v, "", " ")
|
|
case ContentFormatYAML:
|
|
return yaml.Marshal(v)
|
|
case ContentFormatTOML:
|
|
var buf bytes.Buffer
|
|
if err := toml.NewEncoder(&buf).Encode(v); err != nil {
|
|
return nil, err
|
|
}
|
|
return buf.Bytes(), nil
|
|
default:
|
|
return nil, fmt.Errorf("unsupported format %q", format)
|
|
}
|
|
}
|
|
|
|
// buildRenderContext constructs the typed RenderContext from stored data.
|
|
// No external calls are made here.
|
|
func buildRenderContext(profile Profile, enrollment Config, bindings []BindingSnapshot) RenderContext {
|
|
vars := make(map[string]any)
|
|
for k, v := range profile.Defaults {
|
|
vars[k] = v
|
|
}
|
|
for k, v := range enrollment.RenderContext {
|
|
vars[k] = v
|
|
}
|
|
|
|
bctx := make(map[string]BindingContext, len(bindings))
|
|
for _, b := range bindings {
|
|
bctx[b.Slot] = BindingContext{
|
|
Type: b.Type,
|
|
ID: b.ResourceID,
|
|
Snapshot: b.Snapshot,
|
|
Secret: b.SecretSnapshot,
|
|
}
|
|
}
|
|
|
|
return RenderContext{
|
|
Device: DeviceContext{
|
|
ID: enrollment.ID,
|
|
ExternalID: enrollment.ExternalID,
|
|
DomainID: enrollment.DomainID,
|
|
},
|
|
Vars: vars,
|
|
Bindings: bctx,
|
|
}
|
|
}
|
|
|
|
// allowlistedFuncs returns the safe set of template helper functions.
|
|
// No function in this map may call an external service or perform I/O.
|
|
func allowlistedFuncs() template.FuncMap {
|
|
return template.FuncMap{
|
|
"toJSON": func(v any) (string, error) {
|
|
b, err := json.Marshal(v)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(b), nil
|
|
},
|
|
"default": func(def, val any) any {
|
|
if val == nil || val == "" {
|
|
return def
|
|
}
|
|
return val
|
|
},
|
|
"required": func(key string, val any) (any, error) {
|
|
if val == nil || val == "" {
|
|
return nil, fmt.Errorf("required value %q is missing", key)
|
|
}
|
|
return val, nil
|
|
},
|
|
}
|
|
}
|