diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..0175a90 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,101 @@ +run: + deadline: 2m + issues-exit-code: 1 + tests: true + build-tags: [] + skip-dirs: + - vendor$ + - third_party$ + - testdata$ + - examples$ + - Godeps$ + - builtin$ + skip-files: + - ".*\\.pb\\.go$" + - ".*\\.pb\\.gw\\.go$" + - ".*\\.validator\\.pb\\.go$" + modules-download-mode: readonly +output: + format: colored-line-number + print-issued-lines: true + print-linter-name: true +linters: + enable: + - errcheck + - gosimple + - govet + - golint + - gofmt + - ineffassign + - staticcheck + - structcheck + - typecheck + - varcheck + - gocyclo + - maligned + - goconst + - depguard + - misspell + - lll + - nakedret + - prealloc + - gosec + - scopelint + - bodyclose + - stylecheck + - interfacer + - unparam + - deadcode + - gocognit + - goimports + - unused + disable: + - dupl +issues: + exclude-use-default: false + exclude-rules: + - path: _test\.go + linters: + - gocyclo + - errcheck + - dupl + - gosec + - lll + - nakedret + - scopelint +linters-settings: + errcheck: + check-type-assertions: true + check-blank: false + govet: + check-shadowing: false + golint: + min-confidence: 0.8 + set-exit-status: true + gofmt: + simplify: true + gocyclo: + min-complexity: 18 + maligned: + suggest-new: true + goconst: + min-len: 3 + min-occurrences: 3 + depguard: + list-type: blacklist + include-go-root: false + packages: [] + misspell: + locale: US + ignore-words: [] + lll: + line-length: 120 + tab-width: 2 + nakedret: + max-func-lines: 30 + prealloc: + simple: true + range-loops: true + for-loops: false + dupl: + threshold: 150 \ No newline at end of file diff --git a/Makefile b/Makefile index 48dca1c..0193f7d 100644 --- a/Makefile +++ b/Makefile @@ -12,10 +12,13 @@ LD_FLAGS="\ -X 'main.buildCode=`git log --pretty=format:'%H' -n1`' \ " -help: ## Display available make targets - @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[33m%-16s\033[0m %s\n", $$1, $$2}' +## help: Prints this help message +help: + @echo "Commands available" + @sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /' | sort -ca-roots: ## Generate the list of valid CA certificates +## ca-roots: Generate the list of valid CA certificates +ca-roots: @docker run -dit --rm --name ca-roots debian:stable-slim @docker exec --privileged ca-roots sh -c "apt update" @docker exec --privileged ca-roots sh -c "apt install -y ca-certificates" @@ -23,46 +26,57 @@ ca-roots: ## Generate the list of valid CA certificates @docker cp ca-roots:/ca-roots.crt ca-roots.crt @docker stop ca-roots -test: ## Run all tests excluding the vendor dependencies +## docs: Display package documentation on local server +docs: + @echo "Docs available at: http://localhost:8080/pkg/github.com/bryk-io/go-vanity/" + godoc -http=:8080 + +## test: Run all tests excluding the vendor dependencies +test: # Lint helm charts available helm lint helm/* # Static analysis golangci-lint run -v ./... - go-consistent -v ./... # Unit tests go test -race -cover -v ./... -clean: ## Download and compile all dependencies and intermediary products - @-rm -rf vendor +## clean: Verify dependencies and clean intermediary products +clean: + go clean go mod tidy go mod verify -updates: ## List available updates for direct dependencies +## updates: List available updates for direct dependencies +updates: # https://github.com/golang/go/wiki/Modules#how-to-upgrade-and-downgrade-dependencies go list -u -f '{{if (and (not (or .Main .Indirect)) .Update)}}{{.Path}}: {{.Version}} -> {{.Update.Version}}{{end}}' -m all 2> /dev/null -install: ## Install the binary to GOPATH and keep cached all compiled artifacts +## install: Install the binary to GOPATH and keep cached all compiled artifacts +install: @go build -v -ldflags $(LD_FLAGS) -i -o ${GOPATH}/bin/$(BINARY_NAME) -build: ## Build for the current architecture in use, intended for devevelopment +## build: Build for the current architecture in use, intended for devevelopment +build: go build -v -ldflags $(LD_FLAGS) -o $(BINARY_NAME) -build-for: ## Build the availabe binaries for the specified 'os' and 'arch' +## build-for: Build the availabe binaries for the specified 'os' and 'arch' +build-for: CGO_ENABLED=0 GOOS=$(os) GOARCH=$(arch) \ go build -v -ldflags $(LD_FLAGS) \ -o $(dest)$(BINARY_NAME)_$(VERSION_TAG)_$(os)_$(arch)$(suffix) -docker: ## Build docker image - @-rm $(BINARY_NAME)_$(VERSION_TAG)_linux_amd64 ca-roots.crt - @make ca-roots +## docker: Build docker image +docker: + @-rm $(BINARY_NAME)_$(VERSION_TAG)_linux_amd64 @make build-for os=linux arch=amd64 @-docker rmi $(DOCKER_IMAGE):$(VERSION_TAG) @docker build --build-arg VERSION_TAG="$(VERSION_TAG)" --rm -t $(DOCKER_IMAGE):$(VERSION_TAG) . @-rm $(BINARY_NAME)_$(VERSION_TAG)_linux_amd64 ca-roots.crt -release: ## Prepare the artifacts for a new release +## release: Prepare artifacts for a new tagged release +release: @-rm -rf release-$(VERSION_TAG) mkdir release-$(VERSION_TAG) make build-for os=linux arch=amd64 dest=release-$(VERSION_TAG)/ diff --git a/config/main.go b/config.go similarity index 72% rename from config/main.go rename to config.go index 31d815c..e7f43f2 100644 --- a/config/main.go +++ b/config.go @@ -1,4 +1,4 @@ -package config +package main import ( "encoding/json" @@ -7,12 +7,18 @@ import ( "strings" ) +// VCS represents a specific version control system to match +// a path against. type VCS int const ( + // GIT source control management (https://git-scm.com/). GIT VCS = iota + // HG stands for Mercurial (https://www.mercurial-scm.org/wiki/HgSubversion). HG + // SVN stands for Subversion (https://subversion.apache.org/). SVN + // BZR stands for Bazaar (https://bazaar.canonical.com/en/). BZR ) @@ -32,6 +38,7 @@ func (v *VCS) fromString(s string) error { return nil } +// String return a proper textual representation for the VCS code. func (v VCS) String() string { names := [...]string{ "git", @@ -45,10 +52,12 @@ func (v VCS) String() string { return names[v] } +// MarshalJSON provides custom JSON encoding. func (v VCS) MarshalJSON() ([]byte, error) { return json.Marshal(v.String()) } +// UnmarshalJSON provides custom JSON decoding. func (v VCS) UnmarshalJSON(b []byte) error { var s string if err := json.Unmarshal(b, &s); err != nil { @@ -57,10 +66,13 @@ func (v VCS) UnmarshalJSON(b []byte) error { return v.fromString(s) } +// MarshalYAML provides custom YAML encoding. +// nolint: unparam func (v VCS) MarshalYAML() (interface{}, error) { return v.String(), nil } +// UnmarshalYAML provides custom YAML decoding. func (v VCS) UnmarshalYAML(unmarshal func(interface{}) error) error { var s string if err := unmarshal(&s); err != nil { @@ -69,8 +81,9 @@ func (v VCS) UnmarshalYAML(unmarshal func(interface{}) error) error { return v.fromString(s) } -// Configuration is compatible with https://github.com/GoogleCloudPlatform/govanityurls -type Server struct { +// Configuration provides the required server parameters. +// Compatible with https://github.com/GoogleCloudPlatform/govanityurls +type Configuration struct { // Host name to use in meta tags. Host string `json:"host,omitempty" yaml:"host,omitempty"` @@ -82,6 +95,7 @@ type Server struct { Paths map[string]Path `json:"paths" yaml:"paths"` } +// Path matches a specific vanity URL to an external version control system. type Path struct { // Root URL of the repository as it would appear in go-import meta tag. Repo string `json:"repo" yaml:"repo"` @@ -96,7 +110,7 @@ type Path struct { Display string `json:"display,omitempty" yaml:"display,omitempty"` } -// SCL return a properly formatted "Source Code Link" for the path +// SCL return a properly formatted "Source Code Link" for the path. // https://github.com/golang/gddo/wiki/Source-Code-Links func (p Path) SCL() string { if p.Display != "" { @@ -111,8 +125,9 @@ func (p Path) SCL() string { return fmt.Sprintf("%s %s/tree{/dir} %s/blob{/dir}/{file}#L{line}", p.Repo, p.Repo, p.Repo) } -func New() *Server { - return &Server{ +// NewServerConfig returns an empty server configuration instance. +func NewServerConfig() *Configuration { + return &Configuration{ CacheMaxAge: 3600, Paths: make(map[string]Path), } diff --git a/handler.go b/handler.go index a7f7125..3920a6a 100644 --- a/handler.go +++ b/handler.go @@ -5,8 +5,6 @@ import ( "errors" "fmt" "strings" - - "github.com/bryk-io/go-vanity/config" ) type repo struct { @@ -23,11 +21,11 @@ type data struct { } type handler struct { - conf *config.Server + conf *Configuration data *data } -func newHandler(conf *config.Server) *handler { +func newHandler(conf *Configuration) *handler { h := new(handler) h.conf = conf h.data = &data{ @@ -68,4 +66,4 @@ func (h *handler) getRepo(path string) ([]byte, error) { func (h *handler) cache() string { return fmt.Sprintf("public, max-age=%d", h.conf.CacheMaxAge) -} \ No newline at end of file +} diff --git a/main.go b/main.go index a2b1c8d..7ef9a13 100644 --- a/main.go +++ b/main.go @@ -1,3 +1,5 @@ +// Package main provides the go-vanity CLI tool, a basic server implementation +// capable of providing custom URLs to be used for the standard go tools. package main import ( @@ -7,13 +9,15 @@ import ( "io/ioutil" "net/http" "os" + "path/filepath" "strconv" "strings" - "github.com/bryk-io/go-vanity/config" "gopkg.in/yaml.v2" ) +var cacheValue string + func main() { // Validate arguments file, port := getParameters() @@ -23,14 +27,14 @@ func main() { } // Read configuration file - contents, err := ioutil.ReadFile(file) + contents, err := ioutil.ReadFile(filepath.Clean(file)) if err != nil { fmt.Println("failed to read configuration file: ", err) os.Exit(-1) } // Decode configuration file - conf := config.New() + conf := NewServerConfig() if strings.HasSuffix(file, ".yaml") || strings.HasSuffix(file, ".yml") { if err := yaml.Unmarshal(contents, conf); err != nil { fmt.Println("failed to decode YAML configuration file: ", err) @@ -50,45 +54,40 @@ func main() { // Prepare server mux h := newHandler(conf) + cacheValue = h.cache() mux := http.NewServeMux() mux.HandleFunc("/api/ping", func(res http.ResponseWriter, req *http.Request) { - res.WriteHeader(http.StatusOK) + setHeaders(res, "text/plain; charset=utf-8", http.StatusOK) _, _ = res.Write([]byte("pong")) }) mux.HandleFunc("/api/version", func(res http.ResponseWriter, req *http.Request) { js, _ := json.MarshalIndent(versionInfo(), "", " ") - res.Header().Add("Content-Type", "application/json") - res.Header().Add("Cache-Control", h.cache()) - res.WriteHeader(http.StatusOK) + setHeaders(res, "application/json", http.StatusOK) _, _ = res.Write(js) }) mux.HandleFunc("/api/conf", func(res http.ResponseWriter, req *http.Request) { js, _ := json.MarshalIndent(conf, "", " ") - res.Header().Add("Content-Type", "application/json") - res.Header().Add("Cache-Control", h.cache()) - res.WriteHeader(http.StatusOK) + setHeaders(res, "application/json", http.StatusOK) _, _ = res.Write(js) }) mux.HandleFunc("/index.html", func(res http.ResponseWriter, req *http.Request) { index, err := h.getIndex() if err != nil { - res.WriteHeader(http.StatusInternalServerError) + setHeaders(res, "text/plain; charset=utf-8", http.StatusInternalServerError) _, _ = res.Write([]byte(err.Error())) return } - res.Header().Add("Cache-Control", h.cache()) - res.WriteHeader(http.StatusOK) + setHeaders(res, "text/html; charset=utf-8", http.StatusOK) _, _ = res.Write(index) }) mux.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) { repo, err := h.getRepo(strings.TrimSuffix(req.URL.Path, "/")) if err != nil { - res.WriteHeader(http.StatusNotFound) + setHeaders(res, "text/plain; charset=utf-8", http.StatusNotFound) _, _ = res.Write([]byte(err.Error())) return } - res.Header().Add("Cache-Control", h.cache()) - res.WriteHeader(http.StatusOK) + setHeaders(res, "text/html; charset=utf-8", http.StatusOK) _, _ = res.Write(repo) }) @@ -129,3 +128,12 @@ func getParameters() (string, int) { } return file, port } + +func setHeaders(res http.ResponseWriter, ct string, code int) { + res.Header().Add("Cache-Control", cacheValue) + res.Header().Add("Content-Type", ct) + res.Header().Add("X-Content-Type-Options", "nosniff") + res.Header().Add("X-Go-Vanity-Server-Build", buildCode) + res.Header().Add("X-Go-Vanity-Server-Version", coreVersion) + res.WriteHeader(code) +} diff --git a/template.go b/template.go index 8829855..7bf2f70 100644 --- a/template.go +++ b/template.go @@ -20,5 +20,5 @@ var repoTpl = template.Must(template.New("repo").Parse(` -Nothing to see here. -`)) \ No newline at end of file +{{.Import}} ({{.VCS}}) +`)) diff --git a/version.go b/version.go index 9e7d51f..a5c0111 100644 --- a/version.go +++ b/version.go @@ -8,8 +8,8 @@ import ( ) var ( - coreVersion string - buildCode string + coreVersion string + buildCode string buildTimestamp string ) @@ -17,8 +17,8 @@ func versionInfo() map[string]string { var components = map[string]string{ "version": coreVersion, "build_code": buildCode, - "os": fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), - "go": runtime.Version(), + "os": fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), + "go": runtime.Version(), } if buildTimestamp != "" { st, err := strconv.ParseInt(buildTimestamp, 10, 64)