Skip to content

Files

Latest commit

07ffe0b · Jul 25, 2025

History

History

internal

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
Jun 23, 2025
May 7, 2025
Jul 25, 2025
Feb 9, 2024
Aug 29, 2024
Jul 9, 2025
Jul 18, 2025
Jul 25, 2025
Apr 24, 2024
Jul 25, 2025
Feb 19, 2025
May 16, 2025
Jul 9, 2025
Sep 7, 2022
May 16, 2025
Apr 10, 2025
Jul 22, 2025
Jul 25, 2025
Mar 20, 2024
Aug 30, 2023
Jul 25, 2025
Jul 25, 2025
Mar 20, 2024
Jan 21, 2025
Jul 19, 2022
Mar 5, 2025
Oct 27, 2020
Jul 9, 2025
Jul 4, 2025
Oct 19, 2023
Jun 22, 2023
May 7, 2025
Nov 24, 2023
Jul 25, 2025
Jun 3, 2024
Jul 23, 2025
Mar 20, 2024
Jul 25, 2025
Nov 24, 2023
Jul 25, 2025
Oct 6, 2023
Mar 20, 2024
Mar 20, 2024
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.

package docs

import (
	"bytes"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"text/template"

	"github.com/pmezard/go-difflib/difflib"

	"github.com/elastic/elastic-package/internal/builder"
	"github.com/elastic/elastic-package/internal/logger"
	"github.com/elastic/elastic-package/internal/packages"
)

// ReadmeFile contains file name and status of each readme file.
type ReadmeFile struct {
	FileName string
	UpToDate bool
	Diff     string
	Error    error
}

// AreReadmesUpToDate function checks if all the .md readme files are up-to-date.
func AreReadmesUpToDate() ([]ReadmeFile, error) {
	packageRoot, err := packages.MustFindPackageRoot()
	if err != nil {
		return nil, fmt.Errorf("package root not found: %w", err)
	}

	files, err := filepath.Glob(filepath.Join(packageRoot, "_dev", "build", "docs", "*.md"))
	if err != nil && !errors.Is(err, os.ErrNotExist) {
		return nil, fmt.Errorf("reading directory entries failed: %w", err)
	}

	var readmeFiles []ReadmeFile
	for _, filePath := range files {
		fileName := filepath.Base(filePath)
		ok, diff, err := isReadmeUpToDate(fileName, packageRoot)
		if !ok || err != nil {
			readmeFile := ReadmeFile{
				FileName: fileName,
				UpToDate: ok,
				Diff:     diff,
				Error:    err,
			}
			readmeFiles = append(readmeFiles, readmeFile)
		}
	}

	if readmeFiles != nil {
		return readmeFiles, fmt.Errorf("files do not match")
	}
	return readmeFiles, nil
}

func isReadmeUpToDate(fileName, packageRoot string) (bool, string, error) {
	logger.Debugf("Check if %s is up-to-date", fileName)

	rendered, shouldBeRendered, err := generateReadme(fileName, packageRoot)
	if err != nil {
		return false, "", fmt.Errorf("generating readme file failed: %w", err)
	}
	if !shouldBeRendered {
		return true, "", nil // README file is static and doesn't use template.
	}

	existing, found, err := readReadme(fileName, packageRoot)
	if err != nil {
		return false, "", fmt.Errorf("reading README file failed: %w", err)
	}
	if !found {
		return false, "", nil
	}
	if bytes.Equal(existing, rendered) {
		return true, "", nil
	}
	var buf bytes.Buffer
	err = difflib.WriteUnifiedDiff(&buf, difflib.UnifiedDiff{
		A:        difflib.SplitLines(string(existing)),
		B:        difflib.SplitLines(string(rendered)),
		FromFile: "want",
		ToFile:   "got",
		Context:  1,
	})
	return false, buf.String(), err
}

// UpdateReadmes function updates all .md readme files using a defined template
// files. The function doesn't perform any action if the template file is not present.
func UpdateReadmes(packageRoot string) ([]string, error) {
	readmeFiles, err := filepath.Glob(filepath.Join(packageRoot, "_dev", "build", "docs", "*.md"))
	if err != nil {
		return nil, fmt.Errorf("reading directory entries failed: %w", err)
	}

	var targets []string
	for _, filePath := range readmeFiles {
		fileName := filepath.Base(filePath)
		target, err := updateReadme(fileName, packageRoot)
		if err != nil {
			return nil, fmt.Errorf("updating readme file %s failed: %w", fileName, err)
		}

		if target != "" {
			targets = append(targets, target)
		}
	}
	return targets, nil
}

func updateReadme(fileName, packageRoot string) (string, error) {
	logger.Debugf("Update the %s file", fileName)

	rendered, shouldBeRendered, err := generateReadme(fileName, packageRoot)
	if err != nil {
		return "", err
	}
	if !shouldBeRendered {
		return "", nil
	}

	target, err := writeReadme(fileName, packageRoot, rendered)
	if err != nil {
		return "", fmt.Errorf("writing %s file failed: %w", fileName, err)
	}

	packageBuildRoot, err := builder.BuildPackagesDirectory(packageRoot)
	if err != nil {
		return "", fmt.Errorf("package build root not found: %w", err)
	}

	_, err = writeReadme(fileName, packageBuildRoot, rendered)
	if err != nil {
		return "", fmt.Errorf("writing %s file failed: %w", fileName, err)
	}
	return target, nil
}

func generateReadme(fileName, packageRoot string) ([]byte, bool, error) {
	logger.Debugf("Generate %s file (package: %s)", fileName, packageRoot)
	templatePath, found, err := findReadmeTemplatePath(fileName, packageRoot)
	if err != nil {
		return nil, false, fmt.Errorf("can't locate %s template file: %w", fileName, err)
	}
	if !found {
		logger.Debug("README file is static, can't be generated from the template file")
		return nil, false, nil
	}
	logger.Debugf("Template file for %s found: %s", fileName, templatePath)

	linksMap, err := readLinksMap()
	if err != nil {
		return nil, false, err
	}

	rendered, err := renderReadme(fileName, packageRoot, templatePath, linksMap)
	if err != nil {
		return nil, true, fmt.Errorf("rendering Readme failed: %w", err)
	}
	return rendered, true, nil
}

func findReadmeTemplatePath(fileName, packageRoot string) (string, bool, error) {
	templatePath := filepath.Join(packageRoot, "_dev", "build", "docs", fileName)
	_, err := os.Stat(templatePath)
	if err != nil && errors.Is(err, os.ErrNotExist) {
		return "", false, nil // README.md file not found
	}
	if err != nil {
		return "", false, fmt.Errorf("can't stat the %s file: %w", fileName, err)
	}
	return templatePath, true, nil
}

func renderReadme(fileName, packageRoot, templatePath string, linksMap linkMap) ([]byte, error) {
	logger.Debugf("Render %s file (package: %s, templatePath: %s)", fileName, packageRoot, templatePath)

	t := template.New(fileName)
	t, err := t.Funcs(template.FuncMap{
		"event": func(args ...string) (string, error) {
			if len(args) > 0 {
				return renderSampleEvent(packageRoot, args[0])
			}
			return renderSampleEvent(packageRoot, "")
		},
		"fields": func(args ...string) (string, error) {
			if len(args) > 0 {
				dataStreamPath := filepath.Join(packageRoot, "data_stream", args[0])
				return renderExportedFields(dataStreamPath)
			}
			return renderExportedFields(packageRoot)
		},
		"url": func(args ...string) (string, error) {
			options := linkOptions{}
			if len(args) > 1 {
				options.caption = args[1]
			}
			return linksMap.RenderLink(args[0], options)
		},
	}).ParseFiles(templatePath)
	if err != nil {
		return nil, fmt.Errorf("parsing README template failed (path: %s): %w", templatePath, err)
	}

	var rendered bytes.Buffer
	err = t.Execute(&rendered, nil)
	if err != nil {
		return nil, fmt.Errorf("executing template failed: %w", err)
	}
	return rendered.Bytes(), nil
}

func readReadme(fileName, packageRoot string) ([]byte, bool, error) {
	logger.Debugf("Read existing %s file (package: %s)", fileName, packageRoot)

	readmePath := filepath.Join(packageRoot, "docs", fileName)
	b, err := os.ReadFile(readmePath)
	if err != nil && errors.Is(err, os.ErrNotExist) {
		return nil, false, nil
	}
	if err != nil {
		return nil, false, fmt.Errorf("readfile failed (path: %s): %w", readmePath, err)
	}
	return b, true, err
}

func writeReadme(fileName, packageRoot string, content []byte) (string, error) {
	logger.Debugf("Write %s file (package: %s)", fileName, packageRoot)

	docsPath := docsPath(packageRoot)
	logger.Debugf("Create directories: %s", docsPath)
	err := os.MkdirAll(docsPath, 0o755)
	if err != nil {
		return "", fmt.Errorf("mkdir failed (path: %s): %w", docsPath, err)
	}

	aReadmePath := readmePath(fileName, packageRoot)
	logger.Debugf("Write %s file to: %s", fileName, aReadmePath)

	err = os.WriteFile(aReadmePath, content, 0o644)
	if err != nil {
		return "", fmt.Errorf("writing file failed (path: %s): %w", aReadmePath, err)
	}
	return aReadmePath, nil
}

func readmePath(fileName, packageRoot string) string {
	return filepath.Join(docsPath(packageRoot), fileName)
}

func docsPath(packageRoot string) string {
	return filepath.Join(packageRoot, "docs")
}