From a2ae972e814f80184033ee75e9715d1d76323410 Mon Sep 17 00:00:00 2001 From: Kiran Kamineni Date: Wed, 1 May 2019 12:31:14 -0700 Subject: Create kubeconfig files in kubeconfig dir The connectivity api should allow the creation of kubeconfig files in the kubeconfig dir. Issue-ID: MULTICLOUD-292 Change-Id: I5ecc92622648c6c90b71ffad433a132e191cf4b3 Signed-off-by: Kiran Kamineni --- src/k8splugin/internal/app/client.go | 30 +++++++++++++--- src/k8splugin/internal/app/client_test.go | 26 +++++++++++++- src/k8splugin/internal/app/instance.go | 5 ++- src/k8splugin/internal/app/instance_test.go | 33 +++++++++++++++--- src/k8splugin/internal/connection/connection.go | 37 +++++++++++++++++--- .../internal/connection/connectionhandler.go | 40 +++++++++++++++++++--- 6 files changed, 150 insertions(+), 21 deletions(-) diff --git a/src/k8splugin/internal/app/client.go b/src/k8splugin/internal/app/client.go index 7024420c..158d21de 100644 --- a/src/k8splugin/internal/app/client.go +++ b/src/k8splugin/internal/app/client.go @@ -19,6 +19,8 @@ import ( "strings" utils "k8splugin/internal" + "k8splugin/internal/config" + "k8splugin/internal/connection" "k8splugin/internal/helm" pkgerrors "github.com/pkg/errors" @@ -43,12 +45,32 @@ type KubernetesClient struct { restMapper meta.RESTMapper } -// GetKubeClient loads the Kubernetes configuation values stored into the local configuration file -func (k *KubernetesClient) init(configPath string) error { - if configPath == "" { - return pkgerrors.New("config not passed and is not found in ~/.kube. ") +// getKubeConfig uses the connectivity client to get the kubeconfig based on the name +// of the cloudregion. This is written out to a file. +func (k *KubernetesClient) getKubeConfig(cloudregion string) (string, error) { + conn := connection.NewConnectionClient() + kubeConfigPath, err := conn.Download(cloudregion, config.GetConfiguration().KubeConfigDir) + if err != nil { + return "", pkgerrors.Wrap(err, "Downloading kubeconfig") + } + + return kubeConfigPath, nil +} + +// init loads the Kubernetes configuation values stored into the local configuration file +func (k *KubernetesClient) init(cloudregion string) error { + if cloudregion == "" { + return pkgerrors.New("Cloudregion is empty") } + configPath, err := k.getKubeConfig(cloudregion) + if err != nil { + return pkgerrors.Wrap(err, "Get kubeconfig file") + } + + //Remove kubeconfigfile after the clients are created + defer os.Remove(configPath) + config, err := clientcmd.BuildConfigFromFlags("", configPath) if err != nil { return pkgerrors.Wrap(err, "setConfig: Build config from flags raised an error") diff --git a/src/k8splugin/internal/app/client_test.go b/src/k8splugin/internal/app/client_test.go index 4cc533e2..4bfbcb18 100644 --- a/src/k8splugin/internal/app/client_test.go +++ b/src/k8splugin/internal/app/client_test.go @@ -14,12 +14,16 @@ limitations under the License. package app import ( + "encoding/base64" + "io/ioutil" "os" "plugin" "reflect" "testing" utils "k8splugin/internal" + "k8splugin/internal/connection" + "k8splugin/internal/db" "k8splugin/internal/helm" pkgerrors "github.com/pkg/errors" @@ -46,9 +50,29 @@ func LoadMockPlugins(krdLoadedPlugins map[string]*plugin.Plugin) error { func TestInit(t *testing.T) { t.Run("Successfully create Kube Client", func(t *testing.T) { + // Load the mock kube config file into memory + fd, err := ioutil.ReadFile("../../mock_files/mock_configs/mock_kube_config") + if err != nil { + t.Fatal("Unable to read mock_kube_config") + } + + fdbase64 := base64.StdEncoding.EncodeToString(fd) + + // Create mock db with connectivity information in it + db.DBconn = &db.MockDB{ + Items: map[string]map[string][]byte{ + connection.ConnectionKey{CloudRegion: "mock_connection"}.String(): { + "metadata": []byte( + "{\"cloud-region\":\"mock_connection\"," + + "\"cloud-owner\":\"mock_owner\"," + + "\"kubeconfig\": \"" + fdbase64 + "\"}"), + }, + }, + } kubeClient := KubernetesClient{} - err := kubeClient.init("../../mock_files/mock_configs/mock_kube_config") + // Refer to the connection via its name + err = kubeClient.init("mock_connection") if err != nil { t.Fatalf("TestGetKubeClient returned an error (%s)", err) } diff --git a/src/k8splugin/internal/app/instance.go b/src/k8splugin/internal/app/instance.go index 8d289d85..6d0910d0 100644 --- a/src/k8splugin/internal/app/instance.go +++ b/src/k8splugin/internal/app/instance.go @@ -21,7 +21,6 @@ import ( "encoding/json" "math/rand" - "k8splugin/internal/config" "k8splugin/internal/db" "k8splugin/internal/helm" "k8splugin/internal/rb" @@ -120,7 +119,7 @@ func (v *InstanceClient) Create(i InstanceRequest) (InstanceResponse, error) { } k8sClient := KubernetesClient{} - err = k8sClient.init(config.GetConfiguration().KubeConfigDir + "/" + i.CloudRegion) + err = k8sClient.init(i.CloudRegion) if err != nil { return InstanceResponse{}, pkgerrors.Wrap(err, "Getting CloudRegion Information") } @@ -185,7 +184,7 @@ func (v *InstanceClient) Delete(id string) error { } k8sClient := KubernetesClient{} - err = k8sClient.init(config.GetConfiguration().KubeConfigDir + "/" + inst.CloudRegion) + err = k8sClient.init(inst.CloudRegion) if err != nil { return pkgerrors.Wrap(err, "Getting CloudRegion Information") } diff --git a/src/k8splugin/internal/app/instance_test.go b/src/k8splugin/internal/app/instance_test.go index ab39dfb7..6ab14a34 100644 --- a/src/k8splugin/internal/app/instance_test.go +++ b/src/k8splugin/internal/app/instance_test.go @@ -14,12 +14,14 @@ limitations under the License. package app import ( + "encoding/base64" + "io/ioutil" "log" "reflect" "testing" utils "k8splugin/internal" - "k8splugin/internal/config" + "k8splugin/internal/connection" "k8splugin/internal/db" "k8splugin/internal/helm" "k8splugin/internal/rb" @@ -37,6 +39,12 @@ func TestInstanceCreate(t *testing.T) { t.Fatalf("LoadMockPlugins returned an error (%s)", err) } + // Load the mock kube config file into memory + fd, err := ioutil.ReadFile("../../mock_files/mock_configs/mock_kube_config") + if err != nil { + t.Fatal("Unable to read mock_kube_config") + } + t.Run("Successfully create Instance", func(t *testing.T) { db.DBconn = &db.MockDB{ Items: map[string]map[string][]byte{ @@ -145,6 +153,12 @@ func TestInstanceCreate(t *testing.T) { "RZQl9kOgrk+XoOzX68tJ3wYJb0N/RJ0NzPUr5y4YEDBw4cOHDgwIEDBw4cOHDgwIEDBw4" + "cOHDgwIEDB18K/AcxEDJDAHgAAA=="), }, + connection.ConnectionKey{CloudRegion: "mock_connection"}.String(): { + "metadata": []byte( + "{\"cloud-region\":\"mock_connection\"," + + "\"cloud-owner\":\"mock_owner\"," + + "\"kubeconfig\": \"" + base64.StdEncoding.EncodeToString(fd) + "\"}"), + }, }, } @@ -153,10 +167,9 @@ func TestInstanceCreate(t *testing.T) { RBName: "test-rbdef", RBVersion: "v1", ProfileName: "profile1", - CloudRegion: "mock_kube_config", + CloudRegion: "mock_connection", } - config.SetConfigValue("KubeConfigDir", "../../mock_files/mock_configs") ir, err := ic.Create(input) if err != nil { t.Fatalf("TestInstanceCreate returned an error (%s)", err) @@ -311,6 +324,12 @@ func TestInstanceDelete(t *testing.T) { t.Fatalf("TestInstanceDelete returned an error (%s)", err) } + // Load the mock kube config file into memory + fd, err := ioutil.ReadFile("../../mock_files/mock_configs/mock_kube_config") + if err != nil { + t.Fatal("Unable to read mock_kube_config") + } + t.Run("Successfully delete Instance", func(t *testing.T) { db.DBconn = &db.MockDB{ Items: map[string]map[string][]byte{ @@ -322,7 +341,7 @@ func TestInstanceDelete(t *testing.T) { "namespace":"testnamespace", "rb-name":"test-rbdef", "rb-version":"v1", - "cloud-region":"mock_kube_config", + "cloud-region":"mock_connection", "resources": [ { "GVK": { @@ -343,6 +362,12 @@ func TestInstanceDelete(t *testing.T) { ] }`), }, + connection.ConnectionKey{CloudRegion: "mock_connection"}.String(): { + "metadata": []byte( + "{\"cloud-region\":\"mock_connection\"," + + "\"cloud-owner\":\"mock_owner\"," + + "\"kubeconfig\": \"" + base64.StdEncoding.EncodeToString(fd) + "\"}"), + }, }, } diff --git a/src/k8splugin/internal/connection/connection.go b/src/k8splugin/internal/connection/connection.go index dbd1c2e1..b2bdca32 100644 --- a/src/k8splugin/internal/connection/connection.go +++ b/src/k8splugin/internal/connection/connection.go @@ -17,7 +17,11 @@ package connection import ( + "encoding/base64" "encoding/json" + "io/ioutil" + "path/filepath" + "k8splugin/internal/db" pkgerrors "github.com/pkg/errors" @@ -27,7 +31,7 @@ import ( type Connection struct { CloudRegion string `json:"cloud-region"` CloudOwner string `json:"cloud-owner"` - Kubeconfig map[string]interface{} `json:"kubeconfig"` + Kubeconfig string `json:"kubeconfig"` OtherConnectivityList map[string]interface{} `json:"other-connectivity-list"` } @@ -47,14 +51,14 @@ func (dk ConnectionKey) String() string { return string(out) } -// ConnectionManager is an interface exposes the Connection functionality +// ConnectionManager is an interface exposes the Connection functionality type ConnectionManager interface { Create(c Connection) (Connection, error) Get(name string) (Connection, error) Delete(name string) error } -// ConnectionClient implements the ConnectionManager +// ConnectionClient implements the ConnectionManager // It will also be used to maintain some localized state type ConnectionClient struct { storeName string @@ -113,7 +117,7 @@ func (v *ConnectionClient) Get(name string) (Connection, error) { return Connection{}, pkgerrors.New("Error getting Connection") } -// Delete the Connection from database +// Delete the Connection from database func (v *ConnectionClient) Delete(name string) error { //Construct the composite key to select the entry @@ -124,3 +128,28 @@ func (v *ConnectionClient) Delete(name string) error { } return nil } + +// Download the connection information onto a kubeconfig file +// The file is named after the name of the connection and will +// be placed in the provided parent directory +func (v *ConnectionClient) Download(name string, parentdir string) (string, error) { + + conn, err := v.Get(name) + if err != nil { + return "", pkgerrors.Wrap(err, "Getting Connection info") + } + + //Decode the kubeconfig from base64 to string + kubeContent, err := base64.StdEncoding.DecodeString(conn.Kubeconfig) + if err != nil { + return "", pkgerrors.Wrap(err, "Converting from base64") + } + + target := filepath.Join(parentdir, conn.CloudRegion) + err = ioutil.WriteFile(target, kubeContent, 0644) + if err != nil { + return "", pkgerrors.Wrap(err, "Writing kubeconfig to file") + } + + return target, nil +} diff --git a/src/k8splugin/internal/connection/connectionhandler.go b/src/k8splugin/internal/connection/connectionhandler.go index 0f8014fa..8c860d31 100644 --- a/src/k8splugin/internal/connection/connectionhandler.go +++ b/src/k8splugin/internal/connection/connectionhandler.go @@ -17,8 +17,11 @@ package connection import ( + "bytes" + "encoding/base64" "encoding/json" "io" + "io/ioutil" "net/http" "github.com/gorilla/mux" @@ -32,11 +35,25 @@ type ConnectionHandler struct { Client ConnectionManager } -// createHandler handles creation of the connectivity entry in the database +// CreateHandler handles creation of the connectivity entry in the database +// This is a multipart handler. See following example curl request +// curl -i -F "metadata={\"cloud-region\":\"kud\",\"cloud-owner\":\"me\"};type=application/json" \ +// -F file=@/home/user/.kube/config \ +// -X POST http://localhost:8081/v1/connectivity-info func (h ConnectionHandler) CreateHandler(w http.ResponseWriter, r *http.Request) { var v Connection - err := json.NewDecoder(r.Body).Decode(&v) + // Implemenation using multipart form + // Review and enable/remove at a later date + // Set Max size to 16mb here + err := r.ParseMultipartForm(16777216) + if err != nil { + http.Error(w, err.Error(), http.StatusUnprocessableEntity) + return + } + + jsn := bytes.NewBuffer([]byte(r.FormValue("metadata"))) + err = json.NewDecoder(jsn).Decode(&v) switch { case err == io.EOF: http.Error(w, "Empty body", http.StatusBadRequest) @@ -58,11 +75,24 @@ func (h ConnectionHandler) CreateHandler(w http.ResponseWriter, r *http.Request) return } - // Kubeconfig is required. - if v.Kubeconfig == nil { - http.Error(w, "Missing Kubeconfig in POST request", http.StatusBadRequest) + //Read the file section and ignore the header + file, _, err := r.FormFile("file") + if err != nil { + http.Error(w, "Unable to process file", http.StatusUnprocessableEntity) return } + + defer file.Close() + + //Convert the file content to base64 for storage + content, err := ioutil.ReadAll(file) + if err != nil { + http.Error(w, "Unable to read file", http.StatusUnprocessableEntity) + return + } + + v.Kubeconfig = base64.StdEncoding.EncodeToString(content) + ret, err := h.Client.Create(v) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) -- cgit 1.2.3-korg