diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..dd61d54 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,93 @@ +--- +linters-settings: + dupl: + threshold: 100 + funlen: + lines: 100 + statements: 50 + goconst: + min-len: 5 + min-occurrences: 5 + gocritic: + enabled-tags: + - diagnostic + - experimental + - opinionated + - performance + - style + disabled-checks: + - dupImport + - ifElseChain + - octalLiteral + - whyNoLint + - wrapperFunc + gocyclo: + min-complexity: 15 + goimports: + local-prefixes: github.com/golangci/golangci-lint + golint: + min-confidence: 0 + govet: + check-shadowing: true + settings: + printf: + funcs: + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf + lll: + line-length: 140 + maligned: + suggest-new: true + misspell: + locale: US + +linters: + disable-all: true + enable: + - bodyclose + - deadcode + - depguard + - dogsled + - errcheck + - funlen + - goconst + - gocritic + - gocyclo + - gofmt + - goimports + - golint + - goprintffuncname + - gosec + - gosimple + - govet + - ineffassign + - interfacer + - lll + - misspell + - nakedret + - rowserrcheck + - scopelint + - staticcheck + - structcheck + - stylecheck + - typecheck + - unconvert + - unparam + - unused + - varcheck + - whitespace + +# don't enable: +# - dupl +# - gochecknoglobals +# - gochecknoinits +# - gocognit +# - godox +# - gomnd +# - maligned +# - prealloc + +service: + golangci-lint-version: 1.23.x diff --git a/LICENSE.md b/LICENSE.md index f647975..bfe0b4e 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,26 +1,27 @@ -Copyright (c) 2020, MaibornWolff GmbH -All rights reserved. +# BSD 3-Clause License -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: +Copyright (c) 2020, MaibornWolff GmbH. All rights reserved. - * Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - * Neither the name of nor the names of its contributors may be used to - endorse or promote products derived from this software without specific - prior written permission. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index 79747c3..85e2f14 100644 --- a/README.md +++ b/README.md @@ -48,4 +48,4 @@ Have a bug, a feature request or any question? Please [open a new issue](https:/ - [Releases](https://github.com/MaibornWolff/iac-count/releases) - [Contributing](CONTRIBUTING.md) - [Code of Conduct](CODE_OF_CONDUCT.md) -- [License](LICENSE.md) \ No newline at end of file +- [License](LICENSE.md) diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..d419d06 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,47 @@ +package cmd + +import ( + "os" + + "github.com/MaibornWolff/iac-count/pkg/core" + "github.com/MaibornWolff/iac-count/pkg/output" + log "github.com/sirupsen/logrus" + + "github.com/spf13/cobra" +) + +var Debug bool +var Quiet bool +var PrintLevel string + +func init() { + RootCmd.PersistentFlags().BoolVarP(&Debug, "debug", "d", false, "debug level logging") + RootCmd.PersistentFlags().BoolVarP(&Quiet, "quiet", "q", false, "error level logging only") + + RootCmd.PersistentFlags().StringVar(&PrintLevel, "level", "file", "print level (file|role|project)") +} + +func configureLogging() { + if Debug { + log.SetLevel(log.DebugLevel) + } else if Quiet { + log.SetLevel(log.ErrorLevel) + } else { + log.SetLevel(log.WarnLevel) + } + + log.SetOutput(os.Stderr) +} + +var RootCmd = &cobra.Command{ + Use: "ANSIBLE_ROOT", + Short: "analyzes projects", + Long: "analyzes projects", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + configureLogging() + + fileMetrics := core.DirectoryCreator{}.CreateFromPath(args[0], nil).Analyze() + output.PrintMetricsAsCsv(fileMetrics, PrintLevel) + }, +} diff --git a/doc/images/ansible_example_codecharta.png b/doc/images/ansible_example_codecharta.png new file mode 100644 index 0000000..9b50f1e Binary files /dev/null and b/doc/images/ansible_example_codecharta.png differ diff --git a/doc/images/ansible_example_csv.png b/doc/images/ansible_example_csv.png new file mode 100644 index 0000000..2b4856b Binary files /dev/null and b/doc/images/ansible_example_csv.png differ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..fedda70 --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module github.com/MaibornWolff/iac-count + +go 1.13 + +require ( + github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect + github.com/sirupsen/logrus v1.4.2 + github.com/spf13/cobra v0.0.6 + github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/testify v1.2.2 + golang.org/x/sys v0.0.0-20200302083256-062a44052db1 // indirect + gopkg.in/yaml.v2 v2.2.8 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..531e93c --- /dev/null +++ b/go.sum @@ -0,0 +1,150 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.6 h1:breEStsVwemnKh2/s6gMvSdMEkwW0sK8vGStnlVBMCs= +github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302083256-062a44052db1 h1:trYYa2hBaTeei9Bq2uAXwsfNYW4r+xD/tztngRsT0cQ= +golang.org/x/sys v0.0.0-20200302083256-062a44052db1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/util/collectionsUtil.go b/internal/util/collectionsUtil.go new file mode 100644 index 0000000..d2d68b4 --- /dev/null +++ b/internal/util/collectionsUtil.go @@ -0,0 +1,10 @@ +package util + +import ( + "sort" +) + +func Contains(s []string, searchterm string) bool { + i := sort.SearchStrings(s, searchterm) + return i < len(s) && s[i] == searchterm +} diff --git a/internal/util/fileUtil.go b/internal/util/fileUtil.go new file mode 100644 index 0000000..0d8ecce --- /dev/null +++ b/internal/util/fileUtil.go @@ -0,0 +1,104 @@ +package util + +import ( + "io/ioutil" + "mime" + "os" + "path/filepath" + "regexp" + "strings" + + log "github.com/sirupsen/logrus" +) + +func init() { + mapping := make(map[string]string) + mapping[".yaml"] = "text/yaml" + mapping[".yml"] = "text/yaml" + + for key, value := range mapping { + err := mime.AddExtensionType(key, value) + + if err != nil { + log.Errorf("Error %v", err) + } + } +} + +func IsHidden(path string) bool { + basename := filepath.Base(path) + return basename != "." && strings.HasPrefix(basename, ".") +} + +func ParentPath(path string) string { + basename := filepath.Base(path) + + parentPath := strings.TrimSuffix(path, string(filepath.Separator)+basename) + if parentPath == "" || parentPath == path { + parentPath = "." + } + + return parentPath +} + +func RecursiveFileCount(path string) int { + info, err := os.Stat(path) + if err != nil { + log.Warnf("%s", err) + return 0 + } else if !info.IsDir() { + return 1 + } + + numFiles := 0 + err = filepath.Walk(path, + func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() && !IsHidden(path) { + numFiles++ + } + return nil + }) + if err != nil && !os.IsNotExist(err) { + log.Errorf("%s", err) + } + return numFiles +} + +func SubdirCount(path string) int { + numDirs := 0 + + fileinfo, err := ioutil.ReadDir(path) + if err != nil { + log.Errorf("%s", err) + return 0 + } + for i := range fileinfo { + if fileinfo[i].IsDir() && !IsHidden(fileinfo[i].Name()) { + numDirs++ + } + } + + return numDirs +} + +func PathContainsDirName(path, dirName string) bool { + matched, err := regexp.MatchString("[.*/]?"+dirName+"[/.*]?", path) + if err != nil { + log.Warnf("%s", err) + } + return matched +} + +func IsTextFile(path string) bool { + mimeType := mime.TypeByExtension(filepath.Ext(path)) + return strings.HasPrefix(mimeType, "text/") || + strings.HasPrefix(mimeType, "application/json") +} + +func IsYamlFile(path string) bool { + mimeType := mime.TypeByExtension(filepath.Ext(path)) + return strings.HasPrefix(mimeType, "text/yaml") +} diff --git a/internal/util/fileUtil_test.go b/internal/util/fileUtil_test.go new file mode 100644 index 0000000..26a8622 --- /dev/null +++ b/internal/util/fileUtil_test.go @@ -0,0 +1,63 @@ +package util + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRecursiveFileCount(t *testing.T) { + tests := map[string]struct { + path string + output int + }{ + "existing file": { + path: "fileUtil.go", + output: 1, + }, + "directory with multiple files": { + path: ".", + output: 3, + }, + "invalid path": { + path: "nonexistingfile", + output: 0, + }, + } + + for testName, test := range tests { + t.Logf("Running test case %s", testName) + output := RecursiveFileCount(test.path) + assert.Equal(t, test.output, output) + } +} + +func TestIsTextFile(t *testing.T) { + tests := map[string]struct { + path string + result bool + }{ + "json": { + path: "test/data/main.json", + result: true, + }, + "yml": { + path: "test/data/main.yml", + result: true, + }, + "txt": { + path: "test/data/main.txt", + result: true, + }, + "mp3": { + path: "test/data/main.mp3", + result: false, + }, + } + + for testName, test := range tests { + t.Logf("Running test case %s", testName) + result := IsTextFile(test.path) + assert.Equal(t, test.result, result) + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..c7e6c2f --- /dev/null +++ b/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "log" + + "github.com/MaibornWolff/iac-count/cmd" +) + +func main() { + err := cmd.RootCmd.Execute() + + if err != nil { + log.Fatal(err) + } +} diff --git a/model.md b/model.md new file mode 100644 index 0000000..3589e65 --- /dev/null +++ b/model.md @@ -0,0 +1,22 @@ +# Domain Model for iac-count + +## Domain Objects + +- SubjectCreator + - Responsibilities: Creates Subject from path + - Collaborators: Subject +- Subject + - Collaborators: SubjectCreator, Node, Metric, MetricCalculator + - Examples: Directory, File, Yamlfile +- MetricCalculator + - Responsibilities: Calculates Metric from content + - Collaborators: Metric, Subject +- Node + - Collaborators: Metric, Subject +- Metric + - Collaborators: MetricCalculator + - Examples: Loc + +### Extension Points + +- Dependency diff --git a/pkg/core/directory.go b/pkg/core/directory.go new file mode 100644 index 0000000..d9e2908 --- /dev/null +++ b/pkg/core/directory.go @@ -0,0 +1,97 @@ +package core + +import ( + "io/ioutil" + "os" + "path/filepath" + + "github.com/MaibornWolff/iac-count/internal/util" + "github.com/MaibornWolff/iac-count/pkg/metrics" + log "github.com/sirupsen/logrus" +) + +type Directory struct { + path string +} + +func (subject Directory) Path() string { + return subject.path +} + +func (subject Directory) Analyze() map[string]metrics.Node { + nodes := make(map[string]metrics.Node, 1) + + fileinfo, err := ioutil.ReadDir(subject.path) + if err == nil { + for i := range fileinfo { + info := fileinfo[i] + path := filepath.Join(subject.path, info.Name()) + if util.IsHidden(path) { + log.Infof("Skipping hidden path %s", path) + continue + } + + log.Debugf("Analyzing %s", path) + + subject := createSubject(path, info) + + for k, v := range subject.Analyze() { + nodes[k] = v + } + } + } else { + log.Errorf("%s", err) + } + + nodes[subject.path] = metrics.Node{ + Path: subject.path, + NodeType: "dir", + Metrics: subject.CalculateMetrics(), + } + + return nodes +} + +var fileCreator = FileCreator{} +var yamlfileCreator = YamlfileCreator{} +var textfileCreator = TextfileCreator{} +var directoryCreator = DirectoryCreator{} + +func createSubject(path string, info os.FileInfo) Subject { + if !info.IsDir() { + if util.IsYamlFile(path) { + return yamlfileCreator.CreateFromPath(path, info) + } else if util.IsTextFile(path) { + return textfileCreator.CreateFromPath(path, info) + } else { + return fileCreator.CreateFromPath(path, info) + } + } else { + return directoryCreator.CreateFromPath(path, info) + } +} + +func (subject Directory) CalculateMetrics() map[string]metrics.Metric { + path := subject.Path() + var metricMap = make(map[string]metrics.Metric, len(directoryCalculators)) + + for _, calc := range directoryCalculators { + metric := calc.Analyze(path, "") + metricMap[metric.Name()] = metric + } + + return metricMap +} + +var directoryCalculators = []metrics.MetricCalculator{ + metrics.FilesCalculator{}, +} + +type DirectoryCreator struct { +} + +func (creator DirectoryCreator) CreateFromPath(path string, info os.FileInfo) Subject { + return Directory{ + path: path, + } +} diff --git a/pkg/core/directory_test.go b/pkg/core/directory_test.go new file mode 100644 index 0000000..46fb15c --- /dev/null +++ b/pkg/core/directory_test.go @@ -0,0 +1,36 @@ +package core + +import ( + "testing" + + log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" +) + +func TestDirectoryAnalyze(t *testing.T) { + tests := map[string]metricTest{ + "successful files Calculation": { + path: "test/data", + subjectCreator: DirectoryCreator{}, + numberOfNodes: 28, + }, + } + + runMetricTest(t, tests) +} + +type metricTest struct { + path string + subjectCreator SubjectCreator + numberOfNodes int +} + +func runMetricTest(t *testing.T, tests map[string]metricTest) { + for testName, test := range tests { + t.Logf("Running test case %s", testName) + subject := test.subjectCreator.CreateFromPath(test.path, nil) + result := subject.Analyze() + log.Infof("Result: %v", result) + assert.Equal(t, test.numberOfNodes, len(result)) + } +} diff --git a/pkg/core/file.go b/pkg/core/file.go new file mode 100644 index 0000000..1387782 --- /dev/null +++ b/pkg/core/file.go @@ -0,0 +1,52 @@ +package core + +import ( + "os" + + input "github.com/MaibornWolff/iac-count/pkg/input" + + "github.com/MaibornWolff/iac-count/pkg/metrics" +) + +type File struct { + path string +} + +func (subject File) Path() string { + return subject.path +} + +func (subject File) Analyze() map[string]metrics.Node { + nodes := make(map[string]metrics.Node, 1) + nodes[subject.path] = metrics.Node{ + Path: subject.path, + NodeType: "file", + Metrics: subject.CalculateMetrics(), + } + + return nodes +} + +var fileCalculators = []metrics.MetricCalculator{} + +func (subject File) CalculateMetrics() map[string]metrics.Metric { + path := subject.Path() + var metricMap = make(map[string]metrics.Metric, len(fileCalculators)) + + for _, calc := range fileCalculators { + content := input.ReadFileToString(path) + metric := calc.Analyze(path, content) + metricMap[metric.Name()] = metric + } + + return metricMap +} + +type FileCreator struct { +} + +func (creator FileCreator) CreateFromPath(path string, info os.FileInfo) Subject { + return File{ + path: path, + } +} diff --git a/pkg/core/file_test.go b/pkg/core/file_test.go new file mode 100644 index 0000000..9a9d804 --- /dev/null +++ b/pkg/core/file_test.go @@ -0,0 +1,17 @@ +package core + +import ( + "testing" +) + +func TestFileAnalyze(t *testing.T) { + tests := map[string]metricTest{ + "successful files Calculation": { + path: "test/data/taskfile.yaml", + subjectCreator: FileCreator{}, + numberOfNodes: 1, + }, + } + + runMetricTest(t, tests) +} diff --git a/pkg/core/subject.go b/pkg/core/subject.go new file mode 100644 index 0000000..0eb01a4 --- /dev/null +++ b/pkg/core/subject.go @@ -0,0 +1,17 @@ +package core + +import ( + "os" + + "github.com/MaibornWolff/iac-count/pkg/metrics" +) + +type Subject interface { + Path() string + CalculateMetrics() map[string]metrics.Metric + Analyze() map[string]metrics.Node +} + +type SubjectCreator interface { + CreateFromPath(path string, info os.FileInfo) Subject +} diff --git a/pkg/core/test/data/group_vars/main.yml b/pkg/core/test/data/group_vars/main.yml new file mode 100644 index 0000000..1c8f635 --- /dev/null +++ b/pkg/core/test/data/group_vars/main.yml @@ -0,0 +1,4 @@ +--- + +var1: 'false' +var2: 'true' \ No newline at end of file diff --git a/pkg/core/test/data/host_vars/prod.yml b/pkg/core/test/data/host_vars/prod.yml new file mode 100644 index 0000000..99f190c --- /dev/null +++ b/pkg/core/test/data/host_vars/prod.yml @@ -0,0 +1,7 @@ +complex_thingy: + - name: bla + some_other: cdll_mq + +simple_var: ' ablas' +other_type: true + \ No newline at end of file diff --git a/pkg/core/test/data/main.yml b/pkg/core/test/data/main.yml new file mode 100644 index 0000000..6490c19 --- /dev/null +++ b/pkg/core/test/data/main.yml @@ -0,0 +1,28 @@ +--- +# some comment +# another comment + - hosts: webservers + vars: + http_port: 80 + max_clients: 200 + remote_user: root + tasks: + - name: ensure apache is at the latest version + yum: + name: httpd + state: latest + - name: write the apache config file + template: + src: /srv/httpd.j2 + dest: /etc/httpd.conf + notify: + - restart apache + - name: ensure apache is running + service: + name: httpd + state: started + handlers: + - name: restart apache + service: + name: httpd + state: restarted \ No newline at end of file diff --git a/pkg/core/test/data/plugins/someplugin/somepluginfile b/pkg/core/test/data/plugins/someplugin/somepluginfile new file mode 100644 index 0000000..e69de29 diff --git a/pkg/core/test/data/roles/example/defaults/main.yml b/pkg/core/test/data/roles/example/defaults/main.yml new file mode 100644 index 0000000..4f55c01 --- /dev/null +++ b/pkg/core/test/data/roles/example/defaults/main.yml @@ -0,0 +1,15 @@ +--- + +# Some vars +bla: 'blabla' +bla_thingy: 202020 + +# Some other vars +blubb: 'bla blubb' +blubb_thingy: '202021' + +# A whole lot of stupid vars +bla_blubbs: + bla: 'blubb.bla' + enabled: True + default: False diff --git a/pkg/core/test/data/roles/example/files/somefile b/pkg/core/test/data/roles/example/files/somefile new file mode 100644 index 0000000..e69de29 diff --git a/pkg/core/test/data/roles/example/handlers/main.yml b/pkg/core/test/data/roles/example/handlers/main.yml new file mode 100644 index 0000000..9faa1d9 --- /dev/null +++ b/pkg/core/test/data/roles/example/handlers/main.yml @@ -0,0 +1,11 @@ +--- +# some handler + +- name: restart memcached + service: + name: memcached + state: restarted +- name: restart apache + service: + name: apache + state: restarted \ No newline at end of file diff --git a/pkg/core/test/data/roles/example/meta/main.yml b/pkg/core/test/data/roles/example/meta/main.yml new file mode 100644 index 0000000..77247ec --- /dev/null +++ b/pkg/core/test/data/roles/example/meta/main.yml @@ -0,0 +1,14 @@ +--- + +# three dependencies +dependencies: + - role: first_role + vars: + some_parameter: 1 + - role: second_role + vars: + some_port: 80 + - role: third_role + vars: + some_param: gogogo + other_param: 999 \ No newline at end of file diff --git a/pkg/core/test/data/roles/example/plugins/someplugin/somepluginfile b/pkg/core/test/data/roles/example/plugins/someplugin/somepluginfile new file mode 100644 index 0000000..e69de29 diff --git a/pkg/core/test/data/roles/example/tasks/main.yml b/pkg/core/test/data/roles/example/tasks/main.yml new file mode 100644 index 0000000..f42a1c4 --- /dev/null +++ b/pkg/core/test/data/roles/example/tasks/main.yml @@ -0,0 +1,44 @@ +--- +# this file contains some tasks +- name: Name1 + yum: name={{ item }} state=present + with_items: + - first-package + - second-package + +- name: Name2 + copy: src=somesrc dest=somedest + +- name: Name3 + copy: src=someothersrc dest=someotherdest + +- name: Name4 + yum: name={{ item }} state=present + with_items: + - first-other-package + - second-other-package + - third-other-package + +- name: Name5 + yum: name=pkg state=present + tags: pkg + +- name: Name6 + template: src=pkg.conf.j2 dest=/etc/pkg.conf + tags: pkg + notify: restart pkg + +- name: Name7 + service: name=pkgd state=started enabled=yes + tags: pkg + +# comment +- name: Name8 + template: src=template.j2 dest=templetdest + when: ansible_distribution_major_version != '7' + notify: restart someserv + +- name: Name9 + command: somecommand + register: somehandler + changed_when: false diff --git a/pkg/core/test/data/roles/example/templates/sometemplate b/pkg/core/test/data/roles/example/templates/sometemplate new file mode 100644 index 0000000..e69de29 diff --git a/pkg/core/test/data/roles/example/vars/main.yml b/pkg/core/test/data/roles/example/vars/main.yml new file mode 100644 index 0000000..4f55c01 --- /dev/null +++ b/pkg/core/test/data/roles/example/vars/main.yml @@ -0,0 +1,15 @@ +--- + +# Some vars +bla: 'blabla' +bla_thingy: 202020 + +# Some other vars +blubb: 'bla blubb' +blubb_thingy: '202021' + +# A whole lot of stupid vars +bla_blubbs: + bla: 'blubb.bla' + enabled: True + default: False diff --git a/pkg/core/textfile.go b/pkg/core/textfile.go new file mode 100644 index 0000000..f09aec6 --- /dev/null +++ b/pkg/core/textfile.go @@ -0,0 +1,54 @@ +package core + +import ( + "os" + + input "github.com/MaibornWolff/iac-count/pkg/input" + + "github.com/MaibornWolff/iac-count/pkg/metrics" +) + +type Textfile struct { + path string +} + +func (subject Textfile) Path() string { + return subject.path +} + +func (subject Textfile) Analyze() map[string]metrics.Node { + nodes := make(map[string]metrics.Node, 1) + nodes[subject.path] = metrics.Node{ + Path: subject.path, + NodeType: "file", + Metrics: subject.CalculateMetrics(), + } + + return nodes +} + +var textfileCalculators = []metrics.MetricCalculator{ + metrics.LocCalculator{}, +} + +func (subject Textfile) CalculateMetrics() map[string]metrics.Metric { + path := subject.Path() + var metricMap = make(map[string]metrics.Metric, len(fileCalculators)) + + for _, calc := range textfileCalculators { + content := input.ReadFileToString(path) + metric := calc.Analyze(path, content) + metricMap[metric.Name()] = metric + } + + return metricMap +} + +type TextfileCreator struct { +} + +func (creator TextfileCreator) CreateFromPath(path string, info os.FileInfo) Subject { + return Textfile{ + path: path, + } +} diff --git a/pkg/core/textfile_test.go b/pkg/core/textfile_test.go new file mode 100644 index 0000000..4574f71 --- /dev/null +++ b/pkg/core/textfile_test.go @@ -0,0 +1,17 @@ +package core + +import ( + "testing" +) + +func TestTextfileAnalyze(t *testing.T) { + tests := map[string]metricTest{ + "successful files Calculation": { + path: "test/data/taskfile.yaml", + subjectCreator: TextfileCreator{}, + numberOfNodes: 1, + }, + } + + runMetricTest(t, tests) +} diff --git a/pkg/core/yamlfile.go b/pkg/core/yamlfile.go new file mode 100644 index 0000000..de5729d --- /dev/null +++ b/pkg/core/yamlfile.go @@ -0,0 +1,56 @@ +package core + +import ( + "os" + + input "github.com/MaibornWolff/iac-count/pkg/input" + + "github.com/MaibornWolff/iac-count/pkg/metrics" +) + +type Yamlfile struct { + path string +} + +func (subject Yamlfile) Path() string { + return subject.path +} + +func (subject Yamlfile) Analyze() map[string]metrics.Node { + nodes := make(map[string]metrics.Node, 1) + nodes[subject.path] = metrics.Node{ + Path: subject.path, + NodeType: "yaml", + Metrics: subject.CalculateMetrics(), + } + + return nodes +} + +var yamlfileCalculators = []metrics.MetricCalculator{ + metrics.LocCalculator{}, + metrics.RlocCalculator{}, + metrics.CommentlinesCalculator{}, +} + +func (subject Yamlfile) CalculateMetrics() map[string]metrics.Metric { + path := subject.Path() + var metricMap = make(map[string]metrics.Metric, len(fileCalculators)) + + for _, calc := range yamlfileCalculators { + content := input.ReadFileToString(path) + metric := calc.Analyze(path, content) + metricMap[metric.Name()] = metric + } + + return metricMap +} + +type YamlfileCreator struct { +} + +func (creator YamlfileCreator) CreateFromPath(path string, info os.FileInfo) Subject { + return Yamlfile{ + path: path, + } +} diff --git a/pkg/core/yamlfile_test.go b/pkg/core/yamlfile_test.go new file mode 100644 index 0000000..83b5d89 --- /dev/null +++ b/pkg/core/yamlfile_test.go @@ -0,0 +1,17 @@ +package core + +import ( + "testing" +) + +func TestYamlfileAnalyze(t *testing.T) { + tests := map[string]metricTest{ + "successful files Calculation": { + path: "test/data/taskfile.yaml", + subjectCreator: YamlfileCreator{}, + numberOfNodes: 1, + }, + } + + runMetricTest(t, tests) +} diff --git a/pkg/input/file_reader.go b/pkg/input/file_reader.go new file mode 100644 index 0000000..d51225e --- /dev/null +++ b/pkg/input/file_reader.go @@ -0,0 +1,18 @@ +package reader + +import ( + "io/ioutil" + + log "github.com/sirupsen/logrus" +) + +// ReadFileToString returns content of a file as string if it exists +// or empty string if it doesn't exist +func ReadFileToString(path string) string { + content, err := ioutil.ReadFile(path) + if err != nil { + log.Warnf("%s", err) + } + + return string(content) +} diff --git a/pkg/input/file_reader_test.go b/pkg/input/file_reader_test.go new file mode 100644 index 0000000..0c68a01 --- /dev/null +++ b/pkg/input/file_reader_test.go @@ -0,0 +1,29 @@ +package reader + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestReadFileToString(t *testing.T) { + tests := map[string]struct { + path string + lenOutput int + }{ + "existing file": { + path: "test/data/taskfile.yaml", + lenOutput: 852, + }, + "invalid path": { + path: "test/data/unknownfile", + lenOutput: 0, + }, + } + + for testName, test := range tests { + t.Logf("Running test case %s", testName) + output := len(ReadFileToString(test.path)) + assert.Equal(t, test.lenOutput, output) + } +} diff --git a/pkg/input/test/data/taskfile.yaml b/pkg/input/test/data/taskfile.yaml new file mode 100644 index 0000000..f42a1c4 --- /dev/null +++ b/pkg/input/test/data/taskfile.yaml @@ -0,0 +1,44 @@ +--- +# this file contains some tasks +- name: Name1 + yum: name={{ item }} state=present + with_items: + - first-package + - second-package + +- name: Name2 + copy: src=somesrc dest=somedest + +- name: Name3 + copy: src=someothersrc dest=someotherdest + +- name: Name4 + yum: name={{ item }} state=present + with_items: + - first-other-package + - second-other-package + - third-other-package + +- name: Name5 + yum: name=pkg state=present + tags: pkg + +- name: Name6 + template: src=pkg.conf.j2 dest=/etc/pkg.conf + tags: pkg + notify: restart pkg + +- name: Name7 + service: name=pkgd state=started enabled=yes + tags: pkg + +# comment +- name: Name8 + template: src=template.j2 dest=templetdest + when: ansible_distribution_major_version != '7' + notify: restart someserv + +- name: Name9 + command: somecommand + register: somehandler + changed_when: false diff --git a/pkg/input/test/data/varsfile.yaml b/pkg/input/test/data/varsfile.yaml new file mode 100644 index 0000000..99f190c --- /dev/null +++ b/pkg/input/test/data/varsfile.yaml @@ -0,0 +1,7 @@ +complex_thingy: + - name: bla + some_other: cdll_mq + +simple_var: ' ablas' +other_type: true + \ No newline at end of file diff --git a/pkg/input/yaml_reader.go b/pkg/input/yaml_reader.go new file mode 100644 index 0000000..535eb08 --- /dev/null +++ b/pkg/input/yaml_reader.go @@ -0,0 +1,31 @@ +package reader + +import ( + log "github.com/sirupsen/logrus" + + "gopkg.in/yaml.v2" +) + +// Returns list if a yaml string is provided or an empty string otherwise +func ReadYamlAsList(data string) []interface{} { + var m []interface{} + + err := yaml.Unmarshal([]byte(data), &m) + if err != nil { + log.Warnf("%s", err) + } + + return m +} + +// Returns a map if a yaml string is provided or an empty map otherwise +func ReadYamlAsMap(data string) map[string]interface{} { + m := make(map[string]interface{}) + + err := yaml.Unmarshal([]byte(data), &m) + if err != nil { + log.Warnf("%s", err) + } + + return m +} diff --git a/pkg/input/yaml_reader_test.go b/pkg/input/yaml_reader_test.go new file mode 100644 index 0000000..14dd867 --- /dev/null +++ b/pkg/input/yaml_reader_test.go @@ -0,0 +1,45 @@ +package reader + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestReadYamlAsList(t *testing.T) { + tests := map[string]struct { + content string + lenOutput int + }{ + "valid yaml": { + content: ReadFileToString("test/data/taskfile.yaml"), + lenOutput: 9, + }, + "invalid yaml": { + content: " as as as ", + lenOutput: 0, + }, + } + + for testName, test := range tests { + t.Logf("Running test case %s", testName) + output := len(ReadYamlAsList(test.content)) + assert.Equal(t, test.lenOutput, output) + } +} + +func TestReadYamlAsMap(t *testing.T) { + exampleData := ReadFileToString("test/data/varsfile.yaml") + got := ReadYamlAsMap(exampleData)["simple_var"] + want := " ablas" + + assert.Equal(t, want, got) +} + +func TestReadYamlAsMapOnFailure(t *testing.T) { + invalidYaml := " as as as" + got := len(ReadYamlAsMap(invalidYaml)) + want := 0 + + assert.Equal(t, want, got) +} diff --git a/pkg/metrics/commentlines.go b/pkg/metrics/commentlines.go new file mode 100644 index 0000000..f0a7cc8 --- /dev/null +++ b/pkg/metrics/commentlines.go @@ -0,0 +1,29 @@ +package metrics + +type CommentLines struct { + Val int +} + +func (metric CommentLines) Description() string { + return "Number of comment-only lines in file" +} + +func (metric CommentLines) Name() string { + return "comment_lines" +} + +func (metric CommentLines) Value() int { + return metric.Val +} + +func (metric CommentLines) add(additional Metric) Metric { + if additional == nil { + return CommentLines{ + Val: metric.Value(), + } + } + + return CommentLines{ + Val: metric.Value() + additional.Value(), + } +} diff --git a/pkg/metrics/commentlinescalculator.go b/pkg/metrics/commentlinescalculator.go new file mode 100644 index 0000000..3708ca9 --- /dev/null +++ b/pkg/metrics/commentlinescalculator.go @@ -0,0 +1,26 @@ +package metrics + +import ( + "bufio" + "regexp" + "strings" +) + +type CommentlinesCalculator struct { +} + +func (calculator CommentlinesCalculator) Analyze(path, content string) Metric { + re := regexp.MustCompile(`^\s*#`) + count := 0 + scanner := bufio.NewScanner(strings.NewReader(content)) + for scanner.Scan() { + text := scanner.Text() + if re.FindStringIndex(text) != nil { + count++ + } + } + + return Commentlines{ + Val: count, + } +} diff --git a/pkg/metrics/commentlinescalculator_test.go b/pkg/metrics/commentlinescalculator_test.go new file mode 100644 index 0000000..d07c581 --- /dev/null +++ b/pkg/metrics/commentlinescalculator_test.go @@ -0,0 +1,20 @@ +package metrics + +import ( + "testing" + + input "github.com/MaibornWolff/iac-count/pkg/input" +) + +func TestCommentLines(t *testing.T) { + tests := map[string]metricTest{ + "successful comments Calculation": { + path: "test/data/main.yml", + content: input.ReadFileToString("test/data/main.yml"), + calculator: CommentLinesCalculator{}, + output: 2, + }, + } + + runMetricTest(t, tests) +} diff --git a/pkg/metrics/files.go b/pkg/metrics/files.go new file mode 100644 index 0000000..22b7db4 --- /dev/null +++ b/pkg/metrics/files.go @@ -0,0 +1,29 @@ +package metrics + +type Files struct { + Val int +} + +func (metric Files) Description() string { + return "Number of files" +} + +func (metric Files) Name() string { + return "files" +} + +func (metric Files) Value() int { + return metric.Val +} + +func (metric Files) add(additional Metric) Metric { + if additional == nil { + return Files{ + Val: metric.Value(), + } + } + + return Files{ + Val: metric.Value(), + } +} diff --git a/pkg/metrics/filescalculator.go b/pkg/metrics/filescalculator.go new file mode 100644 index 0000000..d7a1f8a --- /dev/null +++ b/pkg/metrics/filescalculator.go @@ -0,0 +1,14 @@ +package metrics + +import ( + "github.com/MaibornWolff/iac-count/internal/util" +) + +type FilesCalculator struct { +} + +func (calculator FilesCalculator) Analyze(path, content string) Metric { + return Files{ + Val: util.RecursiveFileCount(path), + } +} diff --git a/pkg/metrics/filescalculator_test.go b/pkg/metrics/filescalculator_test.go new file mode 100644 index 0000000..044cff7 --- /dev/null +++ b/pkg/metrics/filescalculator_test.go @@ -0,0 +1,18 @@ +package metrics + +import ( + "testing" +) + +func TestFiles(t *testing.T) { + tests := map[string]metricTest{ + "successful files Calculation": { + path: "test/data", + content: "", + calculator: FilesCalculator{}, + output: 12, + }, + } + + runMetricTest(t, tests) +} diff --git a/pkg/metrics/loc.go b/pkg/metrics/loc.go new file mode 100644 index 0000000..0eb3add --- /dev/null +++ b/pkg/metrics/loc.go @@ -0,0 +1,29 @@ +package metrics + +type Loc struct { + Val int +} + +func (metric Loc) Description() string { + return "Number of code lines in file" +} + +func (metric Loc) Name() string { + return "loc" +} + +func (metric Loc) Value() int { + return metric.Val +} + +func (metric Loc) add(additional Metric) Metric { + if additional == nil { + return Loc{ + Val: metric.Value(), + } + } + + return Loc{ + Val: metric.Value() + additional.Value(), + } +} diff --git a/pkg/metrics/loccalculator.go b/pkg/metrics/loccalculator.go new file mode 100644 index 0000000..09f485e --- /dev/null +++ b/pkg/metrics/loccalculator.go @@ -0,0 +1,14 @@ +package metrics + +import ( + "strings" +) + +type LocCalculator struct { +} + +func (calculator LocCalculator) Analyze(path, content string) Metric { + return Loc{ + Val: strings.Count(content, "\n"), + } +} diff --git a/pkg/metrics/loccalculator_test.go b/pkg/metrics/loccalculator_test.go new file mode 100644 index 0000000..059a93d --- /dev/null +++ b/pkg/metrics/loccalculator_test.go @@ -0,0 +1,20 @@ +package metrics + +import ( + "testing" + + input "github.com/MaibornWolff/iac-count/pkg/input" +) + +func TestLoc(t *testing.T) { + tests := map[string]metricTest{ + "successful loc Calculation": { + path: "test/data/main.yml", + content: input.ReadFileToString("test/data/main.yml"), + calculator: LocCalculator{}, + output: 27, + }, + } + + runMetricTest(t, tests) +} diff --git a/pkg/metrics/metric.go b/pkg/metrics/metric.go new file mode 100644 index 0000000..e7099eb --- /dev/null +++ b/pkg/metrics/metric.go @@ -0,0 +1,26 @@ +package metrics + +type Metric interface { + Name() string + Value() int + Description() string + add(metric Metric) Metric +} + +type MetricCalculator interface { + Analyze(path, content string) Metric +} + +func AggregateMetrics(origin, additional *(map[string]Metric)) { // nolint:gocritic + for key, metric := range *origin { + if addMetric, exists := (*additional)[key]; exists { + (*origin)[key] = metric.add(addMetric) + } + } + + for key, addMetric := range *additional { + if _, exists := (*origin)[key]; !exists { + (*origin)[key] = addMetric.add(nil) + } + } +} diff --git a/pkg/metrics/node.go b/pkg/metrics/node.go new file mode 100644 index 0000000..4bb08c0 --- /dev/null +++ b/pkg/metrics/node.go @@ -0,0 +1,7 @@ +package metrics + +type Node struct { + Path string + NodeType string + Metrics map[string]Metric +} diff --git a/pkg/metrics/rloc.go b/pkg/metrics/rloc.go new file mode 100644 index 0000000..e421a13 --- /dev/null +++ b/pkg/metrics/rloc.go @@ -0,0 +1,29 @@ +package metrics + +type Rloc struct { + Val int +} + +func (metric Rloc) Description() string { + return "Number of non-blank, non-comment lines in file" +} + +func (metric Rloc) Name() string { + return "rloc" +} + +func (metric Rloc) Value() int { + return metric.Val +} + +func (metric Rloc) add(additional Metric) Metric { + if additional == nil { + return Rloc{ + Val: metric.Value(), + } + } + + return Rloc{ + Val: metric.Value() + additional.Value(), + } +} diff --git a/pkg/metrics/rloccalculator.go b/pkg/metrics/rloccalculator.go new file mode 100644 index 0000000..4ff2271 --- /dev/null +++ b/pkg/metrics/rloccalculator.go @@ -0,0 +1,26 @@ +package metrics + +import ( + "bufio" + "regexp" + "strings" +) + +type RlocCalculator struct { +} + +func (calculator RlocCalculator) Analyze(path, content string) Metric { + re := regexp.MustCompile(`^\s*[^#\s]`) + count := 0 + scanner := bufio.NewScanner(strings.NewReader(content)) + for scanner.Scan() { + text := scanner.Text() + if re.FindStringIndex(text) != nil && text != "---" { + count++ + } + } + + return Rloc{ + Val: count, + } +} diff --git a/pkg/metrics/rloccalculator_test.go b/pkg/metrics/rloccalculator_test.go new file mode 100644 index 0000000..28bb4a8 --- /dev/null +++ b/pkg/metrics/rloccalculator_test.go @@ -0,0 +1,20 @@ +package metrics + +import ( + "testing" + + input "github.com/MaibornWolff/iac-count/pkg/input" +) + +func TestRloc(t *testing.T) { + tests := map[string]metricTest{ + "successful rloc Calculation": { + path: "test/data/main.yml", + content: input.ReadFileToString("test/data/main.yml"), + calculator: RlocCalculator{}, + output: 25, + }, + } + + runMetricTest(t, tests) +} diff --git a/pkg/metrics/test/data/group_vars/main.yml b/pkg/metrics/test/data/group_vars/main.yml new file mode 100644 index 0000000..1c8f635 --- /dev/null +++ b/pkg/metrics/test/data/group_vars/main.yml @@ -0,0 +1,4 @@ +--- + +var1: 'false' +var2: 'true' \ No newline at end of file diff --git a/pkg/metrics/test/data/host_vars/prod.yml b/pkg/metrics/test/data/host_vars/prod.yml new file mode 100644 index 0000000..99f190c --- /dev/null +++ b/pkg/metrics/test/data/host_vars/prod.yml @@ -0,0 +1,7 @@ +complex_thingy: + - name: bla + some_other: cdll_mq + +simple_var: ' ablas' +other_type: true + \ No newline at end of file diff --git a/pkg/metrics/test/data/main.yml b/pkg/metrics/test/data/main.yml new file mode 100644 index 0000000..6490c19 --- /dev/null +++ b/pkg/metrics/test/data/main.yml @@ -0,0 +1,28 @@ +--- +# some comment +# another comment + - hosts: webservers + vars: + http_port: 80 + max_clients: 200 + remote_user: root + tasks: + - name: ensure apache is at the latest version + yum: + name: httpd + state: latest + - name: write the apache config file + template: + src: /srv/httpd.j2 + dest: /etc/httpd.conf + notify: + - restart apache + - name: ensure apache is running + service: + name: httpd + state: started + handlers: + - name: restart apache + service: + name: httpd + state: restarted \ No newline at end of file diff --git a/pkg/metrics/test/data/plugins/someplugin/somepluginfile b/pkg/metrics/test/data/plugins/someplugin/somepluginfile new file mode 100644 index 0000000..e69de29 diff --git a/pkg/metrics/test/data/roles/example/defaults/main.yml b/pkg/metrics/test/data/roles/example/defaults/main.yml new file mode 100644 index 0000000..4f55c01 --- /dev/null +++ b/pkg/metrics/test/data/roles/example/defaults/main.yml @@ -0,0 +1,15 @@ +--- + +# Some vars +bla: 'blabla' +bla_thingy: 202020 + +# Some other vars +blubb: 'bla blubb' +blubb_thingy: '202021' + +# A whole lot of stupid vars +bla_blubbs: + bla: 'blubb.bla' + enabled: True + default: False diff --git a/pkg/metrics/test/data/roles/example/files/somefile b/pkg/metrics/test/data/roles/example/files/somefile new file mode 100644 index 0000000..e69de29 diff --git a/pkg/metrics/test/data/roles/example/handlers/main.yml b/pkg/metrics/test/data/roles/example/handlers/main.yml new file mode 100644 index 0000000..9faa1d9 --- /dev/null +++ b/pkg/metrics/test/data/roles/example/handlers/main.yml @@ -0,0 +1,11 @@ +--- +# some handler + +- name: restart memcached + service: + name: memcached + state: restarted +- name: restart apache + service: + name: apache + state: restarted \ No newline at end of file diff --git a/pkg/metrics/test/data/roles/example/meta/main.yml b/pkg/metrics/test/data/roles/example/meta/main.yml new file mode 100644 index 0000000..77247ec --- /dev/null +++ b/pkg/metrics/test/data/roles/example/meta/main.yml @@ -0,0 +1,14 @@ +--- + +# three dependencies +dependencies: + - role: first_role + vars: + some_parameter: 1 + - role: second_role + vars: + some_port: 80 + - role: third_role + vars: + some_param: gogogo + other_param: 999 \ No newline at end of file diff --git a/pkg/metrics/test/data/roles/example/plugins/someplugin/somepluginfile b/pkg/metrics/test/data/roles/example/plugins/someplugin/somepluginfile new file mode 100644 index 0000000..e69de29 diff --git a/pkg/metrics/test/data/roles/example/tasks/main.yml b/pkg/metrics/test/data/roles/example/tasks/main.yml new file mode 100644 index 0000000..f42a1c4 --- /dev/null +++ b/pkg/metrics/test/data/roles/example/tasks/main.yml @@ -0,0 +1,44 @@ +--- +# this file contains some tasks +- name: Name1 + yum: name={{ item }} state=present + with_items: + - first-package + - second-package + +- name: Name2 + copy: src=somesrc dest=somedest + +- name: Name3 + copy: src=someothersrc dest=someotherdest + +- name: Name4 + yum: name={{ item }} state=present + with_items: + - first-other-package + - second-other-package + - third-other-package + +- name: Name5 + yum: name=pkg state=present + tags: pkg + +- name: Name6 + template: src=pkg.conf.j2 dest=/etc/pkg.conf + tags: pkg + notify: restart pkg + +- name: Name7 + service: name=pkgd state=started enabled=yes + tags: pkg + +# comment +- name: Name8 + template: src=template.j2 dest=templetdest + when: ansible_distribution_major_version != '7' + notify: restart someserv + +- name: Name9 + command: somecommand + register: somehandler + changed_when: false diff --git a/pkg/metrics/test/data/roles/example/templates/sometemplate b/pkg/metrics/test/data/roles/example/templates/sometemplate new file mode 100644 index 0000000..e69de29 diff --git a/pkg/metrics/test/data/roles/example/vars/main.yml b/pkg/metrics/test/data/roles/example/vars/main.yml new file mode 100644 index 0000000..4f55c01 --- /dev/null +++ b/pkg/metrics/test/data/roles/example/vars/main.yml @@ -0,0 +1,15 @@ +--- + +# Some vars +bla: 'blabla' +bla_thingy: 202020 + +# Some other vars +blubb: 'bla blubb' +blubb_thingy: '202021' + +# A whole lot of stupid vars +bla_blubbs: + bla: 'blubb.bla' + enabled: True + default: False diff --git a/pkg/metrics/test_util_test.go b/pkg/metrics/test_util_test.go new file mode 100644 index 0000000..cbedb00 --- /dev/null +++ b/pkg/metrics/test_util_test.go @@ -0,0 +1,22 @@ +package metrics + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +type metricTest struct { + path string + content string + calculator MetricCalculator + output int +} + +func runMetricTest(t *testing.T, tests map[string]metricTest) { + for testName, test := range tests { + t.Logf("Running test case %s", testName) + result := test.calculator.Analyze(test.path, test.content).Value() + assert.Equal(t, test.output, result) + } +} diff --git a/pkg/output/csvprinter.go b/pkg/output/csvprinter.go new file mode 100644 index 0000000..9681911 --- /dev/null +++ b/pkg/output/csvprinter.go @@ -0,0 +1,84 @@ +package output + +import ( + "fmt" + "log" + "sort" + "strconv" + "strings" + + "github.com/MaibornWolff/iac-count/internal/util" + "github.com/MaibornWolff/iac-count/pkg/metrics" +) + +const ( + PrintLevelRole = "role" + PrintLevelFile = "file" + PrintLevelProject = "project" +) + +func csvHeader(metricNames []string) string { + return "path,type," + strings.Join(metricNames, ",") +} + +func csvBodyLine(k string, v metrics.Node, metricNames []string) string { + var sb strings.Builder + sb.WriteString(k) + sb.WriteString(",") + sb.WriteString(v.NodeType) + for _, metricName := range metricNames { + sb.WriteString(",") + for _, metric := range v.Metrics { + if metric != nil && metric.Name() == metricName { + sb.WriteString(strconv.Itoa(metric.Value())) + } + } + } + + return sb.String() +} + +func PrintMetricsAsCsv(metrics map[string]metrics.Node, level string) { + switch level { + case PrintLevelFile: + printAsCsv(metrics, func(it string) bool { return true }) + case PrintLevelRole: + printAsCsv(metrics, func(it string) bool { return it == "role" || it == "ansible_project" }) + case PrintLevelProject: + printAsCsv(metrics, func(it string) bool { return it == "ansible_project" }) + default: + log.Fatalf("Unknown printing level: %s", level) + } +} + +func calculatedMetricNames(metrics map[string]metrics.Node) []string { + var metricNames []string + + for _, node := range metrics { + for metricName := range node.Metrics { + if !util.Contains(metricNames, metricName) { + metricNames = append(metricNames, metricName) + sort.Strings(metricNames) + } + } + } + + return metricNames +} + +func printAsCsv(metrics map[string]metrics.Node, filter func(string) bool) { + metricNames := calculatedMetricNames(metrics) + fmt.Println(csvHeader(metricNames)) + + keys := make([]string, 0, len(metrics)) + for k := range metrics { + keys = append(keys, k) + } + sort.Strings(keys) + + for _, k := range keys { + if filter(metrics[k].NodeType) { + fmt.Println(csvBodyLine(k, metrics[k], metricNames)) + } + } +}