1
0
mirror of https://github.com/quay/quay.git synced 2026-01-27 18:42:52 +03:00
Files
quay/config-tool/pkg/lib/editor/editor.go

171 lines
5.8 KiB
Go

// @title Config Tool Editor API
// @version 0.0
// @contact.name Jonathan King
// @contact.email joking@redhat.com
// @BasePath /api/v1
// @securityDefinitions.basic BasicAuth
// @schemes http
package editor
import (
"crypto/tls"
"errors"
"io/ioutil"
"mime"
"net/http"
"os"
log "github.com/sirupsen/logrus"
auth "github.com/abbot/go-http-auth"
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
_ "github.com/quay/quay/config-tool/docs"
httpSwagger "github.com/swaggo/http-swagger"
"golang.org/x/crypto/bcrypt"
)
// ServerOptions holds information regarding the set up of the config-tool server
type ServerOptions struct {
username string
password string
port string
configPath string
staticContentPath string
operatorEndpoint string
readOnlyFieldGroups []string
podNamespace string // Optional
podName string // Optional
publicKeyPath string
privateKeyPath string
}
// ConfigBundle is the current state of the config bundle on the server. It may read from a path on disk and then edited through the API.
type ConfigBundle struct {
Config map[string]interface{} `json:"config.yaml" yaml:"config.yaml"`
Certificates map[string][]byte `json:"certs,omitempty" yaml:"certs,omitempty"`
ManagedFieldGroups []string `json:"managedFieldGroups,omitempty" yaml:"managedFieldGroups,omitempty"`
}
// RunConfigEditor runs the configuration editor server.
func RunConfigEditor(password, configPath, operatorEndpoint string, readOnlyFieldGroups []string) {
// FIX THIS
publicKeyPath := os.Getenv("CONFIG_TOOL_PUBLIC_KEY")
privateKeyPath := os.Getenv("CONFIG_TOOL_PRIVATE_KEY")
staticContentPath, exists := os.LookupEnv("CONFIG_EDITOR_STATIC_CONTENT_PATH")
if !exists {
staticContentPath = "pkg/lib/editor/static"
}
podNamespace := os.Getenv("MY_POD_NAMESPACE")
podName := os.Getenv("MY_POD_NAME")
if operatorEndpoint != "" && (podNamespace == "" || podName == "") {
panic("If you would like to use operator reconfiguration features you must specify your namespace and pod name") // FIXME (jonathan) - come up with better error message
}
if readOnlyFieldGroups == nil {
readOnlyFieldGroups = []string{}
}
opts := &ServerOptions{
username: "quayconfig", // FIXME (jonathan) - add option to change username
password: password,
port: "8080", // FIXME (jonathan) - add option to change port
configPath: configPath,
staticContentPath: staticContentPath,
operatorEndpoint: operatorEndpoint,
readOnlyFieldGroups: readOnlyFieldGroups,
podNamespace: podNamespace,
podName: podName,
publicKeyPath: publicKeyPath,
privateKeyPath: privateKeyPath,
}
hashed, _ := bcrypt.GenerateFromPassword([]byte(opts.password), 5)
authenticator := auth.NewBasicAuthenticator(opts.username, func(user, realm string) string {
if user == opts.username {
return string(hashed)
}
return ""
})
mime.AddExtensionType(".css", "text/css; charset=utf-8")
mime.AddExtensionType(".js", "application/javascript; charset=utf-8")
if opts.operatorEndpoint != "" {
log.Printf("Using Operator Endpoint: " + opts.operatorEndpoint)
}
r := chi.NewRouter()
if debug := os.Getenv("DEBUGLOG"); debug != "" {
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
}
// Function handlers
r.Get("/", rootHandler(opts))
r.Get("/api/v1/config", auth.JustCheck(authenticator, getMountedConfigBundle(opts)))
r.Post("/api/v1/config/validate", auth.JustCheck(authenticator, validateConfigBundle(opts)))
r.Post("/api/v1/config/download", auth.JustCheck(authenticator, downloadConfigBundle(opts)))
r.Post("/api/v1/config/operator", auth.JustCheck(authenticator, commitToOperator(opts)))
r.Get("/swagger/*", httpSwagger.Handler(
httpSwagger.URL("/docs/swagger.json"), // FIXME(jonathan) - This can eventually be changed to the github link to this file.
))
// File handlers
r.Get("/static/*", func(w http.ResponseWriter, r *http.Request) {
fs := http.StripPrefix("/static/", http.FileServer(http.Dir(opts.staticContentPath)))
fs.ServeHTTP(w, r)
})
r.Get("/docs/*", func(w http.ResponseWriter, r *http.Request) {
fs := http.StripPrefix("/docs/", http.FileServer(http.Dir("docs")))
fs.ServeHTTP(w, r)
})
// Create server base
s := &http.Server{
Addr: ":" + opts.port,
Handler: r,
}
// Try to load TLS
tlsConfig, err := loadTLS(opts.publicKeyPath, opts.privateKeyPath)
if err != nil {
log.Warningf("An error occurred loading TLS: " + err.Error() + ". Server falling back to HTTP.")
log.Infof("Running the configuration editor with HTTP on port %v with username %s", opts.port, opts.username)
log.Fatal(s.ListenAndServe())
} else {
s.TLSConfig = tlsConfig
log.Infof("Running the configuration editor with HTTPS on port %v with username %s", opts.port, opts.username)
log.Fatal(s.ListenAndServeTLS("", ""))
}
}
// tlsConfig will attempt to create a tls config given a public and private key. It returns an error if it fails to create a Config.
func loadTLS(publicKeyPath, privateKeyPath string) (*tls.Config, error) {
if publicKeyPath == "" {
return nil, errors.New("No public key provided for HTTPS")
}
if privateKeyPath == "" {
return nil, errors.New("No private key provided for HTTPS")
}
crt, err := ioutil.ReadFile(publicKeyPath)
if err != nil {
return nil, errors.New("Could not open public key: " + publicKeyPath)
}
key, err := ioutil.ReadFile(privateKeyPath)
if err != nil {
return nil, errors.New("Could not open private key: " + privateKeyPath)
}
cert, err := tls.X509KeyPair(crt, key)
if err != nil {
return nil, errors.New("Could not load X509 key pair: " + err.Error())
}
return &tls.Config{
Certificates: []tls.Certificate{cert}}, nil
}