2015-07-01 15:35:44 +00:00
/ *
Copyright 2015 The Kubernetes Authors All rights reserved .
Licensed under the Apache License , Version 2.0 ( the "License" ) ;
you may not use this file except in compliance with the License .
You may obtain a copy of the License at
http : //www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing , software
distributed under the License is distributed on an "AS IS" BASIS ,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
See the License for the specific language governing permissions and
limitations under the License .
* /
package main
import (
"errors"
"fmt"
"io/ioutil"
"os"
2015-12-14 18:37:00 +00:00
"os/exec"
2015-07-11 15:07:26 +00:00
"path"
2015-07-01 15:35:44 +00:00
"path/filepath"
"strings"
flag "github.com/spf13/pflag"
)
2015-12-14 18:37:00 +00:00
// This needs to be updated when we cut a new release series.
const latestReleaseBranch = "release-1.1"
2015-07-01 15:35:44 +00:00
var (
2015-11-05 22:16:26 +00:00
verbose = flag . Bool ( "verbose" , false , "On verification failure, emit pre-munge and post-munge versions." )
2015-07-20 16:45:58 +00:00
verify = flag . Bool ( "verify" , false , "Exit with status 1 if files would have needed changes but do not change." )
rootDir = flag . String ( "root-dir" , "" , "Root directory containing documents to be processed." )
// "repo-root" seems like a dumb name, this is the relative path (from rootDir) to get to the repoRoot
relRoot = flag . String ( "repo-root" , ".." , ` Appended to -- root - dir to get the repository root .
2015-07-10 19:29:40 +00:00
It ' s done this way so that generally you just have to set -- root - dir .
Examples :
* -- root - dir = docs / -- repo - root = . . means the repository root is . /
* -- root - dir = / usr / local / long / path / repo / docs / -- repo - root = . . means the repository root is / usr / local / long / path / repo /
* -- root - dir = / usr / local / long / path / repo / docs / admin -- repo - root = . . / . . means the repository root is / usr / local / long / path / repo / ` )
skipMunges = flag . String ( "skip-munges" , "" , "Comma-separated list of munges to *not* run. Available munges are: " + availableMungeList )
2015-07-20 16:45:58 +00:00
repoRoot string
2015-07-01 15:35:44 +00:00
ErrChangesNeeded = errors . New ( "mungedocs: changes required" )
2015-07-09 23:52:03 +00:00
2015-12-14 18:37:00 +00:00
// This records the files in the rootDir in upstream/latest-release
filesInLatestRelease string
// This indicates if the munger is running inside Jenkins
inJenkins bool
2015-07-10 21:35:47 +00:00
// All of the munge operations to perform.
2015-07-09 23:52:03 +00:00
// TODO: allow selection from command line. (e.g., just check links in the examples directory.)
2015-07-10 21:35:47 +00:00
allMunges = [ ] munge {
2015-09-25 20:56:50 +00:00
// Simple "check something" functions must run first.
{ "preformat-balance" , checkPreformatBalance } ,
// Functions which modify state.
2015-07-24 21:51:48 +00:00
{ "remove-whitespace" , updateWhitespace } ,
2015-07-10 21:35:47 +00:00
{ "table-of-contents" , updateTOC } ,
2015-07-20 22:33:40 +00:00
{ "unversioned-warning" , updateUnversionedWarning } ,
2015-07-24 21:51:48 +00:00
{ "md-links" , updateLinks } ,
{ "blank-lines-surround-preformatted" , updatePreformatted } ,
{ "header-lines" , updateHeaderLines } ,
2015-07-20 16:45:58 +00:00
{ "analytics" , updateAnalytics } ,
2015-07-24 21:51:48 +00:00
{ "kubectl-dash-f" , updateKubectlFileTargets } ,
2015-07-18 00:35:25 +00:00
{ "sync-examples" , syncExamples } ,
2015-07-09 23:52:03 +00:00
}
2015-07-10 19:29:40 +00:00
availableMungeList = func ( ) string {
names := [ ] string { }
for _ , m := range allMunges {
names = append ( names , m . name )
}
return strings . Join ( names , "," )
} ( )
2015-07-01 15:35:44 +00:00
)
2015-07-10 21:35:47 +00:00
// a munge processes a document, returning an updated document xor an error.
// The fn is NOT allowed to mutate 'before', if changes are needed it must copy
// data into a new byte array and return that.
type munge struct {
name string
2015-07-20 16:45:58 +00:00
fn func ( filePath string , mlines mungeLines ) ( after mungeLines , err error )
2015-07-10 21:35:47 +00:00
}
2015-07-01 15:35:44 +00:00
2015-07-09 23:52:03 +00:00
type fileProcessor struct {
// Which munge functions should we call?
2015-07-10 21:35:47 +00:00
munges [ ] munge
2015-07-09 23:52:03 +00:00
// Are we allowed to make changes?
verifyOnly bool
2015-07-01 15:35:44 +00:00
}
// Either change a file or verify that it needs no changes (according to modify argument)
2015-07-10 21:35:47 +00:00
func ( f fileProcessor ) visit ( path string ) error {
2015-07-01 15:35:44 +00:00
if ! strings . HasSuffix ( path , ".md" ) {
return nil
}
2015-07-09 23:52:03 +00:00
fileBytes , err := ioutil . ReadFile ( path )
2015-07-01 15:35:44 +00:00
if err != nil {
return err
}
2015-07-20 16:45:58 +00:00
mungeLines := getMungeLines ( string ( fileBytes ) )
2015-07-09 23:52:03 +00:00
modificationsMade := false
2015-07-10 21:35:47 +00:00
errFound := false
filePrinted := false
2015-07-09 23:52:03 +00:00
for _ , munge := range f . munges {
2015-07-20 16:45:58 +00:00
after , err := munge . fn ( path , mungeLines )
if err != nil || ! after . Equal ( mungeLines ) {
2015-07-10 21:35:47 +00:00
if ! filePrinted {
fmt . Printf ( "%s\n----\n" , path )
filePrinted = true
}
fmt . Printf ( "%s:\n" , munge . name )
2015-11-05 22:16:26 +00:00
if * verbose {
fmt . Printf ( "INPUT: <<<%v>>>\n" , mungeLines )
fmt . Printf ( "MUNGED: <<<%v>>>\n" , after )
}
2015-07-10 21:35:47 +00:00
if err != nil {
fmt . Println ( err )
errFound = true
} else {
fmt . Println ( "contents were modified" )
2015-07-09 23:52:03 +00:00
modificationsMade = true
}
2015-07-10 21:35:47 +00:00
fmt . Println ( "" )
2015-07-01 15:35:44 +00:00
}
2015-07-20 16:45:58 +00:00
mungeLines = after
2015-07-01 15:35:44 +00:00
}
2015-07-09 23:52:03 +00:00
// Write out new file with any changes.
if modificationsMade {
2015-07-10 21:35:47 +00:00
if f . verifyOnly {
// We're not allowed to make changes.
return ErrChangesNeeded
}
2015-07-20 16:45:58 +00:00
ioutil . WriteFile ( path , mungeLines . Bytes ( ) , 0644 )
2015-07-09 23:52:03 +00:00
}
2015-07-10 21:35:47 +00:00
if errFound {
return ErrChangesNeeded
}
2015-07-01 15:35:44 +00:00
return nil
}
2015-07-10 21:35:47 +00:00
func newWalkFunc ( fp * fileProcessor , changesNeeded * bool ) filepath . WalkFunc {
return func ( path string , info os . FileInfo , err error ) error {
if err := fp . visit ( path ) ; err != nil {
* changesNeeded = true
if err != ErrChangesNeeded {
return err
}
}
return nil
}
}
2015-07-10 19:29:40 +00:00
func wantedMunges ( ) ( filtered [ ] munge ) {
skipList := strings . Split ( * skipMunges , "," )
skipped := map [ string ] bool { }
for _ , m := range skipList {
if len ( m ) > 0 {
skipped [ m ] = true
}
}
for _ , m := range allMunges {
if ! skipped [ m . name ] {
filtered = append ( filtered , m )
} else {
// Remove from the map so we can verify that everything
// requested was in fact valid.
delete ( skipped , m . name )
}
}
if len ( skipped ) != 0 {
fmt . Fprintf ( os . Stderr , "ERROR: requested to skip %v, but these are not valid munges. (valid: %v)\n" , skipped , availableMungeList )
os . Exit ( 1 )
}
return filtered
}
2015-07-01 15:35:44 +00:00
func main ( ) {
2015-07-20 16:45:58 +00:00
var err error
2015-07-01 15:35:44 +00:00
flag . Parse ( )
if * rootDir == "" {
fmt . Fprintf ( os . Stderr , "usage: %s [--verify] --root-dir <docs root>\n" , flag . Arg ( 0 ) )
os . Exit ( 1 )
}
2015-07-20 16:45:58 +00:00
repoRoot = path . Join ( * rootDir , * relRoot )
repoRoot , err = filepath . Abs ( repoRoot )
if err != nil {
2015-07-11 15:07:26 +00:00
fmt . Fprintf ( os . Stderr , "ERROR: %v\n" , err )
os . Exit ( 2 )
}
2015-12-14 18:37:00 +00:00
absRootDir , err := filepath . Abs ( * rootDir )
if err != nil {
fmt . Fprintf ( os . Stderr , "ERROR: %v\n" , err )
os . Exit ( 2 )
}
inJenkins = len ( os . Getenv ( "JENKINS_HOME" ) ) != 0
out , err := exec . Command ( "git" , "ls-tree" , "-r" , "--name-only" , fmt . Sprintf ( "%s/%s" , "upstream" , latestReleaseBranch ) , absRootDir ) . CombinedOutput ( )
if err != nil {
if inJenkins {
fmt . Fprintf ( os . Stderr , "output: %s,\nERROR: %v\n" , out , err )
os . Exit ( 2 )
} else {
fmt . Fprintf ( os . Stdout , "output: %s,\nERROR: %v\n" , out , err )
fmt . Fprintf ( os . Stdout , "`git ls-tree -r --name-only upstream/%s failed. We'll ignore this error locally, but Jenkins may pick an error. Munger uses the output of this command to determine in unversioned warning, if it should add a link to the doc in release branch.\n" , latestReleaseBranch )
filesInLatestRelease = ""
}
} else {
filesInLatestRelease = string ( out )
}
2015-07-09 23:52:03 +00:00
fp := fileProcessor {
2015-07-10 19:29:40 +00:00
munges : wantedMunges ( ) ,
2015-07-09 23:52:03 +00:00
verifyOnly : * verify ,
}
2015-07-01 15:35:44 +00:00
// For each markdown file under source docs root, process the doc.
2015-07-10 21:35:47 +00:00
// - If any error occurs: exit with failure (exit >1).
// - If verify is true: exit 0 if no changes needed, exit 1 if changes
// needed.
// - If verify is false: exit 0 if changes successfully made or no
2015-07-15 16:49:21 +00:00
// changes needed, exit 1 if manual changes are needed.
2015-07-10 21:35:47 +00:00
var changesNeeded bool
2015-07-20 16:45:58 +00:00
err = filepath . Walk ( * rootDir , newWalkFunc ( & fp , & changesNeeded ) )
2015-07-01 15:35:44 +00:00
if err != nil {
2015-07-10 21:35:47 +00:00
fmt . Fprintf ( os . Stderr , "ERROR: %v\n" , err )
2015-07-01 15:35:44 +00:00
os . Exit ( 2 )
}
2015-07-15 16:49:21 +00:00
if changesNeeded {
if * verify {
fmt . Fprintf ( os . Stderr , "FAIL: changes needed but not made due to --verify\n" )
} else {
fmt . Fprintf ( os . Stderr , "FAIL: some manual changes are still required.\n" )
}
2015-07-10 21:35:47 +00:00
os . Exit ( 1 )
}
2015-07-01 15:35:44 +00:00
}