internal/doc/doc.go (200 lines of code) (raw):

// Package doc contains the types and methods for generating documentation from an Alzlib library member. package doc import ( "context" "fmt" "io" "slices" "strings" "github.com/Azure/alzlib" "github.com/nao1215/markdown" ) func AlzlibReadmeMd(ctx context.Context, w io.Writer, libs ...alzlib.LibraryReference) error { az := alzlib.NewAlzLib(nil) if err := az.Init(ctx, libs...); err != nil { return fmt.Errorf("doc.AlzlibReadmeMd: failed to initialize alzlib: %w", err) } metadataS := az.Metadata() metad := metadataS[len(metadataS)-1] md := markdown.NewMarkdown(w) md = alzlibReadmeMdTitle(md, metad) md = alzlibReadmeMdDependencies(md, metad.Dependencies()) md = alzlibReadmeMdUsage(md, metad.Path()) md = alzlibReadmeMdArchitectures(md, az) md = alzlibReadmeMdArchetypes(md, az) md = alzlibReadmeMdPolicyDefaultValues(md, az) md = md.HorizontalRule() md = alzlibReadmeMdContents(md, az) return md.Build() } func alzlibReadmeMdTitle(md *markdown.Markdown, metad *alzlib.Metadata) *markdown.Markdown { name := metad.Name() if name == "" { name = "No name in metadata" } displayName := metad.DisplayName() if displayName == "" { displayName = "No display name in metadata" } description := metad.Description() if description == "" { description = "No description in metadata" } return md.H1f("%s (%s)", name, displayName).LF(). PlainText(description).LF() } func alzlibReadmeMdDependencies(md *markdown.Markdown, deps alzlib.LibraryReferences) *markdown.Markdown { if len(deps) == 0 { return md } md = md.H2("Dependencies").LF() for _, dep := range deps { md = md.BulletList(dep.String()) } return md.LF() } func alzlibReadmeMdUsage(md *markdown.Markdown, path string) *markdown.Markdown { return md.H2("Usage").LF(). CodeBlocks(markdown.SyntaxHighlight("terraform"), fmt.Sprintf(`provider "alz" { library_references = [ { path = "%s" tag = "0000.00.0" # Replace with the desired version } ] }`, path)).LF() } func alzlibReadmeMdArchitectures(md *markdown.Markdown, az *alzlib.AlzLib) *markdown.Markdown { archs := az.Architectures() if len(archs) == 0 { return md } md = md.H2("Architectures").LF(). PlainText("The following architectures are available in this library, please note that the diagrams denote the management group display name and, in brackets, the associated archetypes:").LF() for _, a := range archs { md = alzlibReadmeMdArchitecture(md, az.Architecture(a)) } return md } func alzlibReadmeMdArchetypes(md *markdown.Markdown, az *alzlib.AlzLib) *markdown.Markdown { archetypes := az.Archetypes() if len(archetypes) == 0 { return md } md = md.H2("Archetypes").LF() for _, a := range archetypes { archetype := az.Archetype(a) pds := archetype.PolicyDefinitions.ToSlice() slices.Sort(pds) psds := archetype.PolicySetDefinitions.ToSlice() slices.Sort(psds) rds := archetype.RoleDefinitions.ToSlice() slices.Sort(rds) pas := archetype.PolicyAssignments.ToSlice() slices.Sort(pas) if len(pds) > 0 || len(psds) > 0 || len(pas) > 0 || len(rds) > 0 { md = md.H3("archetype `" + archetype.Name() + "`").LF() if len(pds) > 0 { md.H4(a+" policy definitions").LF(). Details(fmt.Sprintf("%d policy definitions", archetype.PolicyDefinitions.Cardinality()), "\n- "+strings.Join(pds, "\n- ")).LF() } if len(psds) > 0 { md = md.H4(a+" policy set definitions").LF(). Details(fmt.Sprintf("%d policy set definitions", archetype.PolicySetDefinitions.Cardinality()), "\n- "+strings.Join(psds, "\n- ")).LF() } if len(pas) > 0 { md = md.H4(a+" policy assignments").LF(). Details(fmt.Sprintf("%d policy assignments", archetype.PolicyAssignments.Cardinality()), "\n- "+strings.Join(pas, "\n- ")).LF() } if len(rds) > 0 { md = md.H4(a+" role definitions").LF(). Details(fmt.Sprintf("%d role definitions", archetype.RoleDefinitions.Cardinality()), "\n- "+strings.Join(rds, "\n- ")).LF() } } } return md } func alzlibReadmeMdContents(md *markdown.Markdown, az *alzlib.AlzLib) *markdown.Markdown { md = md.H2("Contents").LF() if len(az.PolicyDefinitions()) > 0 { md = md.H3("all policy definitions").LF(). Details(fmt.Sprintf("%d policy definitions", len(az.PolicyDefinitions())), "\n- "+strings.Join(az.PolicyDefinitions(), "\n- ")).LF() } if len(az.PolicySetDefinitions()) > 0 { md = md.H3("all policy set definitions").LF(). Details(fmt.Sprintf("%d policy set definitions", len(az.PolicySetDefinitions())), "\n- "+strings.Join(az.PolicySetDefinitions(), "\n- ")).LF() } if len(az.PolicyAssignments()) > 0 { md = md.H3("all policy assignments").LF(). Details(fmt.Sprintf("%d policy assignments", len(az.PolicyAssignments())), "\n- "+strings.Join(az.PolicyAssignments(), "\n- ")).LF() } if len(az.RoleDefinitions()) > 0 { md = md.H3("all role definitions").LF(). Details(fmt.Sprintf("%d role definitions", len(az.RoleDefinitions())), "\n- "+strings.Join(az.RoleDefinitions(), "\n- ")).LF() } return md } func alzlibReadmeMdArchitecture(md *markdown.Markdown, a *alzlib.Architecture) *markdown.Markdown { return md.H3("architecture `"+a.Name()+"`").LF(). Note("This hierarchy will be deployed as a child of the user-supplied root management group.").LF(). CodeBlocks("mermaid", mermaidFromArchitecture(a)).LF() } func mermaidFromArchitecture(a *alzlib.Architecture) string { sb := strings.Builder{} sb.WriteString("flowchart TD\n") rootMgs := a.RootMgs() slices.SortFunc(rootMgs, sortFuncArchitectureManagementGroup) for _, mg := range rootMgs { mermaidFromArchitectureRecursion(&sb, mg) } return sb.String() } func mermaidFromArchitectureRecursion(sb *strings.Builder, mg *alzlib.ArchitectureManagementGroup) { archs := mg.Archetypes() archetypes := make([]string, len(archs)) for i, a := range archs { archetypes[i] = a.Name() } archetypesStr := strings.Join(archetypes, ", ") fmtStr := ` %s["%s (%s)"] ` sb.WriteString(fmt.Sprintf(fmtStr, mg.Id(), mg.DisplayName(), archetypesStr)) children := mg.Children() slices.SortFunc(children, sortFuncArchitectureManagementGroup) for _, child := range children { sb.WriteString(fmt.Sprintf(" %s --> %s\n", mg.Id(), child.Id())) mermaidFromArchitectureRecursion(sb, child) } } // sortFuncArchitectureManagementGroup is a sort function for alzlib.ArchitectureManagementGroup for use in slices.SortFunc. func sortFuncArchitectureManagementGroup(a, b *alzlib.ArchitectureManagementGroup) int { if a.Id() < b.Id() { return -1 } if a.Id() > b.Id() { return 1 } return 0 } // func metadataDependenciesToAlzlibProviderLibRefs(deps alzlib.LibraryReferences) string { // sb := strings.Builder{} // if len(deps) == 0 { // return sb.String() // } // for _, dep := range deps { // switch d := dep.(type) { // case *alzlib.AlzLibraryReference: // sb.WriteString("\n {\n") // sb.WriteString(fmt.Sprintf(" path = \"%s\"\n", d.Path())) // sb.WriteString(fmt.Sprintf(" ref = \"%s\"\n", d.Ref())) // sb.WriteString(" }\n") // case *alzlib.CustomLibraryReference: // sb.WriteString("\n {\n") // sb.WriteString(fmt.Sprintf(" custom_url = \"%s\"\n", d.String())) // sb.WriteString(" }\n") // } // } // return sb.String() // } func alzlibReadmeMdPolicyDefaultValues(md *markdown.Markdown, az *alzlib.AlzLib) *markdown.Markdown { pdvs := az.PolicyDefaultValues() if len(pdvs) == 0 { return md } md = md.H2("Policy Default Values").LF().PlainText("The following policy default values are available in this library:").LF() for _, pdv := range pdvs { md = md.H3("default name `" + pdv + "`").LF() desc := az.PolicyDefaultValue(pdv).Description() if desc != "" { md = md.PlainText(desc).LF() } t := markdown.TableSet{ Header: []string{"Assignment", "Parameter Names"}, Rows: [][]string{}, } for _, assignment := range az.PolicyDefaultValue(pdv).Assignments() { t.Rows = append(t.Rows, []string{assignment, strings.Join(az.PolicyDefaultValue(pdv).AssignmentParameters(assignment), ", ")}) } md = md.Table(t).LF() } return md }