Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

doc(reference): Generate documentation for policies #848

Merged
merged 11 commits into from
Nov 16, 2023
24 changes: 23 additions & 1 deletion cmd/admxgen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ func main() {
log.Error(err)
os.Exit(2)
}
if err := installDoc(&rootCmd, viper); err != nil {
log.Error(err)
os.Exit(2)
}

if err := rootCmd.Execute(); err != nil {
log.Error(err)
Expand Down Expand Up @@ -94,7 +98,7 @@ func installAdmx(rootCmd *cobra.Command, viper *viper.Viper) error {
Long: gotext.Get("Collects all intermediary policy definition files in SOURCE directory to create admx and adml templates in DEST, based on CATEGORIES_DEF.yaml."),
Args: cobra.ExactArgs(3),
RunE: func(cmd *cobra.Command, args []string) error {
return admxgen.Generate(args[0], args[1], args[2], *autoDetectReleases, *allowMissingKeys)
return admxgen.GenerateAD(args[0], args[1], args[2], *autoDetectReleases, *allowMissingKeys)
},
}
autoDetectReleases = cmd.Flags().BoolP("auto-detect-releases", "a", false, gotext.Get("override supported releases in categories definition file and will takes all yaml files in SOURCE directory and use the basename as their versions."))
Expand All @@ -107,6 +111,24 @@ func installAdmx(rootCmd *cobra.Command, viper *viper.Viper) error {
return nil
}

func installDoc(rootCmd *cobra.Command, viper *viper.Viper) error {
cmd := &cobra.Command{
Use: "doc CATEGORIES_DEF.YAML SOURCE DEST",
Short: gotext.Get("Create markdown documentation"),
Long: gotext.Get("Collects all intermediary policy definition files in SOURCE directory to create markdown documentation in DEST, based on CATEGORIES_DEF.yaml."),
Args: cobra.ExactArgs(3),
RunE: func(cmd *cobra.Command, args []string) error {
return admxgen.GenerateDoc(args[0], args[1], args[2])
},
}
if err := bindFlags(viper, cmd.Flags()); err != nil {
return errors.New(gotext.Get("can't install command flag bindings: %v", err))
}

rootCmd.AddCommand(cmd)
return nil
}

// bindFlags each cobra flag in a flagset to its associated viper env, ignoring config
// Compare to the viper automated binding, it translates - to _.
func bindFlags(viper *viper.Viper, flags *pflag.FlagSet) (errBind error) {
Expand Down
113 changes: 113 additions & 0 deletions internal/ad/admxgen/admxgen.go
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,119 @@ func (g generator) expandedCategoriesToADMX(expandedCategories []expandedCategor
return nil
}

func expandedCategoriesToMD(expandedCategories []expandedCategory, rootDest string, currentRelPath string) (err error) {
computerDest, userDest := filepath.Join(rootDest, "Computer Policies", currentRelPath), filepath.Join(rootDest, "User Policies", currentRelPath)
// bootstrap first directories
if currentRelPath == "." {
for _, dest := range []string{rootDest, computerDest, userDest} {
if dest != rootDest {
if err := os.RemoveAll(dest); err != nil {
return err
}
}
if err := os.Mkdir(dest, 0700); err != nil {
if dest == rootDest && errors.Is(err, os.ErrExist) {
continue
}
return err
}
}
}

for _, ec := range expandedCategories {
// Directories: create both paths.
computerCategoryDir, userCategoryDir := filepath.Join(computerDest, ec.DisplayName), filepath.Join(userDest, ec.DisplayName)
for _, p := range []string{computerCategoryDir, userCategoryDir} {
if err := os.Mkdir(p, 0700); err != nil {
return err
}
}

// This is a list of policies in the current directory.
for _, p := range ec.Policies {
dest := computerCategoryDir
if p.Class == "User" {
dest = userCategoryDir
}
polDetails := p.ReleasesElements["all"]

input := struct {
Location string
Key string
DisplayName string
ExplainText string
ElementType string
Class string
RangeValuesMin string
RangeValuesMax string
Choices []string
}{
strings.ReplaceAll(
strings.TrimLeft(filepath.Join(dest, filepath.Base(polDetails.DisplayName)), rootDest),
"/", " -> "),
p.Key,
polDetails.DisplayName,
strings.ReplaceAll(
strings.ReplaceAll(
strings.TrimSpace(strings.TrimPrefix(p.ExplainText, "-")),
"[", "`["),
"]", "]`"),
string(polDetails.ElementType),
p.Class,
polDetails.RangeValues.Min,
polDetails.RangeValues.Max,
polDetails.Choices,
}

f, err := os.Create(filepath.Join(dest, filepath.Base(polDetails.Key)) + ".md")
if err != nil {
return fmt.Errorf(i18n.G("can't create md file: %v"), err)
}
defer decorate.LogFuncOnError(f.Close)
t := template.Must(template.New("doc policy").Parse(docPolicyTemplate))
err = t.Execute(f, input)
if err != nil {
return err
}
}

// Expand children categories.
if err := expandedCategoriesToMD(ec.Children, rootDest, filepath.Join(currentRelPath, ec.DisplayName)); err != nil {
return err
}

// Remove any folders that are still empty after the expansion of children categories and policies
// (the leaves will be removed first, and so children empty categories are already purged).
for _, p := range []string{computerCategoryDir, userCategoryDir} {
if err := removeDirIfEmpty(p); err != nil {
return err
}
}
}

// Remove root directories if they are empty
if currentRelPath == "." {
for _, p := range []string{computerDest, userDest, rootDest} {
if err := removeDirIfEmpty(p); err != nil {
return err
}
}
}

return nil
}

func removeDirIfEmpty(p string) error {
children, err := os.ReadDir(p)
if err != nil {
return err
}
if len(children) > 0 {
return nil
}
return os.Remove(p)
}

func (g generator) collectCategoriesPolicies(category expandedCategory, parent string) ([]categoryForADMX, []policyForADMX) {
if parent == "" {
parent = category.Parent
Expand Down
50 changes: 48 additions & 2 deletions internal/ad/admxgen/admxgen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ func TestExpand(t *testing.T) {
}
}

func TestGenerate(t *testing.T) {
func TestGenerateAD(t *testing.T) {
t.Parallel()

tests := map[string]struct {
Expand Down Expand Up @@ -119,7 +119,7 @@ func TestGenerate(t *testing.T) {
require.NoError(t, err, "Setup: should create a file as destination")
}

err := admxgen.Generate(catDef, src, dst, tc.autoDetectReleases, false)
err := admxgen.GenerateAD(catDef, src, dst, tc.autoDetectReleases, false)
if tc.wantErr {
require.Error(t, err, "admx should have errored out")
return
Expand All @@ -142,3 +142,49 @@ func TestGenerate(t *testing.T) {
})
}
}

func TestGenerateDoc(t *testing.T) {
t.Parallel()

tests := map[string]struct {
autoDetectReleases bool
destIsFile bool

wantErr bool
}{
"releases from yaml": {},
"autodetect overrides releases from yaml": {autoDetectReleases: true},

// Error cases
"invalid definition file": {wantErr: true},
"category expansion fails": {wantErr: true},
"doc generation fails": {destIsFile: true, wantErr: true},
}
for name, tc := range tests {
name := name

tc := tc
t.Run(name, func(t *testing.T) {
t.Parallel()

catDef := filepath.Join(testutils.TestFamilyPath(t), name+".yaml")
src := filepath.Join(testutils.TestFamilyPath(t), "src")
dst := filepath.Join(t.TempDir(), "subdir")

if tc.destIsFile {
f, err := os.Create(dst)
f.Close()
require.NoError(t, err, "Setup: should create a file as destination")
}

err := admxgen.GenerateDoc(catDef, src, dst)
if tc.wantErr {
require.Error(t, err, "GenerateDoc should have errored out")
return
}
require.NoError(t, err, "GenerateDoc failed but shouldn't have")

testutils.CompareTreesWithFiltering(t, dst, testutils.GoldenPath(t), testutils.Update())
})
}
}
30 changes: 30 additions & 0 deletions internal/ad/admxgen/docpolicy.md.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# {{.DisplayName}}

{{.ExplainText}}

{{ if or (ne .RangeValuesMin "") (ne .RangeValuesMax "") -}}

<span style="font-size: larger;">**Valid range**</span>

* Min: {{.RangeValuesMin}}
* Max: {{.RangeValuesMax}}

{{ end -}}

{{- if ne (len .Choices) 0 -}}

<span style="font-size: larger;">**Valid values**</span>

{{ range $i, $c := .Choices -}}
* {{$c}}
{{ end -}}
{{- end }}

<span style="font-size: larger;">**Metadata**</span>

| Element | Value |
| --- | --- |
| Location | {{.Location}} |
| Registry Key | {{.Key}} |
| Element type | {{.ElementType}} |
| Class: | {{.Class}} |
45 changes: 43 additions & 2 deletions internal/ad/admxgen/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ var admxTemplate string
//go:embed adml.template
var admlTemplate string

//go:embed docpolicy.md.template
var docPolicyTemplate string

// Expand will expand any policies on the system into a list of expanded policies.
func Expand(src, dst, root, currentSession string) error {
release, err := adcommon.GetVersionID(root)
Expand Down Expand Up @@ -116,8 +119,8 @@ type categoryFileStruct struct {
Categories []category
}

// Generate creates and merge all policies into ADMX/ADML files.
func Generate(categoryDefinition, src, dst string, autoDetectReleases, allowMissingKeys bool) error {
// GenerateAD creates and merge all policies into ADMX/ADML files.
func GenerateAD(categoryDefinition, src, dst string, autoDetectReleases, allowMissingKeys bool) error {
// Load all expanded categories
policies, catfs, err := loadDefinitions(categoryDefinition, src)
if err != nil {
Expand Down Expand Up @@ -156,6 +159,44 @@ func Generate(categoryDefinition, src, dst string, autoDetectReleases, allowMiss
return nil
}

// GenerateDoc creates and merge all policies into documentation files.
func GenerateDoc(categoryDefinition, src, dst string) error {
// Load all expanded categories
policies, catfs, err := loadDefinitions(categoryDefinition, src)
if err != nil {
return err
}

// Collect supported releases
var supportedReleases []string
files, err := os.ReadDir(src)
if err != nil {
return fmt.Errorf("can't read source directory: %w", err)
}
for _, f := range files {
if !strings.HasSuffix(f.Name(), ".yaml") {
continue
}
n := strings.TrimSuffix(f.Name(), ".yaml")
supportedReleases = append(supportedReleases, n)
}

g := generator{
distroID: catfs.DistroID,
supportedReleases: supportedReleases,
}
ec, err := g.generateExpandedCategories(catfs.Categories, policies, false)
if err != nil {
return err
}
err = expandedCategoriesToMD(ec, dst, ".")
if err != nil {
return err
}

return nil
}

func loadDefinitions(categoryDefinition, src string) (ep []common.ExpandedPolicy, cfs categoryFileStruct, err error) {
defer decorate.OnError(&err, i18n.G("can't load category definition"))

Expand Down
Loading