From fbfe53f28f7e01adcc85e43e9916727ca9b3d28d Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Wed, 15 May 2019 18:50:40 +0000 Subject: [PATCH] server: Add /pointerconfig API This is prep for fixing https://github.com/openshift/machine-config-operator/issues/683 Today the installer generates this "pointer Ignition" which ends up as e.g. user-data in AWS, which is just a pair of (MCS URL, root CA). We need to do this because of size limitations on AWS user data. It makes a lot of sense for the MCO to be in control of generating the pointer ignition config too, as it helps centralize knowledge of Ignition. Ultimately, the goal here is tighter integration between machineAPI and the the MCO; e.g. the machineAPI asks to generate the pointer Ignition config, rather than having it stored as a static secret. This could then unlock the ability for the MCO to inject e.g. one time auth tokens. --- pkg/server/api.go | 71 +++++++++++++++++++++++++++------- pkg/server/api_test.go | 4 ++ pkg/server/bootstrap_server.go | 5 +++ pkg/server/cluster_server.go | 50 +++++++++++++++++++++++- pkg/server/server.go | 1 + 5 files changed, 116 insertions(+), 15 deletions(-) diff --git a/pkg/server/api.go b/pkg/server/api.go index 16dc150dc6..e0a50255a7 100644 --- a/pkg/server/api.go +++ b/pkg/server/api.go @@ -7,6 +7,8 @@ import ( "path" "github.com/golang/glog" + + ignv2_2types "github.com/coreos/ignition/config/v2_2/types" ) type poolRequest struct { @@ -29,6 +31,9 @@ type APIServer struct { func NewAPIServer(a *APIHandler, p int, is bool, c, k string) *APIServer { mux := http.NewServeMux() mux.Handle("/config/", a) + mux.Handle("/pointerconfig/", &pointerHandler { + server: a.server, + }) mux.Handle("/healthz", &healthHandler{}) mux.Handle("/", &defaultHandler{}) @@ -61,6 +66,35 @@ func (a *APIServer) Serve() { } } +func completeIgnitionRequest(w http.ResponseWriter, r *http.Request, conf *ignv2_2types.Config) { + if conf == nil { + w.Header().Set("Content-Length", "0") + w.WriteHeader(http.StatusNotFound) + return + } + + data, err := json.Marshal(conf) + if err != nil { + w.Header().Set("Content-Length", "0") + w.WriteHeader(http.StatusInternalServerError) + glog.Errorf("failed to marshal config: %v", err) + return + } + + w.Header().Set("Content-Length", fmt.Sprintf("%d", len(data))) + w.Header().Set("Content-Type", "application/json") + if r.Method == http.MethodHead { + w.WriteHeader(http.StatusOK) + return + } + + _, err = w.Write(data) + if err != nil { + glog.Errorf("failed to write response: %v", err) + } +} + + // APIHandler is the HTTP Handler for the // Machine Config Server. type APIHandler struct { @@ -101,31 +135,42 @@ func (sh *APIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { glog.Errorf("couldn't get config for req: %v, error: %v", cr, err) return } - if conf == nil && err == nil { + completeIgnitionRequest(w, r, conf) +} + +// pointerHandler is the HTTP Handler for the +// Machine Config Server. +type pointerHandler struct { + server Server +} + +// ServeHTTP handles the requests for the machine config server +// API handler. +func (sh *pointerHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet && r.Method != http.MethodHead { w.Header().Set("Content-Length", "0") - w.WriteHeader(http.StatusNotFound) + w.WriteHeader(http.StatusMethodNotAllowed) return } - data, err := json.Marshal(conf) - if err != nil { + if r.URL.Path == "" { w.Header().Set("Content-Length", "0") - w.WriteHeader(http.StatusInternalServerError) - glog.Errorf("failed to marshal %v config: %v", cr, err) + w.WriteHeader(http.StatusBadRequest) return } - w.Header().Set("Content-Length", fmt.Sprintf("%d", len(data))) - w.Header().Set("Content-Type", "application/json") - if r.Method == http.MethodHead { - w.WriteHeader(http.StatusOK) - return + cr := poolRequest{ + machineConfigPool: path.Base(r.URL.Path), } - _, err = w.Write(data) + conf, err := sh.server.GetPointerConfig(cr) if err != nil { - glog.Errorf("failed to write %v response: %v", cr, err) + w.Header().Set("Content-Length", "0") + w.WriteHeader(http.StatusInternalServerError) + glog.Errorf("couldn't get config for req: %v, error: %v", cr, err) + return } + completeIgnitionRequest(w, r, conf) } type healthHandler struct{} diff --git a/pkg/server/api_test.go b/pkg/server/api_test.go index edb9a4be76..7871b28431 100644 --- a/pkg/server/api_test.go +++ b/pkg/server/api_test.go @@ -1,6 +1,7 @@ package server import ( + "errors" "fmt" "io/ioutil" "net/http" @@ -17,6 +18,9 @@ type mockServer struct { func (ms *mockServer) GetConfig(pr poolRequest) (*ignv2_2types.Config, error) { return ms.GetConfigFn(pr) } +func (ms *mockServer) GetPointerConfig(pr poolRequest) (*ignv2_2types.Config, error) { + return nil, errors.New("Not implemented") +} type checkResponse func(t *testing.T, response *http.Response) diff --git a/pkg/server/bootstrap_server.go b/pkg/server/bootstrap_server.go index e394ea6e32..05b529606e 100644 --- a/pkg/server/bootstrap_server.go +++ b/pkg/server/bootstrap_server.go @@ -39,6 +39,11 @@ func NewBootstrapServer(dir, kubeconfig string) (Server, error) { }, nil } +// GetPointerConfig is not implemented yet - currently the installer generates this +func (bsc *bootstrapServer) GetPointerConfig(cr poolRequest) (*ignv2_2types.Config, error) { + return nil, fmt.Errorf("Not implemented") +} + // GetConfig fetches the machine config(type - Ignition) from the bootstrap server, // based on the pool request. // It returns nil for conf, error if the config isn't found. It returns a formatted diff --git a/pkg/server/cluster_server.go b/pkg/server/cluster_server.go index 9cd0db4002..d7185d13e7 100644 --- a/pkg/server/cluster_server.go +++ b/pkg/server/cluster_server.go @@ -3,9 +3,11 @@ package server import ( "fmt" "io/ioutil" + "net/url" "path/filepath" - ignv2_2types "github.com/coreos/ignition/config/v2_2/types" + ignition "github.com/coreos/ignition/config/v2_2/types" + "github.com/vincent-petithory/dataurl" yaml "github.com/ghodss/yaml" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -29,6 +31,11 @@ const ( var _ = Server(&clusterServer{}) type clusterServer struct { + // apiServerHost is the hostname of the API server + apiServerHost string + // rootCA is the root CA + rootCA []byte + // machineClient is used to interact with the // machine config, pool objects. machineClient v1.MachineconfigurationV1Interface @@ -48,16 +55,55 @@ func NewClusterServer(kubeConfig, apiserverURL string) (Server, error) { return nil, fmt.Errorf("Failed to create Kubernetes rest client: %v", err) } + rootCA, err := ioutil.ReadFile("/run/secrets/kubernetes.io/serviceaccount/ca.crt") + if err != nil { + return nil, err + } + + apiURL, err := url.Parse(apiserverURL) + if err != nil { + return nil, err + } + mc := v1.NewForConfigOrDie(restConfig) return &clusterServer{ + apiServerHost: apiURL.Hostname(), + rootCA: rootCA, machineClient: mc, kubeconfigFunc: func() ([]byte, []byte, error) { return kubeconfigFromSecret(bootstrapTokenDir, apiserverURL) }, }, nil } +// GetPointerConfig ends up as e.g. user-data in AWS. +func (cs *clusterServer) GetPointerConfig(cr poolRequest) (*ignition.Config, error) { + return &ignition.Config{ + Ignition: ignition.Ignition{ + Version: ignition.MaxVersion.String(), + Config: ignition.IgnitionConfig{ + Append: []ignition.ConfigReference{{ + Source: func() *url.URL { + return &url.URL{ + Scheme: "https", + Host: fmt.Sprintf("%s:22623", cs.apiServerHost), + Path: fmt.Sprintf("/config/%s", cr.machineConfigPool), + } + }().String(), + }}, + }, + Security: ignition.Security{ + TLS: ignition.TLS{ + CertificateAuthorities: []ignition.CaReference{{ + Source: dataurl.EncodeBytes([]byte(cs.rootCA)), + }}, + }, + }, + }, + }, nil +} + // GetConfig fetches the machine config(type - Ignition) from the cluster, // based on the pool request. -func (cs *clusterServer) GetConfig(cr poolRequest) (*ignv2_2types.Config, error) { +func (cs *clusterServer) GetConfig(cr poolRequest) (*ignition.Config, error) { mp, err := cs.machineClient.MachineConfigPools().Get(cr.machineConfigPool, metav1.GetOptions{}) if err != nil { return nil, fmt.Errorf("could not fetch pool. err: %v", err) diff --git a/pkg/server/server.go b/pkg/server/server.go index 776252a437..ad4d4aef39 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -34,6 +34,7 @@ type appenderFunc func(*ignv2_2types.Config) error // Server defines the interface that is implemented by different // machine config server implementations. type Server interface { + GetPointerConfig(poolRequest) (*ignv2_2types.Config, error) GetConfig(poolRequest) (*ignv2_2types.Config, error) }