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/controllers.go
Jonathan King f013a27b6f reconfigure: Remove ca-bundle.crt and service-ca.crt (PROJQUAY-5233) (#2231)
- We need to strip out the cluster provided certs from the reconfigure rrequest to avoid duplication.
This resolves JSON encoding/decoding issues found in passing this cert through the config editor
2023-09-12 17:04:35 +00:00

323 lines
9.5 KiB
Go

package editor
import (
"archive/tar"
"bytes"
"compress/gzip"
"encoding/json"
"io/ioutil"
"net/http"
"path"
"strings"
jsoniter "github.com/json-iterator/go"
"github.com/quay/quay/config-tool/pkg/lib/config"
conf "github.com/quay/quay/config-tool/pkg/lib/config"
"github.com/quay/quay/config-tool/pkg/lib/shared"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v3"
)
func rootHandler(opts *ServerOptions) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
p := opts.staticContentPath + "/index.html"
if len(opts.operatorEndpoint) > 0 {
http.SetCookie(w, &http.Cookie{Name: "QuayOperatorEndpoint", Value: opts.operatorEndpoint})
}
http.SetCookie(w, &http.Cookie{Name: "QuayReadOnlyFieldGroups", Value: strings.Join(opts.readOnlyFieldGroups, ",")})
http.ServeFile(w, r, p)
return
}
w.WriteHeader(404)
}
}
// @Summary Returns the mounted config bundle.
// @Description This endpoint will load the config bundle mounted by the config-tool into memory. This state can then be modified, validated, downloaded, and optionally committed to a Quay operator instance.
// @Produce json
// @Success 200 {object} ConfigBundle
// @Router /config [get]
func getMountedConfigBundle(opts *ServerOptions) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
// Fill defaults
var config map[string]interface{}
defaultFieldGroups, err := conf.NewConfig(map[string]interface{}{})
if err != nil {
log.Errorf(err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Fill defaults
for _, fg := range defaultFieldGroups {
fgBytes, err := yaml.Marshal(fg)
if err != nil {
log.Errorf(err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = yaml.Unmarshal(fgBytes, &config)
if err != nil {
log.Errorf(err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
// Read config file
configFilePath := path.Join(opts.configPath, "config.yaml")
configBytes, err := ioutil.ReadFile(configFilePath)
if err != nil {
// Mount not found, but will continue with defaults
w.WriteHeader(http.StatusAccepted)
} else {
w.WriteHeader(http.StatusOK)
if err = yaml.Unmarshal(configBytes, &config); err != nil {
log.Errorf(err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
// Get all certs in directory
certs := shared.LoadCerts(opts.configPath)
resp := ConfigBundle{
Config: config,
Certificates: certs,
}
var json = jsoniter.ConfigCompatibleWithStandardLibrary
js, err := json.Marshal(resp)
if err != nil {
log.Errorf(err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Add("Content-Type", "application/json")
w.Write(js)
}
}
// @Summary Downloads a config bundle as a tar.gz
// @Description This endpoint will download the config bundle in the request body as a tar.gz
// @Accept json
// @Param configBundle body ConfigBundle true "JSON Representing Config Bundle"
// @Produce multipart/form-data
// @Success 200
// @Router /config/download [post]
func downloadConfigBundle(opts *ServerOptions) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var confBundle ConfigBundle
err := json.NewDecoder(r.Body).Decode(&confBundle)
if err != nil {
log.Errorf(err.Error())
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
files := make(map[string][]byte)
files["config.yaml"], err = yaml.Marshal(confBundle.Config)
if err != nil {
log.Errorf(err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
for certName, contents := range confBundle.Certificates {
files[certName] = contents
}
var buf bytes.Buffer
gw := gzip.NewWriter(&buf)
tw := tar.NewWriter(gw)
hdr := &tar.Header{
Name: "extra_ca_certs/",
Typeflag: tar.TypeDir,
Mode: 0777,
}
if err := tw.WriteHeader(hdr); err != nil {
log.Errorf(err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
for name, contents := range files {
hdr := &tar.Header{
Name: name,
Mode: 0777,
Size: int64(len(contents)),
}
if err := tw.WriteHeader(hdr); err != nil {
log.Errorf(err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if _, err := tw.Write(contents); err != nil {
log.Errorf(err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
tw.Close()
gw.Close()
w.Header().Set("Content-type", "application/zip")
w.Header().Set("Content-Disposition", "attachment; filename=quay-config.tar.gz")
w.Write(buf.Bytes())
}
}
// @Summary Validates a config bundle.
// @Description This endpoint will validate the config bundle contained in the request body.
// @Accept json
// @Param configBundle body ConfigBundle true "JSON Representing Config Bundle"
// @Produce json
// @Success 200
// @Router /config/validate [post]
func validateConfigBundle(opts *ServerOptions) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
log.Debug("Received config bundle. Decoding into ConfigBundle struct")
var configBundle ConfigBundle
err := json.NewDecoder(r.Body).Decode(&configBundle)
if err != nil {
log.Errorf(err.Error())
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
configBundle.Config = shared.FixNumbers(configBundle.Config)
configBundle.Config = shared.RemoveNullValues(configBundle.Config)
loaded, err := config.NewConfig(configBundle.Config)
if err != nil {
log.Errorf(err.Error())
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
mode := r.URL.Query().Get("mode")
if mode == "" {
mode = "online"
}
opts := shared.Options{
Mode: mode,
Certificates: configBundle.Certificates,
}
errors := loaded.Validate(opts)
var json = jsoniter.ConfigCompatibleWithStandardLibrary
js, err := json.Marshal(errors)
if err != nil {
log.Errorf(err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Add("Content-Type", "application/json")
w.Write(js)
}
}
// @Summary Commits a config bundle to a Quay operator instance.
// @Description Handles an HTTP POST request containing a new `config.yaml`, adds any uploaded certs, and calls an API endpoint on the Quay Operator to create a new `Secret`.
// @Accept json
// @Param configBundle body ConfigBundle true "JSON Representing Config Bundle"
// @Produce json
// @Success 200
// @Router /config/operator [post]
func commitToOperator(opts *ServerOptions) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var configBundle ConfigBundle
err := json.NewDecoder(r.Body).Decode(&configBundle)
if err != nil {
log.Errorf(err.Error())
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
configBundle.Config = shared.FixNumbers(configBundle.Config)
configBundle.Config = shared.RemoveNullValues(configBundle.Config)
// TODO(alecmerdler): For each managed component fieldgroup, remove its fields from `config.yaml` using `Fields()` function...
newConfig, err := config.NewConfig(configBundle.Config)
if err != nil {
log.Errorf(err.Error())
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
for _, fieldGroup := range configBundle.ManagedFieldGroups {
// NOTE: Skip `HostSettings` because it is a special case where we allow setting the fields.
if fieldGroup == "HostSettings" {
continue
}
if fg, ok := newConfig[fieldGroup]; ok {
fields := fg.Fields()
for _, field := range fields {
delete(configBundle.Config, field)
}
}
}
// We must strip out cluster provisioned CA certs, as they will be mounted into the pod from separate config maps
for certName := range configBundle.Certificates {
if certName == "service-ca.crt" || certName == "ca-bundle.crt" {
delete(configBundle.Certificates, certName)
log.Infof("Removed Openshift certificate %s from user provided config bundle before committing to operator", certName)
}
}
// TODO: Define struct type for this with correct `yaml` tags
preSecret := map[string]interface{}{
"quayRegistryName": strings.Split(opts.podName, "-quay-config-editor")[0],
"namespace": opts.podNamespace,
"config.yaml": configBundle.Config,
"certs": configBundle.Certificates,
}
var json = jsoniter.ConfigCompatibleWithStandardLibrary
js, err := json.Marshal(preSecret)
if err != nil {
log.Errorf(err.Error())
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// FIXME: Currently hardcoding
req, err := http.NewRequest("POST", opts.operatorEndpoint+"/reconfigure", bytes.NewBuffer(js))
if err != nil {
log.Errorf(err.Error())
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
log.Errorf(err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Errorf(err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(resp.StatusCode)
w.Header().Add("Content-Type", "application/json")
w.Write(body)
}
}