diff --git a/.goosarch b/.goosarch index 1a617034..48b5a268 100644 --- a/.goosarch +++ b/.goosarch @@ -6,6 +6,5 @@ linux/arm linux/arm64 linux/amd64 linux/386 -solaris/amd64 windows/amd64 windows/386 diff --git a/.travis.yml b/.travis.yml index 7462498e..068af19e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -43,9 +43,6 @@ jobs: - cross/reg-linux-386 - cross/reg-linux-386.md5 - cross/reg-linux-386.sha256 - - cross/reg-solaris-amd64 - - cross/reg-solaris-amd64.md5 - - cross/reg-solaris-amd64.sha256 - cross/reg-windows-amd64 - cross/reg-windows-amd64.md5 - cross/reg-windows-amd64.sha256 diff --git a/Dockerfile b/Dockerfile index 5c88f231..7d6bcedc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,5 +28,10 @@ FROM scratch COPY --from=builder /usr/bin/reg /usr/bin/reg COPY --from=builder /etc/ssl/certs/ /etc/ssl/certs +COPY server/static /src/static +COPY server/templates /src/templates + +WORKDIR /src + ENTRYPOINT [ "reg" ] CMD [ "--help" ] diff --git a/Makefile b/Makefile index 1f149729..8d241b1c 100644 --- a/Makefile +++ b/Makefile @@ -43,7 +43,7 @@ static: ## Builds a static executable -tags "$(BUILDTAGS) static_build" \ ${GO_LDFLAGS_STATIC} -o $(NAME) . -all: clean build fmt lint test staticcheck vet install build-server ## Runs a clean, build, fmt, lint, test, staticcheck, vet and install +all: clean build fmt lint test staticcheck vet install ## Runs a clean, build, fmt, lint, test, staticcheck, vet and install .PHONY: fmt fmt: ## Verifies all files have been `gofmt`ed @@ -207,20 +207,6 @@ snakeoil: ## Update snakeoil certs for testing mv $(CURDIR)/key.pem $(CURDIR)/testutils/snakeoil/key.pem mv $(CURDIR)/cert.pem $(CURDIR)/testutils/snakeoil/cert.pem -.PHONY: build-server -build-server: $(NAME)-server ## Builds a dynamic executable for reg-server - -$(NAME)-server: $(wildcard */*.go) VERSION.txt - @echo "+ $@" - $(GO) build -tags "$(BUILDTAGS)" ${GO_LDFLAGS} -o $(NAME)-server ./server/... - -.PHONY: static-server -static-server: ## Builds a static reg-server executable - @echo "+ $@" - CGO_ENABLED=0 $(GO) build \ - -tags "$(BUILDTAGS) static_build" \ - ${GO_LDFLAGS_STATIC} -o $(NAME)-server ./server - .PHONY: help help: @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' diff --git a/server/handlers.go b/handlers.go similarity index 93% rename from server/handlers.go rename to handlers.go index deb49b94..3a27acdf 100644 --- a/server/handlers.go +++ b/handlers.go @@ -3,11 +3,13 @@ package main import ( "encoding/json" "fmt" + "html/template" "net/http" "net/url" "os" "path/filepath" "strings" + "sync" "time" "github.com/genuinetools/reg/clair" @@ -17,8 +19,10 @@ import ( ) type registryController struct { - reg *registry.Registry - cl *clair.Clair + reg *registry.Registry + cl *clair.Clair + l sync.Mutex + tmpl *template.Template } type v1Compatibility struct { @@ -44,7 +48,9 @@ type AnalysisResult struct { } func (rc *registryController) repositories(staticDir string) error { - updating = true + rc.l.Lock() + defer rc.l.Unlock() + logrus.Info("fetching catalog") result := AnalysisResult{ @@ -52,7 +58,7 @@ func (rc *registryController) repositories(staticDir string) error { LastUpdated: time.Now().Local().Format(time.RFC1123), } - repoList, err := r.Catalog("") + repoList, err := rc.reg.Catalog("") if err != nil { return fmt.Errorf("getting catalog failed: %v", err) } @@ -67,7 +73,7 @@ func (rc *registryController) repositories(staticDir string) error { result.Repositories = append(result.Repositories, r) } - // parse & execute the template + // Parse & execute the template. logrus.Info("executing the template repositories") path := filepath.Join(staticDir, "index.html") @@ -81,12 +87,11 @@ func (rc *registryController) repositories(staticDir string) error { } defer f.Close() - if err := tmpl.ExecuteTemplate(f, "repositories", result); err != nil { + if err := rc.tmpl.ExecuteTemplate(f, "repositories", result); err != nil { f.Close() return fmt.Errorf("execute template repositories failed: %v", err) } - updating = false return nil } @@ -179,7 +184,7 @@ func (rc *registryController) tagsHandler(w http.ResponseWriter, r *http.Request result.Repositories = append(result.Repositories, rp) } - if err := tmpl.ExecuteTemplate(w, "tags", result); err != nil { + if err := rc.tmpl.ExecuteTemplate(w, "tags", result); err != nil { logrus.WithFields(logrus.Fields{ "func": "tags", "URL": r.URL, @@ -245,7 +250,7 @@ func (rc *registryController) vulnerabilitiesHandler(w http.ResponseWriter, r *h return } - if err := tmpl.ExecuteTemplate(w, "vulns", result); err != nil { + if err := rc.tmpl.ExecuteTemplate(w, "vulns", result); err != nil { logrus.WithFields(logrus.Fields{ "func": "vulnerabilities", "URL": r.URL, diff --git a/main.go b/main.go index 93a96dae..bf6daeef 100644 --- a/main.go +++ b/main.go @@ -4,7 +4,10 @@ import ( "context" "flag" "fmt" + "os" + "os/signal" "strings" + "syscall" "time" "github.com/genuinetools/pkg/cli" @@ -43,6 +46,7 @@ func main() { &listCommand{}, &manifestCommand{}, &removeCommand{}, + &serverCommand{}, &tagsCommand{}, &vulnsCommand{}, } @@ -69,6 +73,20 @@ func main() { // Set the before function. p.Before = func(ctx context.Context) error { + // On ^C, or SIGTERM handle exit. + signals := make(chan os.Signal, 0) + signal.Notify(signals, os.Interrupt) + signal.Notify(signals, syscall.SIGTERM) + var cancel context.CancelFunc + ctx, cancel = context.WithCancel(ctx) + go func() { + for sig := range signals { + cancel() + logrus.Infof("Received %s, exiting.", sig.String()) + os.Exit(0) + } + }() + // Set the log level. if debug { logrus.SetLevel(logrus.DebugLevel) diff --git a/server.go b/server.go new file mode 100644 index 00000000..5620ecda --- /dev/null +++ b/server.go @@ -0,0 +1,176 @@ +package main + +import ( + "context" + "flag" + "fmt" + "html/template" + "net/http" + "os" + "path/filepath" + "strings" + "time" + + "github.com/genuinetools/reg/clair" + "github.com/gorilla/mux" + wordwrap "github.com/mitchellh/go-wordwrap" + "github.com/sirupsen/logrus" +) + +const serverHelp = `Run a static UI server for a registry.` + +func (cmd *serverCommand) Name() string { return "server" } +func (cmd *serverCommand) Args() string { return "[OPTIONS]" } +func (cmd *serverCommand) ShortHelp() string { return serverHelp } +func (cmd *serverCommand) LongHelp() string { return serverHelp } +func (cmd *serverCommand) Hidden() bool { return false } + +func (cmd *serverCommand) Register(fs *flag.FlagSet) { + fs.DurationVar(&cmd.interval, "interval", time.Hour, "interval to generate new index.html's at") + + fs.StringVar(&cmd.registryServer, "registry", "", "URL to the private registry (ex. r.j3ss.co)") + fs.StringVar(&cmd.registryServer, "r", "", "URL to the private registry (ex. r.j3ss.co)") + + fs.StringVar(&cmd.clairServer, "clair", "", "url to clair instance") + + fs.StringVar(&cmd.cert, "cert", "", "path to ssl cert") + fs.StringVar(&cmd.key, "key", "", "path to ssl key") + fs.StringVar(&cmd.port, "port", "8080", "port for server to run on") + + fs.BoolVar(&cmd.once, "once", false, "generate an output once and then exit") +} + +type serverCommand struct { + interval time.Duration + registryServer string + clairServer string + + once bool + + cert string + key string + port string +} + +func (cmd *serverCommand) Run(ctx context.Context, args []string) error { + // Create the registry client. + r, err := createRegistryClient(cmd.registryServer) + if err != nil { + return err + } + + // Create the registry controller for the handlers. + rc := registryController{ + reg: r, + } + + // Create a clair client if the user passed in a server address. + if len(cmd.clairServer) < 1 { + rc.cl, err = clair.New(cmd.clairServer, clair.Opt{ + Insecure: insecure, + Debug: debug, + Timeout: timeout, + }) + if err != nil { + return fmt.Errorf("creation of clair client at %s failed: %v", cmd.clairServer, err) + } + } + + // Get the path to the static directory. + wd, err := os.Getwd() + if err != nil { + return err + } + staticDir := filepath.Join(wd, "static") + templateDir := filepath.Join(staticDir, "../templates") + + // Make sure all the paths exist. + tmplPaths := []string{ + staticDir, + filepath.Join(templateDir, "vulns.html"), + filepath.Join(templateDir, "repositories.html"), + filepath.Join(templateDir, "tags.html"), + } + for _, path := range tmplPaths { + if _, err := os.Stat(path); os.IsNotExist(err) { + return fmt.Errorf("template %s not found", path) + } + } + + funcMap := template.FuncMap{ + "trim": func(s string) string { + return wordwrap.WrapString(s, 80) + }, + "color": func(s string) string { + switch s = strings.ToLower(s); s { + case "high": + return "danger" + case "critical": + return "danger" + case "defcon1": + return "danger" + case "medium": + return "warning" + case "low": + return "info" + case "negligible": + return "info" + case "unknown": + return "default" + default: + return "default" + } + }, + } + + rc.tmpl = template.Must(template.New("").Funcs(funcMap).ParseGlob(templateDir + "/*.html")) + + // Create the initial index. + logrus.Info("creating initial static index") + if err := rc.repositories(staticDir); err != nil { + return fmt.Errorf("creating index failed: %v", err) + } + + if cmd.once { + logrus.Info("output generated, exiting...") + return nil + } + + ticker := time.NewTicker(cmd.interval) + go func() { + // Create more indexes every X minutes based off interval. + for range ticker.C { + logrus.Info("creating timer based static index") + if err := rc.repositories(staticDir); err != nil { + logrus.Warnf("creating static index failed: %v", err) + } + } + }() + + // Create mux server. + mux := mux.NewRouter() + mux.UseEncodedPath() + + // Static files handler. + staticHandler := http.FileServer(http.Dir(staticDir)) + mux.HandleFunc("/repo/{repo}/tags", rc.tagsHandler) + mux.HandleFunc("/repo/{repo}/tags/", rc.tagsHandler) + mux.HandleFunc("/repo/{repo}/tag/{tag}", rc.vulnerabilitiesHandler) + mux.HandleFunc("/repo/{repo}/tag/{tag}/", rc.vulnerabilitiesHandler) + mux.HandleFunc("/repo/{repo}/tag/{tag}/vulns", rc.vulnerabilitiesHandler) + mux.HandleFunc("/repo/{repo}/tag/{tag}/vulns/", rc.vulnerabilitiesHandler) + mux.HandleFunc("/repo/{repo}/tag/{tag}/vulns.json", rc.vulnerabilitiesHandler) + mux.PathPrefix("/static/").Handler(http.StripPrefix("/static/", staticHandler)) + mux.Handle("/", staticHandler) + + // Set up the server. + server := &http.Server{ + Addr: ":" + cmd.port, + Handler: mux, + } + logrus.Infof("Starting server on port %q", cmd.port) + if len(cmd.cert) > 0 && len(cmd.key) > 0 { + return server.ListenAndServeTLS(cmd.cert, cmd.key) + } + return server.ListenAndServe() +} diff --git a/server/Dockerfile b/server/Dockerfile deleted file mode 100644 index d2ac3f28..00000000 --- a/server/Dockerfile +++ /dev/null @@ -1,34 +0,0 @@ -FROM golang:alpine as builder -MAINTAINER Jessica Frazelle - -ENV PATH /go/bin:/usr/local/go/bin:$PATH -ENV GOPATH /go - -RUN apk add --no-cache \ - ca-certificates - -RUN set -x \ - && apk add --no-cache --virtual .build-deps \ - git \ - gcc \ - libc-dev \ - libgcc \ - && go get -v github.com/genuinetools/reg \ - && cd /go/src/github.com/genuinetools/reg \ - && CGO_ENABLED=0 go build -a -tags netgo -ldflags '-extldflags "-static"' -o /usr/bin/reg-server ./server \ - && apk del .build-deps \ - && rm -rf /go \ - && echo "Build complete." - -FROM scratch - -COPY --from=builder /usr/bin/reg-server /usr/bin/reg-server -COPY --from=builder /etc/ssl/certs/ /etc/ssl/certs - -COPY static /src/static -COPY templates /src/templates - -WORKDIR /src - -ENTRYPOINT [ "reg-server" ] -CMD [ "--help" ] diff --git a/server/server.go b/server/server.go deleted file mode 100644 index 17cc63b5..00000000 --- a/server/server.go +++ /dev/null @@ -1,250 +0,0 @@ -package main - -import ( - "context" - "flag" - "html/template" - "net/http" - "os" - "path/filepath" - "strings" - "time" - - "github.com/genuinetools/pkg/cli" - "github.com/genuinetools/reg/clair" - "github.com/genuinetools/reg/registry" - "github.com/genuinetools/reg/repoutils" - "github.com/genuinetools/reg/version" - "github.com/gorilla/mux" - wordwrap "github.com/mitchellh/go-wordwrap" - "github.com/sirupsen/logrus" -) - -var ( - insecure bool - forceNonSSL bool - skipPing bool - - interval time.Duration - timeout time.Duration - - username string - password string - registryServer string - clairServer string - - once bool - - cert string - key string - port string - - debug bool - - updating bool - r *registry.Registry - cl *clair.Clair - tmpl *template.Template -) - -func main() { - // Create a new cli program. - p := cli.NewProgram() - p.Name = "reg-server" - p.Description = "Docker registry v2 static UI server" - - // Set the GitCommit and Version. - p.GitCommit = version.GITCOMMIT - p.Version = version.VERSION - - // Setup the global flags. - p.FlagSet = flag.NewFlagSet("global", flag.ExitOnError) - p.FlagSet.BoolVar(&insecure, "insecure", false, "do not verify tls certificates") - p.FlagSet.BoolVar(&insecure, "k", false, "do not verify tls certificates") - - p.FlagSet.BoolVar(&forceNonSSL, "force-non-ssl", false, "force allow use of non-ssl") - p.FlagSet.BoolVar(&forceNonSSL, "f", false, "force allow use of non-ssl") - - p.FlagSet.BoolVar(&skipPing, "skip-ping", false, "skip pinging the registry while establishing connection") - - p.FlagSet.DurationVar(&interval, "interval", time.Hour, "interval to generate new index.html's at") - p.FlagSet.DurationVar(&timeout, "timeout", time.Minute, "timeout for HTTP requests") - - p.FlagSet.StringVar(&username, "username", "", "username for the registry") - p.FlagSet.StringVar(&username, "u", "", "username for the registry") - - p.FlagSet.StringVar(&password, "password", "", "password for the registry") - p.FlagSet.StringVar(&password, "p", "", "password for the registry") - - p.FlagSet.StringVar(®istryServer, "registry", "", "URL to the private registry (ex. r.j3ss.co)") - p.FlagSet.StringVar(®istryServer, "r", "", "URL to the private registry (ex. r.j3ss.co)") - - p.FlagSet.StringVar(&clairServer, "clair", "", "url to clair instance") - - p.FlagSet.StringVar(&cert, "cert", "", "path to ssl cert") - p.FlagSet.StringVar(&key, "key", "", "path to ssl key") - p.FlagSet.StringVar(&port, "port", "8080", "port for server to run on") - - p.FlagSet.BoolVar(&once, "once", false, "generate an output once and then exit") - - p.FlagSet.BoolVar(&debug, "d", false, "enable debug logging") - - // Set the before function. - p.Before = func(ctx context.Context) error { - // Set the log level. - if debug { - logrus.SetLevel(logrus.DebugLevel) - } - - return nil - } - - // Set the main program action. - p.Action = func(ctx context.Context) error { - auth, err := repoutils.GetAuthConfig(username, password, registryServer) - if err != nil { - logrus.Fatal(err) - } - - // Create the registry client. - r, err = registry.New(auth, registry.Opt{ - Insecure: insecure, - Debug: debug, - SkipPing: skipPing, - Timeout: timeout, - }) - if err != nil { - logrus.Fatal(err) - } - - // create a clair instance if needed - if len(clairServer) < 1 { - cl, err = clair.New(clairServer, clair.Opt{ - Insecure: insecure, - Debug: debug, - Timeout: timeout, - }) - if err != nil { - logrus.Warnf("creation of clair failed: %v", err) - } - } - - // get the path to the static directory - wd, err := os.Getwd() - if err != nil { - logrus.Fatal(err) - } - staticDir := filepath.Join(wd, "static") - - // create the template - templateDir := filepath.Join(staticDir, "../templates") - - // make sure all the templates exist - vulns := filepath.Join(templateDir, "vulns.html") - if _, err := os.Stat(vulns); os.IsNotExist(err) { - logrus.Fatalf("Template %s not found", vulns) - } - layout := filepath.Join(templateDir, "repositories.html") - if _, err := os.Stat(layout); os.IsNotExist(err) { - logrus.Fatalf("Template %s not found", layout) - } - tags := filepath.Join(templateDir, "tags.html") - if _, err := os.Stat(tags); os.IsNotExist(err) { - logrus.Fatalf("Template %s not found", tags) - } - - funcMap := template.FuncMap{ - "trim": func(s string) string { - return wordwrap.WrapString(s, 80) - }, - "color": func(s string) string { - switch s = strings.ToLower(s); s { - case "high": - return "danger" - case "critical": - return "danger" - case "defcon1": - return "danger" - case "medium": - return "warning" - case "low": - return "info" - case "negligible": - return "info" - case "unknown": - return "default" - default: - return "default" - } - }, - } - - tmpl = template.Must(template.New("").Funcs(funcMap).ParseGlob(templateDir + "/*.html")) - - rc := registryController{ - reg: r, - cl: cl, - } - - // create the initial index - logrus.Info("creating initial static index") - if err := rc.repositories(staticDir); err != nil { - logrus.Fatalf("Error creating index: %v", err) - } - - if once { - logrus.Info("Output generated") - return nil - } - - ticker := time.NewTicker(interval) - - go func() { - // create more indexes every X minutes based off interval - for range ticker.C { - if !updating { - logrus.Info("creating timer based static index") - if err := rc.repositories(staticDir); err != nil { - logrus.Warnf("creating static index failed: %v", err) - updating = false - } - } else { - logrus.Warnf("skipping timer based static index update for %s", interval.String()) - } - } - }() - - // create mux server - mux := mux.NewRouter() - mux.UseEncodedPath() - - // static files handler - staticHandler := http.FileServer(http.Dir(staticDir)) - mux.HandleFunc("/repo/{repo}/tags", rc.tagsHandler) - mux.HandleFunc("/repo/{repo}/tags/", rc.tagsHandler) - mux.HandleFunc("/repo/{repo}/tag/{tag}", rc.vulnerabilitiesHandler) - mux.HandleFunc("/repo/{repo}/tag/{tag}/", rc.vulnerabilitiesHandler) - mux.HandleFunc("/repo/{repo}/tag/{tag}/vulns", rc.vulnerabilitiesHandler) - mux.HandleFunc("/repo/{repo}/tag/{tag}/vulns/", rc.vulnerabilitiesHandler) - mux.HandleFunc("/repo/{repo}/tag/{tag}/vulns.json", rc.vulnerabilitiesHandler) - mux.PathPrefix("/static/").Handler(http.StripPrefix("/static/", staticHandler)) - mux.Handle("/", staticHandler) - - // set up the server - server := &http.Server{ - Addr: ":" + port, - Handler: mux, - } - logrus.Infof("Starting server on port %q", port) - if len(cert) > 0 && len(key) > 0 { - logrus.Fatal(server.ListenAndServeTLS(cert, key)) - } else { - logrus.Fatal(server.ListenAndServe()) - } - - return nil - } - - // Run our program. - p.Run() -} diff --git a/vulns.go b/vulns.go index 21448cc2..24202410 100644 --- a/vulns.go +++ b/vulns.go @@ -60,7 +60,7 @@ func (cmd *vulnsCommand) Run(ctx context.Context, args []string) error { Insecure: insecure, }) if err != nil { - return err + return fmt.Errorf("creation of clair client at %s failed: %v", cmd.clairServer, err) } // Get the vulnerability report.