-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathdocs.go
More file actions
344 lines (302 loc) · 11.7 KB
/
docs.go
File metadata and controls
344 lines (302 loc) · 11.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"strings"
"unicode"
"github.com/spf13/cobra"
"github.com/spf13/cobra/doc"
)
const docsShortDesc = `Generate documentation files for Kosli CLI. `
const docsLongDesc = docsShortDesc + `
This command can generate documentation in the following formats: Markdown.
`
type docsOptions struct {
dest string
topCmd *cobra.Command
generateHeaders bool
}
func newDocsCmd(out io.Writer) *cobra.Command {
o := &docsOptions{}
cmd := &cobra.Command{
Use: "docs",
Short: docsShortDesc,
Long: docsLongDesc,
Hidden: true,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
o.topCmd = cmd.Root()
return o.run()
},
}
f := cmd.Flags()
f.StringVar(&o.dest, "dir", "./", "The directory to which documentation is written.")
f.BoolVar(&o.generateHeaders, "generate-headers", true, "Generate standard headers for markdown files.")
return cmd
}
func (o *docsOptions) run() error {
if o.generateHeaders {
linkHandler := func(name string) string {
base := strings.TrimSuffix(name, path.Ext(name))
return "/client_reference/" + strings.ToLower(base) + "/"
}
hdrFunc := func(filename string, beta, deprecated bool, summary string) string {
base := filepath.Base(filename)
name := strings.TrimSuffix(base, path.Ext(base))
title := strings.ToLower(strings.Replace(name, "_", " ", -1))
return fmt.Sprintf("---\ntitle: \"%s\"\nbeta: %t\ndeprecated: %t\nsummary: \"%s\"\n---\n\n", title, beta, deprecated, summary)
}
return MereklyGenMarkdownTreeCustom(o.topCmd, o.dest, hdrFunc, linkHandler)
}
return doc.GenMarkdownTree(o.topCmd, o.dest)
}
func MereklyGenMarkdownTreeCustom(cmd *cobra.Command, dir string, filePrepender func(string, bool, bool, string) string, linkHandler func(string) string) error {
for _, c := range cmd.Commands() {
// skip all unavailable commands except deprecated ones
if (!c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand()) && c.Deprecated == "" {
continue
}
if err := MereklyGenMarkdownTreeCustom(c, dir, filePrepender, linkHandler); err != nil {
return err
}
}
if !cmd.HasParent() || !cmd.HasSubCommands() {
basename := strings.Replace(cmd.CommandPath(), " ", "_", -1) + ".md"
filename := filepath.Join(dir, basename)
summary := cmd.Short
f, err := os.Create(filename)
if err != nil {
return err
}
defer f.Close()
if _, err := io.WriteString(f, filePrepender(filename, isBeta(cmd), isDeprecated(cmd), summary)); err != nil {
return err
}
if err := KosliGenMarkdownCustom(cmd, f, linkHandler); err != nil {
return err
}
}
return nil
}
// KosliGenMarkdownCustom creates custom markdown output.
func KosliGenMarkdownCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string) string) error {
cmd.InitDefaultHelpCmd()
cmd.InitDefaultHelpFlag()
buf := new(bytes.Buffer)
name := cmd.CommandPath()
buf.WriteString("# " + name + "\n\n")
if isBeta(cmd) {
buf.WriteString("{{% hint warning %}}\n")
buf.WriteString(fmt.Sprintf("**%s** is a beta feature. ", name))
buf.WriteString("Beta features provide early access to product functionality. ")
buf.WriteString("These features may change between releases without warning, or can be removed in a ")
buf.WriteString("future release.\n")
buf.WriteString("Please contact us to enable this feature for your organization.\n")
// buf.WriteString("You can enable beta features by using the `kosli enable beta` command.")
buf.WriteString("{{% /hint %}}\n")
}
if isDeprecated(cmd) {
buf.WriteString("{{% hint danger %}}\n")
buf.WriteString(fmt.Sprintf("**%s** is deprecated. %s ", name, cmd.Deprecated))
buf.WriteString("Deprecated commands will be removed in a future release.\n")
buf.WriteString("{{% /hint %}}\n")
}
if len(cmd.Long) > 0 {
buf.WriteString("## Synopsis\n\n")
buf.WriteString(strings.Replace(cmd.Long, "^", "`", -1) + "\n\n")
}
if cmd.Runnable() {
buf.WriteString(fmt.Sprintf("```shell\n%s\n```\n\n", cmd.UseLine()))
}
if err := printOptions(buf, cmd, name); err != nil {
return err
}
urlSafeName := url.QueryEscape(name)
liveExamplesBuf := new(bytes.Buffer)
for _, ci := range []string{"GitHub", "GitLab"} {
if liveYamlDocExists(ci, urlSafeName) {
liveExamplesBuf.WriteString(fmt.Sprintf("{{< tab \"%v\" >}}", ci))
liveExamplesBuf.WriteString(fmt.Sprintf("View an example of the `%s` command in %s.\n\n", name, ci))
liveExamplesBuf.WriteString(fmt.Sprintf("In [this YAML file](%v)", yamlURL(ci, urlSafeName)))
if liveEventDocExists(ci, urlSafeName) {
liveExamplesBuf.WriteString(fmt.Sprintf(", which created [this Kosli Event](%v).", eventURL(ci, urlSafeName)))
}
liveExamplesBuf.WriteString("{{< /tab >}}")
}
}
liveExamples := liveExamplesBuf.String()
if len(liveExamples) > 0 {
buf.WriteString("## Live Examples in different CI systems\n\n")
buf.WriteString("{{< tabs \"live-examples\" \"col-no-wrap\" >}}")
buf.WriteString(liveExamples)
buf.WriteString("{{< /tabs >}}\n\n")
}
liveCliFullCommand, liveCliURL, liveCliExists := liveCliDocExists(name)
if liveCliExists {
buf.WriteString("## Live Example\n\n")
buf.WriteString("{{< raw-html >}}")
buf.WriteString(fmt.Sprintf("To view a live example of '%s' you can run the commands below (for the <a href=\"https://app.kosli.com/cyber-dojo/environments/aws-prod/snapshots/\">cyber-dojo</a> demo organization).<br/><a href=\"%s\">Run the commands below and view the output.</a>", name, liveCliURL))
buf.WriteString("<pre>")
buf.WriteString("export KOSLI_ORG=cyber-dojo\n")
buf.WriteString("export KOSLI_API_TOKEN=Pj_XT2deaVA6V1qrTlthuaWsmjVt4eaHQwqnwqjRO3A # read-only\n")
buf.WriteString(liveCliFullCommand)
buf.WriteString("</pre>")
buf.WriteString("{{< / raw-html >}}\n\n")
}
if len(cmd.Example) > 0 {
// This is an attempt to tidy up the non-live examples, so they each have their own title.
// Note: The contents of the title lines could also contain < and > characters which will
// be lost if simply embedded in a md ## section.
buf.WriteString("## Examples Use Cases\n\n")
buf.WriteString("These examples all assume that the flags `--api-token`, `--org`, `--host`, (and `--flow`, `--trail` when required), are set/provided. \n\n")
// Some non-title lines contain a # character, (eg in a snappish) so we have to
// split on newlines first and then only split on # in the first position
example := strings.TrimSpace(cmd.Example)
lines := strings.Split(example, "\n")
// Some commands have #titles spanning several lines (that is, each title line starts with a # character)
if name == "kosli report approval" {
buf.WriteString(fmt.Sprintf("```shell\n%s\n```\n\n", example))
} else if name == "kosli request approval" {
buf.WriteString(fmt.Sprintf("```shell\n%s\n```\n\n", example))
} else if name == "kosli snapshot server" {
buf.WriteString(fmt.Sprintf("```shell\n%s\n```\n\n", example))
} else if lines[0][0] != '#' {
// Some commands, eg 'kosli assert snapshot' have no #title
// and their example starts immediately with the kosli command.
buf.WriteString(fmt.Sprintf("```shell\n%s\n```\n\n", example))
} else {
// The rest we can format nicely
all := hashTitledExamples(lines)
for i := 0; i < len(all); i++ {
exampleLines := all[i]
// Some titles have a trailing colon, some don't
title := strings.Trim(exampleLines[0], ":")
if len(title) > 0 {
buf.WriteString(fmt.Sprintf("**%s**\n\n", strings.TrimSpace(title[1:])))
buf.WriteString(fmt.Sprintf("```shell\n%s\n```\n\n", strings.Join(exampleLines[1:], "\n")))
}
}
}
}
_, err := buf.WriteTo(w)
return err
}
func hashTitledExamples(lines []string) [][]string {
// Some non-title lines contain a # character, so we have split on newlines first
// and then split on # which are the first character in their line
result := make([][]string, 0)
example := make([]string, 0)
for _, line := range lines {
if strings.HasPrefix(line, "#") {
result = append(result, example) // See result[1:] at end
example = make([]string, 0)
}
if !isSetWithEnvVar(line) {
example = append(example, choppedLineContinuation(line))
}
}
result = append(result, example)
return result[1:]
}
func isSetWithEnvVar(line string) bool {
trimmed_line := strings.TrimSpace(line)
if strings.HasPrefix(trimmed_line, "--api-token ") {
return true
} else if strings.HasPrefix(trimmed_line, "--host ") {
return true
} else if strings.HasPrefix(trimmed_line, "--org ") {
return true
} else if strings.HasPrefix(trimmed_line, "--flow ") {
return true
} else if strings.HasPrefix(trimmed_line, "--trail ") {
return true
} else {
return false
}
}
func choppedLineContinuation(line string) string {
trimmed_line := strings.TrimRightFunc(line, unicode.IsSpace)
return strings.TrimSuffix(trimmed_line, "\\")
}
func printOptions(buf *bytes.Buffer, cmd *cobra.Command, name string) error {
flags := cmd.NonInheritedFlags()
flags.SetOutput(buf)
if flags.HasAvailableFlags() {
buf.WriteString("## Flags\n")
buf.WriteString("| Flag | Description |\n")
buf.WriteString("| :--- | :--- |\n")
usages := CommandsInTable(flags)
fmt.Fprint(buf, usages)
buf.WriteString("\n\n")
}
parentFlags := cmd.InheritedFlags()
parentFlags.SetOutput(buf)
if parentFlags.HasAvailableFlags() {
buf.WriteString("## Flags inherited from parent commands\n")
buf.WriteString("| Flag | Description |\n")
buf.WriteString("| :--- | :--- |\n")
usages := CommandsInTable(parentFlags)
fmt.Fprint(buf, usages)
buf.WriteString("\n\n")
}
return nil
}
const baseURL = "https://app.kosli.com/api/v2/livedocs/cyber-dojo"
func liveYamlDocExists(ci string, command string) bool {
url := fmt.Sprintf("%v/yaml_exists?ci=%v&command=%v", baseURL, strings.ToLower(ci), command)
return liveDocExists(url)
}
func liveEventDocExists(ci string, command string) bool {
url := fmt.Sprintf("%v/event_exists?ci=%v&command=%v", baseURL, strings.ToLower(ci), command)
return liveDocExists(url)
}
func liveCliDocExists(command string) (string, string, bool) {
fullCommand, ok := liveCliMap[command]
if ok {
plussed := strings.Replace(fullCommand, " ", "+", -1)
exists_url := fmt.Sprintf("%v/cli_exists?command=%v", baseURL, plussed)
url := fmt.Sprintf("%v/cli?command=%v", baseURL, plussed)
return fullCommand, url, liveDocExists(exists_url)
} else {
return "", "", false
}
}
func liveDocExists(url string) bool {
response, err := http.Get(url)
if err != nil {
return false
}
defer response.Body.Close()
decoder := json.NewDecoder(response.Body)
var exists bool
err = decoder.Decode(&exists)
if err != nil {
return false
}
return exists
}
func yamlURL(ci string, command string) string {
return fmt.Sprintf("%v/yaml?ci=%v&command=%v", baseURL, strings.ToLower(ci), command)
}
func eventURL(ci string, command string) string {
return fmt.Sprintf("%v/event?ci=%v&command=%v", baseURL, strings.ToLower(ci), command)
}
var liveCliMap = map[string]string{
"kosli list environments": "kosli list environments --output=json",
"kosli get environment": "kosli get environment aws-prod --output=json",
"kosli log environment": "kosli log environment aws-prod --output=json",
"kosli list snapshots": "kosli list snapshots aws-prod --output=json",
"kosli get snapshot": "kosli get snapshot aws-prod --output=json",
"kosli diff snapshots": "kosli diff snapshots aws-beta aws-prod --output=json",
"kosli list flows": "kosli list flows --output=json",
"kosli get flow": "kosli get flow dashboard-ci --output=json",
"kosli list trails": "kosli list trails dashboard-ci --output=json",
"kosli get trail": "kosli get trail dashboard-ci 1159a6f1193150681b8484545150334e89de6c1c --output=json",
}