diff options
-rw-r--r-- | deployments/Dockerfile | 5 | ||||
-rw-r--r-- | kud/sites/examples/provider_nw_setup.sh | 65 | ||||
-rw-r--r-- | kud/tests/ovn_provider_nw_test.sh | 191 | ||||
-rwxr-xr-x | kud/tests/plugin.sh | 43 | ||||
-rw-r--r-- | src/k8splugin/api/api.go | 7 | ||||
-rw-r--r-- | src/k8splugin/api/brokerhandler.go | 164 | ||||
-rw-r--r-- | src/k8splugin/api/brokerhandler_test.go | 224 | ||||
-rw-r--r-- | src/k8splugin/cmd/main.go | 11 | ||||
-rw-r--r-- | src/k8splugin/go.mod | 3 | ||||
-rw-r--r-- | src/k8splugin/go.sum | 1 | ||||
-rw-r--r-- | src/k8splugin/internal/app/client.go | 146 | ||||
-rw-r--r-- | src/k8splugin/internal/app/client_test.go | 6 | ||||
-rw-r--r-- | src/k8splugin/internal/app/instance.go | 4 | ||||
-rw-r--r-- | src/k8splugin/internal/auth/auth.go | 107 | ||||
-rw-r--r-- | src/k8splugin/internal/auth/auth_test.go | 47 | ||||
-rw-r--r-- | src/k8splugin/mock_files/mock_certs/auth_test_certificate.pem | 21 | ||||
-rw-r--r-- | src/k8splugin/mock_files/mock_certs/auth_test_key.pem | 28 | ||||
-rw-r--r-- | src/k8splugin/plugins/generic/plugin.go | 109 |
18 files changed, 1141 insertions, 41 deletions
diff --git a/deployments/Dockerfile b/deployments/Dockerfile index 961f6766..1dcb5a30 100644 --- a/deployments/Dockerfile +++ b/deployments/Dockerfile @@ -31,10 +31,11 @@ RUN apt-get update && apt-get install -y -qq apt-transport-https curl \ && apt-get update && apt install -y -qq ovn-common WORKDIR /opt/multicloud/k8s -ADD ./k8plugin ./ -ADD ./*.so ./ RUN chown onap:onap /opt/multicloud/k8s -R +ADD --chown=onap ./k8plugin ./ +ADD --chown=onap ./bin/*.so ./ + USER onap CMD ["./k8plugin"] diff --git a/kud/sites/examples/provider_nw_setup.sh b/kud/sites/examples/provider_nw_setup.sh new file mode 100644 index 00000000..fbe0011d --- /dev/null +++ b/kud/sites/examples/provider_nw_setup.sh @@ -0,0 +1,65 @@ +#!/bin/bash +# SPDX-license-identifier: Apache-2.0 +############################################################################## +# Copyright (c) 2018 +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + +set -o errexit +set -o nounset +set -o pipefail + +map_list="" +function create_vlan { + local dev=$1 + local id=$2 + local name=$3 + + # Create VLAN for device + sudo ip link add link $dev name $name type vlan id $id + #sudo ip addr add $ip dev $name + sudo ip link set $name up +} + +# Create provider network for interface +function create_provider_network { + local provider_name=$1 + local interface=$2 + + bridge_name=br-$provider_name + network_name=nw_$provider_name + port_name=server-localnet_$provider_name + + # Create OVS bridge and move the interface to the bridge + sudo ovs-vsctl --may-exist add-br $bridge_name + sudo ovs-vsctl --may-exist add-port $bridge_name $interface + + #Create OVN Switch + sudo ovn-nbctl --may-exist ls-add $provider_name + # Add port of type localnet to the Switch + sudo ovn-nbctl --may-exist lsp-add $provider_name $port_name + sudo ovn-nbctl lsp-set-addresses $port_name unknown + sudo ovn-nbctl lsp-set-type $port_name localnet + #Set port with the network name to map to ovs bridge + sudo ovn-nbctl lsp-set-options $port_name network_name=$network_name + # Prepare bridge to network mapping for OVS + map_list=${map_list}${network_name}:${bridge_name}, +} + +create_vlan eth1 100 eth1.100 +create_vlan eth1 200 eth1.200 + +provider_net1=prod-net1 +provider_net2=prod-net2 + +create_provider_network $provider_net1 eth1.100 +create_provider_network $provider_net2 eth1.200 + +#Set OVS with the bridge to network mapping +map_list=${map_list%?} +sudo ovs-vsctl set open . external-ids:ovn-bridge-mappings=$map_list + + diff --git a/kud/tests/ovn_provider_nw_test.sh b/kud/tests/ovn_provider_nw_test.sh new file mode 100644 index 00000000..9b32d904 --- /dev/null +++ b/kud/tests/ovn_provider_nw_test.sh @@ -0,0 +1,191 @@ +#!/bin/bash +# SPDX-license-identifier: Apache-2.0 +############################################################################## +# Copyright (c) 2018 +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + +set -o errexit +set -o nounset +set -o pipefail + +source _common.sh +source _functions.sh + +# populate_CSAR_ovn4nfv() - Create content used for OVN4NFV functional test +function populate_CSAR_provider_network { + local csar_id=$1 + + _checks_args $csar_id + pushd ${CSAR_DIR}/${csar_id} + + cat << MULTUS_NET > onap-ovn4nfvk8s-network.yaml +apiVersion: "k8s.cni.cncf.io/v1" +kind: NetworkAttachmentDefinition +metadata: + name: $ovn_multus_network_name +spec: + config: '{ + "cniVersion": "0.3.1", + "name": "ovn4nfv-k8s-plugin", + "type": "ovn4nfvk8s-cni" + }' +MULTUS_NET + + cat << NETWORK > ovn-virt-net1.yaml +apiVersion: v1 +kind: onapNetwork +metadata: + name: ovn-virt-net1 + cnitype : ovn4nfvk8s +spec: + name: ovn-virt-net1 + subnet: 10.1.20.0/24 + gateway: 10.1.20.1/24 +NETWORK + + cat << NETWORK > ovn-virt-net2.yaml +apiVersion: v1 +kind: onapNetwork +metadata: + name: ovn-virt-net2 + cnitype : ovn4nfvk8s +spec: + name: ovn-virt-net2 + subnet: 10.1.21.0/24 + gateway: 10.1.21.1/24 +NETWORK + + cat << DEPLOYMENT > firewall.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: firewall + labels: + app: ovn4nfv +spec: + replicas: 1 + selector: + matchLabels: + app: ovn4nfv + template: + metadata: + labels: + app: ovn4nfv + annotations: + k8s.v1.cni.cncf.io/networks: '[{ "name": "$ovn_multus_network_name"}]' + ovnNetwork: '[ { "name": ""ovn-virt-net1"", "interface": "net0" , "defaultGateway": "false", "ipAddress":"10.1.20.2"}, + { "name": "prod-net1", "interface": "net1", "defaultGateway": "false", "ipAddress":"10.1.5.1/24"}]' + ovnNetworkRoutes: '[{ "dst": "0.0.0.0/0", "gw": "10.1.20.3", "dev": "net0" }]' + + spec: + containers: + - name: firewall + image: "busybox" + command: ["top"] + stdin: true + tty: true +DEPLOYMENT + + cat << DEPLOYMENT > webcache.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: webcache + labels: + app: ovn4nfv +spec: + replicas: 1 + selector: + matchLabels: + app: ovn4nfv + template: + metadata: + labels: + app: ovn4nfv + annotations: + k8s.v1.cni.cncf.io/networks: '[{ "name": "$ovn_multus_network_name"}]' + ovnNetwork: '[{ "name": "ovn-virt-net1", "interface": "net0" , "defaultGateway": "false", "ipAddress":"10.1.20.3"}, + { "name": "ovn-virt-net2", "interface": "net1" , "defaultGateway": "false", "ipAddress":"10.1.21.2"}]' + ovnNetworkRoutes: '[{ "dst": "10.1.5.0/24", "gw": "10.1.20.2", "dev": "net0" }, + { "dst": "0.0.0.0/0", "gw": "10.1.21.3", "dev": "net1" }]' + + spec: + containers: + - name: webcache + image: "busybox" + command: ["top"] + stdin: true + tty: true +DEPLOYMENT + + cat << DEPLOYMENT > sdwan.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: sdwan + labels: + app: ovn4nfv +spec: + replicas: 1 + selector: + matchLabels: + app: ovn4nfv + template: + metadata: + labels: + app: ovn4nfv + annotations: + k8s.v1.cni.cncf.io/networks: '[{ "name": "$ovn_multus_network_name"}]' + ovnNetwork: '[ { "name": ""ovn-virt-net2"", "interface": "net0" , "defaultGateway": "false", "ipAddress":"10.1.21.3"}, + { "name": "prod-net2", "interface": "net1", "defaultGateway": "false", "ipAddress":"10.1.10.2/24"}]' + ovnNetworkRoutes: '[{ "dst": "0.0.0.0/0", "gw": "10.1.10.1", "dev": "net1" }, + { "dst": "10.1.5.0/24", "gw": "10.1.21.2", "dev": "net0" }, + { "dst": "10.1.20.0/24", "gw": "10.1.21.2", "dev": "net0" }]' + + spec: + containers: + - name: sdwan + image: "busybox" + command: ["top"] + stdin: true + tty: true +DEPLOYMENT + popd +} + +csar_id=d5718572-3b9a-11e9-b210-d663bd873dda +# Setup +install_ovn_deps +populate_CSAR_provider_network $csar_id + +pushd ${CSAR_DIR}/${csar_id} +for net in ovn-virt-net1 ovn-virt-net2; do + cleanup_network $net.yaml + echo "Create OVN Network $net network" + init_network $net.yaml +done +kubectl apply -f onap-ovn4nfvk8s-network.yaml +setup firewall webcache sdwan + +# Test +deployment_pod=$(kubectl get pods | grep firewall | awk '{print $1}') +echo "===== $deployment_pod details =====" +kubectl exec -it $deployment_pod -- ip a +multus_nic=$(kubectl exec -it $deployment_pod -- ifconfig | grep "net1") +if [ -z "$multus_nic" ]; then + echo "The $deployment_pod pod doesn't contain the net1 nic" + exit 1 +fi + +# Teardown +teardown firewall webcache sdwan +cleanup_network ovn-virt-net1.yaml +cleanup_network ovn-virt-net2.yaml +popd + + + diff --git a/kud/tests/plugin.sh b/kud/tests/plugin.sh index 2de55b61..1137f1b9 100755 --- a/kud/tests/plugin.sh +++ b/kud/tests/plugin.sh @@ -17,12 +17,14 @@ source _common.sh source _functions.sh base_url="http://localhost:8081" +#Will resolve to file $KUBE_CONFIG_DIR/kud cloud_region_id="kud" namespace="testns" csar_id="94e414f6-9ca4-11e8-bb6a-52540067263b" rb_name="test-rbdef" rb_version="v1" profile_name="profile1" +release_name="testrelease" vnf_customization_uuid="ebe353d2-30b7-11e9-9515-525400277b3d" # _build_generic_sim() - Creates a generic simulator image in case that doesn't exist @@ -95,7 +97,7 @@ payload_raw=" \"profile-name\": \"${profile_name}\", \"rb-name\": \"${rb_name}\", \"rb-version\": \"${rb_version}\", - \"release-name\": \"testrelease\", + \"release-name\": \"${release_name}\", \"namespace\": \"$namespace\", \"kubernetesversion\": \"$kubeversion\", \"labels\": { @@ -120,31 +122,22 @@ fi print_msg "Instantiate Profile" payload_raw=" { - \"cloud_region_id\": \"$cloud_region_id\", + \"cloud-region\": \"$cloud_region_id\", \"rb-name\":\"$rb_name\", \"rb-version\":\"$rb_version\", - \"profile-name\":\"$profile_name\", - \"csar_id\": \"$csar_id\" + \"profile-name\":\"$profile_name\" } " payload=$(echo $payload_raw | tr '\n' ' ') -vnf_id=$(curl -s -d "$payload" "${base_url}/v1/vnf_instances/" | jq -r '.vnf_id') +inst_id=$(curl -s -d "$payload" "${base_url}/v1/instance" | jq -r '.id') print_msg "Validating Kubernetes" -kubectl get --no-headers=true --namespace=${namespace} deployment ${cloud_region_id}-${namespace}-${vnf_id}-testrelease-vault-consul-dev -kubectl get --no-headers=true --namespace=${namespace} service ${cloud_region_id}-${namespace}-${vnf_id}-override-vault-consul -echo "VNF Instance created succesfully with id: $vnf_id" - -print_msg "Listing VNF Instances" -vnf_id_list=$(curl -s -X GET "${base_url}/v1/vnf_instances/${cloud_region_id}/${namespace}" | jq -r '.vnf_id_list') -if [[ "$vnf_id_list" != *"${vnf_id}"* ]]; then - echo $vnf_id_list - echo "VNF Instance not stored" - exit 1 -fi +kubectl get --no-headers=true --namespace=${namespace} deployment ${release_name}-vault-consul-dev +kubectl get --no-headers=true --namespace=${namespace} service override-vault-consul +echo "VNF Instance created succesfully with id: $inst_id" -print_msg "Getting $vnf_id VNF Instance information" -vnf_details=$(curl -s -X GET "${base_url}/v1/vnf_instances/${cloud_region_id}/${namespace}/${vnf_id}") +print_msg "Getting $inst_id VNF Instance information" +vnf_details=$(curl -s -X GET "${base_url}/v1/instance/${inst_id}") if [[ -z "$vnf_details" ]]; then echo "Cannot retrieved VNF Instance details" exit 1 @@ -159,9 +152,17 @@ if [[ 500 -ne $(curl -o /dev/null -w %{http_code} -s -X GET "${base_url}/v1/rb/d exit 1 fi -print_msg "Deleting $vnf_id VNF Instance" -curl -X DELETE "${base_url}/v1/vnf_instances/${cloud_region_id}/${namespace}/${vnf_id}" -if [[ 404 -ne $(curl -o /dev/null -w %{http_code} -s -X GET "${base_url}${cloud_region_id}/${namespace}/${vnf_id}") ]]; then +print_msg "Deleting $profile_name Resource Bundle Profile" +curl -X DELETE "${base_url}/v1/rb/definition/$rb_name/$rb_version/profile/$profile_name" +if [[ 500 -ne $(curl -o /dev/null -w %{http_code} -s -X GET "${base_url}/v1/rb/definition/$rb_name/$rb_version/profile/$profile_name") ]]; then + echo "Resource Bundle Profile not deleted" +# TODO: Change the HTTP code for 404 when the resource is not found in the API + exit 1 +fi + +print_msg "Deleting $inst_id VNF Instance" +curl -X DELETE "${base_url}/v1/instance/${inst_id}" +if [[ 404 -ne $(curl -o /dev/null -w %{http_code} -s -X GET "${base_url}/${inst_id}") ]]; then echo "VNF Instance not deleted" exit 1 fi diff --git a/src/k8splugin/api/api.go b/src/k8splugin/api/api.go index 2003a809..604ccdc8 100644 --- a/src/k8splugin/api/api.go +++ b/src/k8splugin/api/api.go @@ -41,6 +41,13 @@ func NewRouter(defClient rb.DefinitionManager, // (TODO): Fix update method // instRouter.HandleFunc("/{vnfInstanceId}", UpdateHandler).Methods("PUT") + brokerHandler := brokerInstanceHandler{client: instClient} + instRouter.HandleFunc("/{cloud-owner}/{cloud-region}/infra_workload", brokerHandler.createHandler).Methods("POST") + instRouter.HandleFunc("/{cloud-owner}/{cloud-region}/infra_workload/{instID}", + brokerHandler.getHandler).Methods("GET") + instRouter.HandleFunc("/{cloud-owner}/{cloud-region}/infra_workload/{instID}", + brokerHandler.deleteHandler).Methods("DELETE") + //Setup resource bundle definition routes if defClient == nil { defClient = rb.NewDefinitionClient() diff --git a/src/k8splugin/api/brokerhandler.go b/src/k8splugin/api/brokerhandler.go new file mode 100644 index 00000000..28e44231 --- /dev/null +++ b/src/k8splugin/api/brokerhandler.go @@ -0,0 +1,164 @@ +/* +Copyright 2018 Intel Corporation. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package api + +import ( + "encoding/json" + "io" + "net/http" + + "k8splugin/internal/app" + + "github.com/gorilla/mux" +) + +// Used to store the backend implementation objects +// Also simplifies the mocking needed for unit testing +type brokerInstanceHandler struct { + // Interface that implements the Instance operations + client app.InstanceManager +} + +type brokerRequest struct { + GenericVnfID string `json:"generic-vnf-id"` + VFModuleID string `json:"vf-module-id"` + VFModuleModelInvariantID string `json:"vf-module-model-invariant-id"` + VFModuleModelVersionID string `json:"vf-module-model-version-id"` + VFModuleModelCustomizationID string `json:"vf-module-model-customization-id"` + OOFDirectives map[string]interface{} `json:"oof_directives"` + SDNCDirections map[string]interface{} `json:"sdnc_directives"` + UserDirectives map[string]interface{} `json:"user_directives"` + TemplateType string `json:"template_type"` + TemplateData map[string]interface{} `json:"template_data"` +} + +type brokerPOSTResponse struct { + TemplateType string `json:"template_type"` + WorkloadID string `json:"workload_id"` + TemplateResponse map[string][]string `json:"template_response"` +} + +type brokerGETResponse struct { + TemplateType string `json:"template_type"` + WorkloadID string `json:"workload_id"` + WorkloadStatus string `json:"workload_status"` +} + +func (b brokerInstanceHandler) createHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + cloudRegion := vars["cloud-region"] + + var req brokerRequest + err := json.NewDecoder(r.Body).Decode(&req) + switch { + case err == io.EOF: + http.Error(w, "Body empty", http.StatusBadRequest) + return + case err != nil: + http.Error(w, err.Error(), http.StatusUnprocessableEntity) + return + } + + // Check body for expected parameters + if req.VFModuleModelCustomizationID == "" { + http.Error(w, "vf-module-model-customization-id is empty", http.StatusBadRequest) + return + } + + rbName, ok := req.UserDirectives["definition-name"] + if !ok { + http.Error(w, "definition-name is missing from user-directives", http.StatusBadRequest) + return + } + + rbVersion, ok := req.UserDirectives["definition-version"] + if !ok { + http.Error(w, "definition-version is missing from user-directives", http.StatusBadRequest) + return + } + + profileName, ok := req.UserDirectives["profile-name"] + if !ok { + http.Error(w, "profile-name is missing from user-directives", http.StatusBadRequest) + return + } + + // Setup the resource parameters for making the request + var instReq app.InstanceRequest + instReq.RBName = rbName.(string) + instReq.RBVersion = rbVersion.(string) + instReq.ProfileName = profileName.(string) + instReq.CloudRegion = cloudRegion + + resp, err := b.client.Create(instReq) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + brokerResp := brokerPOSTResponse{ + TemplateType: "heat", + WorkloadID: resp.ID, + TemplateResponse: resp.Resources, + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + err = json.NewEncoder(w).Encode(brokerResp) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +// getHandler retrieves information about an instance via the ID +func (b brokerInstanceHandler) getHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + instanceID := vars["instID"] + + resp, err := b.client.Get(instanceID) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + brokerResp := brokerGETResponse{ + TemplateType: "heat", + WorkloadID: resp.ID, + WorkloadStatus: "CREATED", + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + err = json.NewEncoder(w).Encode(brokerResp) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +// deleteHandler method terminates an instance via the ID +func (b brokerInstanceHandler) deleteHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + instanceID := vars["instID"] + + err := b.client.Delete(instanceID) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) +} diff --git a/src/k8splugin/api/brokerhandler_test.go b/src/k8splugin/api/brokerhandler_test.go new file mode 100644 index 00000000..f35a835b --- /dev/null +++ b/src/k8splugin/api/brokerhandler_test.go @@ -0,0 +1,224 @@ +/* +Copyright 2018 Intel Corporation. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package api + +import ( + "bytes" + "encoding/json" + "io" + "io/ioutil" + "net/http" + "net/http/httptest" + "reflect" + "testing" + + "k8splugin/internal/app" + + pkgerrors "github.com/pkg/errors" +) + +func TestBrokerCreateHandler(t *testing.T) { + testCases := []struct { + label string + input io.Reader + expected brokerPOSTResponse + expectedCode int + instClient *mockInstanceClient + }{ + { + label: "Missing body failure", + expectedCode: http.StatusBadRequest, + }, + { + label: "Invalid JSON request format", + input: bytes.NewBuffer([]byte("invalid")), + expectedCode: http.StatusUnprocessableEntity, + }, + { + label: "Missing parameter failure", + input: bytes.NewBuffer([]byte(`{ + "vf-module-model-customization-id": "84sdfkio938", + "user_directives": { + "definition-name": "test-rbdef", + "definition-version": "v1" } + }`)), + expectedCode: http.StatusBadRequest, + }, + { + label: "Succesfully create an Instance", + input: bytes.NewBuffer([]byte(`{ + "vf-module-model-customization-id": "84sdfkio938", + "user_directives": { + "definition-name": "test-rbdef", + "definition-version": "v1", + "profile-name": "profile1" + } + }`)), + expected: brokerPOSTResponse{ + WorkloadID: "HaKpys8e", + TemplateType: "heat", + TemplateResponse: map[string][]string{ + "deployment": []string{"test-deployment"}, + "service": []string{"test-service"}, + }, + }, + expectedCode: http.StatusCreated, + instClient: &mockInstanceClient{ + items: []app.InstanceResponse{ + { + ID: "HaKpys8e", + RBName: "test-rbdef", + RBVersion: "v1", + ProfileName: "profile1", + CloudRegion: "region1", + Namespace: "testnamespace", + Resources: map[string][]string{ + "deployment": []string{"test-deployment"}, + "service": []string{"test-service"}, + }, + }, + }, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + + request := httptest.NewRequest("POST", "/v1/cloudowner/cloudregion/infra_workload", testCase.input) + resp := executeRequest(request, NewRouter(nil, nil, testCase.instClient)) + + if testCase.expectedCode != resp.StatusCode { + body, _ := ioutil.ReadAll(resp.Body) + t.Log(string(body)) + t.Fatalf("Request method returned: \n%v\n and it was expected: \n%v", resp.StatusCode, testCase.expectedCode) + } + + if resp.StatusCode == http.StatusCreated { + var response brokerPOSTResponse + err := json.NewDecoder(resp.Body).Decode(&response) + if err != nil { + t.Fatalf("Parsing the returned response got an error (%s)", err) + } + if !reflect.DeepEqual(testCase.expected, response) { + t.Fatalf("TestGetHandler returned:\n result=%v\n expected=%v", + response, testCase.expected) + } + } + }) + } +} + +func TestBrokerGetHandler(t *testing.T) { + testCases := []struct { + label string + input string + expectedCode int + expectedResponse brokerGETResponse + instClient *mockInstanceClient + }{ + { + label: "Fail to retrieve Instance", + input: "HaKpys8e", + expectedCode: http.StatusInternalServerError, + instClient: &mockInstanceClient{ + err: pkgerrors.New("Internal error"), + }, + }, + { + label: "Succesful get an Instance", + input: "HaKpys8e", + expectedCode: http.StatusOK, + expectedResponse: brokerGETResponse{ + TemplateType: "heat", + WorkloadID: "HaKpys8e", + WorkloadStatus: "CREATED", + }, + instClient: &mockInstanceClient{ + items: []app.InstanceResponse{ + { + ID: "HaKpys8e", + RBName: "test-rbdef", + RBVersion: "v1", + ProfileName: "profile1", + CloudRegion: "region1", + Namespace: "testnamespace", + Resources: map[string][]string{ + "deployment": []string{"test-deployment"}, + "service": []string{"test-service"}, + }, + }, + }, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + request := httptest.NewRequest("GET", "/v1/cloudowner/cloudregion/infra_workload/"+testCase.input, nil) + resp := executeRequest(request, NewRouter(nil, nil, testCase.instClient)) + + if testCase.expectedCode != resp.StatusCode { + t.Fatalf("Request method returned: %v and it was expected: %v", + resp.StatusCode, testCase.expectedCode) + } + if resp.StatusCode == http.StatusOK { + var response brokerGETResponse + err := json.NewDecoder(resp.Body).Decode(&response) + if err != nil { + t.Fatalf("Parsing the returned response got an error (%s)", err) + } + if !reflect.DeepEqual(testCase.expectedResponse, response) { + t.Fatalf("TestGetHandler returned:\n result=%v\n expected=%v", + response, testCase.expectedResponse) + } + } + }) + } +} + +func TestBrokerDeleteHandler(t *testing.T) { + testCases := []struct { + label string + input string + expectedCode int + instClient *mockInstanceClient + }{ + { + label: "Fail to destroy VNF", + input: "HaKpys8e", + expectedCode: http.StatusInternalServerError, + instClient: &mockInstanceClient{ + err: pkgerrors.New("Internal error"), + }, + }, + { + label: "Succesful delete a VNF", + input: "HaKpys8e", + expectedCode: http.StatusAccepted, + instClient: &mockInstanceClient{}, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.label, func(t *testing.T) { + request := httptest.NewRequest("DELETE", "/v1/cloudowner/cloudregion/infra_workload/"+testCase.input, nil) + resp := executeRequest(request, NewRouter(nil, nil, testCase.instClient)) + + if testCase.expectedCode != resp.StatusCode { + t.Fatalf("Request method returned: %v and it was expected: %v", resp.StatusCode, testCase.expectedCode) + } + }) + } +} diff --git a/src/k8splugin/cmd/main.go b/src/k8splugin/cmd/main.go index 96e1c8e5..607e3fe1 100644 --- a/src/k8splugin/cmd/main.go +++ b/src/k8splugin/cmd/main.go @@ -24,6 +24,7 @@ import ( "k8splugin/api" utils "k8splugin/internal" + "k8splugin/internal/auth" "github.com/gorilla/handlers" ) @@ -55,5 +56,13 @@ func main() { close(connectionsClose) }() - log.Fatal(httpServer.ListenAndServe()) + tlsConfig, err := auth.GetTLSConfig("ca.cert", "server.cert", "server.key") + if err != nil { + log.Println("Error Getting TLS Configuration. Starting without TLS...") + log.Fatal(httpServer.ListenAndServe()) + } else { + httpServer.TLSConfig = tlsConfig + // empty strings because tlsconfig already has this information + err = httpServer.ListenAndServeTLS("", "") + } } diff --git a/src/k8splugin/go.mod b/src/k8splugin/go.mod index 59c7a173..474e5102 100644 --- a/src/k8splugin/go.mod +++ b/src/k8splugin/go.mod @@ -69,6 +69,7 @@ require ( github.com/xdg/stringprep v1.0.0 // indirect go.etcd.io/etcd v3.3.12+incompatible go.mongodb.org/mongo-driver v1.0.0 + golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 golang.org/x/net v0.0.0-20181201002055-351d144fa1fc golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890 // indirect golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 // indirect @@ -77,7 +78,7 @@ require ( gopkg.in/square/go-jose.v2 v2.2.2 // indirect gopkg.in/yaml.v2 v2.2.1 k8s.io/api v0.0.0-20181126151915-b503174bad59 - k8s.io/apiextensions-apiserver v0.0.0-20181126155829-0cd23ebeb688 // indirect + k8s.io/apiextensions-apiserver v0.0.0-20181126155829-0cd23ebeb688 k8s.io/apimachinery v0.0.0-20181126123746-eddba98df674 k8s.io/apiserver v0.0.0-20181126153457-92fdef3a232a // indirect k8s.io/cli-runtime v0.0.0-20190107235426-31214e12222d // indirect diff --git a/src/k8splugin/go.sum b/src/k8splugin/go.sum index 453a45d9..401999b8 100644 --- a/src/k8splugin/go.sum +++ b/src/k8splugin/go.sum @@ -261,6 +261,7 @@ k8s.io/client-go v9.0.0+incompatible h1:/PdJjifJTjMFe0G4ESclZDcwF1+bFePTJ2xf+MXj k8s.io/client-go v9.0.0+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s= k8s.io/client-go v10.0.0+incompatible h1:h3fciHPG0O5QEzATTFoRw/YGtDsU6pxrMrAhxiTtcq0= k8s.io/client-go v10.0.0+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s= +k8s.io/client-go v11.0.0+incompatible h1:X3ykd+Z4G8MojP9TVDOR+h/IrpYJEolfR8W2B/FGKrk= k8s.io/helm v2.12.1+incompatible h1:Fw6it7ALJfqbbX95U3is3aswD6E8nh4aUYtvgzfna8A= k8s.io/helm v2.12.1+incompatible/go.mod h1:LZzlS4LQBHfciFOurYBFkCMTaZ0D1l+p0teMg7TSULI= k8s.io/helm v2.12.2+incompatible h1:vtddbkiGNMOd8maDDZDc111Rm9E5JeeNWDndows18i8= diff --git a/src/k8splugin/internal/app/client.go b/src/k8splugin/internal/app/client.go index fa5fdfd5..cd1ec8a2 100644 --- a/src/k8splugin/internal/app/client.go +++ b/src/k8splugin/internal/app/client.go @@ -21,17 +21,30 @@ import ( utils "k8splugin/internal" pkgerrors "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/client-go/discovery" + "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" + "k8s.io/client-go/restmapper" "k8s.io/client-go/tools/clientcmd" "k8s.io/helm/pkg/tiller" ) -type kubernetesClient struct { - clientSet *kubernetes.Clientset +// KubernetesResource is the interface that is implemented +type KubernetesResource interface { + Create(yamlFilePath string, namespace string, client *KubernetesClient) (string, error) + Delete(kind string, name string, namespace string, client *KubernetesClient) error +} + +type KubernetesClient struct { + clientSet *kubernetes.Clientset + dynamicClient dynamic.Interface + discoverClient *discovery.DiscoveryClient + restMapper meta.RESTMapper } // GetKubeClient loads the Kubernetes configuation values stored into the local configuration file -func (k *kubernetesClient) init(configPath string) error { +func (k *KubernetesClient) init(configPath string) error { if configPath == "" { return pkgerrors.New("config not passed and is not found in ~/.kube. ") } @@ -46,10 +59,20 @@ func (k *kubernetesClient) init(configPath string) error { return err } + k.dynamicClient, err = dynamic.NewForConfig(config) + if err != nil { + return pkgerrors.Wrap(err, "Creating dynamic client") + } + + k.discoverClient, err = discovery.NewDiscoveryClientForConfig(config) + if err != nil { + return pkgerrors.Wrap(err, "Creating discovery client") + } + return nil } -func (k *kubernetesClient) ensureNamespace(namespace string) error { +func (k *KubernetesClient) ensureNamespace(namespace string) error { namespacePlugin, ok := utils.LoadedPlugins["namespace"] if !ok { return pkgerrors.New("No plugin for namespace resource found") @@ -82,7 +105,51 @@ func (k *kubernetesClient) ensureNamespace(namespace string) error { return nil } -func (k *kubernetesClient) createKind(kind string, files []string, namespace string) ([]string, error) { +func (k *KubernetesClient) createGeneric(kind string, files []string, namespace string) ([]string, error) { + + log.Println("Processing items of Kind: " + kind) + + //Check if have the mapper before loading the plugin + err := k.updateMapper() + if err != nil { + return nil, pkgerrors.Wrap(err, "Unable to create RESTMapper") + } + + pluginObject, ok := utils.LoadedPlugins["generic"] + if !ok { + return nil, pkgerrors.New("No generic plugin found") + } + + symbol, err := pluginObject.Lookup("ExportedVariable") + if err != nil { + return nil, pkgerrors.Wrap(err, "No ExportedVariable symbol found") + } + + genericPlugin, ok := symbol.(KubernetesResource) + if !ok { + return nil, pkgerrors.New("ExportedVariable is not KubernetesResource type") + } + + //Iterate over each file of a particular kind here + var resourcesCreated []string + for _, f := range files { + if _, err := os.Stat(f); os.IsNotExist(err) { + return nil, pkgerrors.New("File " + f + "does not exists") + } + + log.Println("Processing file: " + f) + + name, err := genericPlugin.Create(f, namespace, k) + if err != nil { + return nil, pkgerrors.Wrap(err, "Error in generic plugin") + } + + resourcesCreated = append(resourcesCreated, name) + } + return resourcesCreated, nil +} + +func (k *KubernetesClient) createKind(kind string, files []string, namespace string) ([]string, error) { log.Println("Processing items of Kind: " + kind) @@ -103,7 +170,8 @@ func (k *kubernetesClient) createKind(kind string, files []string, namespace str typePlugin, ok := utils.LoadedPlugins[strings.ToLower(kind)] if !ok { - return nil, pkgerrors.New("No plugin for kind " + kind + " found") + log.Println("No plugin for kind " + kind + " found. Using generic Plugin") + return k.createGeneric(kind, files, namespace) } symCreateResourceFunc, err := typePlugin.Lookup("Create") @@ -123,7 +191,7 @@ func (k *kubernetesClient) createKind(kind string, files []string, namespace str return resourcesCreated, nil } -func (k *kubernetesClient) createResources(resMap map[string][]string, +func (k *KubernetesClient) createResources(resMap map[string][]string, namespace string) (map[string][]string, error) { err := k.ensureNamespace(namespace) @@ -163,17 +231,47 @@ func (k *kubernetesClient) createResources(resMap map[string][]string, return createdResourceMap, nil } -func (k *kubernetesClient) deleteKind(kind string, resources []string, namespace string) error { +func (k *KubernetesClient) deleteGeneric(kind string, resources []string, namespace string) error { + log.Println("Deleting items of Kind: " + kind) + + pluginObject, ok := utils.LoadedPlugins["generic"] + if !ok { + return pkgerrors.New("No generic plugin found") + } + + symbol, err := pluginObject.Lookup("ExportedVariable") + if err != nil { + return pkgerrors.Wrap(err, "No ExportedVariable symbol found") + } + + //Assert that it implements the KubernetesResource + genericPlugin, ok := symbol.(KubernetesResource) + if !ok { + return pkgerrors.New("ExportedVariable is not KubernetesResource type") + } + + for _, res := range resources { + err = genericPlugin.Delete(kind, res, namespace, k) + if err != nil { + return pkgerrors.Wrap(err, "Error in generic plugin") + } + } + + return nil +} + +func (k *KubernetesClient) deleteKind(kind string, resources []string, namespace string) error { log.Println("Deleting items of Kind: " + kind) typePlugin, ok := utils.LoadedPlugins[strings.ToLower(kind)] if !ok { - return pkgerrors.New("No plugin for resource " + kind + " found") + log.Println("No plugin for kind " + kind + " found. Using generic Plugin") + return k.deleteGeneric(kind, resources, namespace) } symDeleteResourceFunc, err := typePlugin.Lookup("Delete") if err != nil { - return pkgerrors.Wrap(err, "Error fetching "+kind+" plugin") + return pkgerrors.Wrap(err, "Error findinf Delete symbol in plugin") } for _, res := range resources { @@ -187,7 +285,7 @@ func (k *kubernetesClient) deleteKind(kind string, resources []string, namespace return nil } -func (k *kubernetesClient) deleteResources(resMap map[string][]string, namespace string) error { +func (k *KubernetesClient) deleteResources(resMap map[string][]string, namespace string) error { //TODO: Investigate if deletion should be in a particular order for kind, resourceNames := range resMap { err := k.deleteKind(kind, resourceNames, namespace) @@ -198,3 +296,29 @@ func (k *kubernetesClient) deleteResources(resMap map[string][]string, namespace return nil } + +func (k *KubernetesClient) updateMapper() error { + //Create restMapper if not already done + if k.restMapper != nil { + return nil + } + + groupResources, err := restmapper.GetAPIGroupResources(k.discoverClient) + if err != nil { + return pkgerrors.Wrap(err, "Get GroupResources") + } + + k.restMapper = restmapper.NewDiscoveryRESTMapper(groupResources) + return nil +} + +//GetMapper returns the RESTMapper that was created for this client +func (k *KubernetesClient) GetMapper() meta.RESTMapper { + return k.restMapper +} + +//GetDynamicClient returns the dynamic client that is needed for +//unstructured REST calls to the apiserver +func (k *KubernetesClient) GetDynamicClient() dynamic.Interface { + return k.dynamicClient +} diff --git a/src/k8splugin/internal/app/client_test.go b/src/k8splugin/internal/app/client_test.go index 5999cfa0..b3436431 100644 --- a/src/k8splugin/internal/app/client_test.go +++ b/src/k8splugin/internal/app/client_test.go @@ -45,7 +45,7 @@ func LoadMockPlugins(krdLoadedPlugins map[string]*plugin.Plugin) error { func TestInit(t *testing.T) { t.Run("Successfully create Kube Client", func(t *testing.T) { - kubeClient := kubernetesClient{} + kubeClient := KubernetesClient{} err := kubeClient.init("../../mock_files/mock_configs/mock_config") if err != nil { t.Fatalf("TestGetKubeClient returned an error (%s)", err) @@ -71,7 +71,7 @@ func TestCreateResources(t *testing.T) { t.Fatalf("LoadMockPlugins returned an error (%s)", err) } - k8 := kubernetesClient{ + k8 := KubernetesClient{ clientSet: &kubernetes.Clientset{}, } @@ -100,7 +100,7 @@ func TestDeleteResources(t *testing.T) { t.Fatalf("LoadMockPlugins returned an error (%s)", err) } - k8 := kubernetesClient{ + k8 := KubernetesClient{ clientSet: &kubernetes.Clientset{}, } diff --git a/src/k8splugin/internal/app/instance.go b/src/k8splugin/internal/app/instance.go index a5b35fef..93305c30 100644 --- a/src/k8splugin/internal/app/instance.go +++ b/src/k8splugin/internal/app/instance.go @@ -118,7 +118,7 @@ func (v *InstanceClient) Create(i InstanceRequest) (InstanceResponse, error) { return InstanceResponse{}, pkgerrors.Wrap(err, "Error resolving helm charts") } - k8sClient := kubernetesClient{} + k8sClient := KubernetesClient{} err = k8sClient.init(os.Getenv("KUBE_CONFIG_DIR") + "/" + i.CloudRegion) if err != nil { return InstanceResponse{}, pkgerrors.Wrap(err, "Getting CloudRegion Information") @@ -183,7 +183,7 @@ func (v *InstanceClient) Delete(id string) error { return pkgerrors.Wrap(err, "Error getting Instance") } - k8sClient := kubernetesClient{} + k8sClient := KubernetesClient{} err = k8sClient.init(os.Getenv("KUBE_CONFIG_DIR") + "/" + inst.CloudRegion) if err != nil { return pkgerrors.Wrap(err, "Getting CloudRegion Information") diff --git a/src/k8splugin/internal/auth/auth.go b/src/k8splugin/internal/auth/auth.go new file mode 100644 index 00000000..3da8f2af --- /dev/null +++ b/src/k8splugin/internal/auth/auth.go @@ -0,0 +1,107 @@ +/* + * Copyright 2018 Intel Corporation, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package auth + +import ( + "crypto/tls" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "io/ioutil" + "log" + + pkgerrors "github.com/pkg/errors" +) + +// GetTLSConfig initializes a tlsConfig using the CA's certificate +// This config is then used to enable the server for mutual TLS +func GetTLSConfig(caCertFile string, certFile string, keyFile string) (*tls.Config, error) { + + // Initialize tlsConfig once + caCert, err := ioutil.ReadFile(caCertFile) + + if err != nil { + return nil, pkgerrors.Wrap(err, "Read CA Cert file") + } + + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(caCert) + + tlsConfig := &tls.Config{ + // Change to RequireAndVerify once we have mandatory certs + ClientAuth: tls.VerifyClientCertIfGiven, + ClientCAs: caCertPool, + MinVersion: tls.VersionTLS12, + } + + certPEMBlk, err := readPEMBlock(certFile) + if err != nil { + return nil, pkgerrors.Wrap(err, "Read Cert File") + } + + keyPEMBlk, err := readPEMBlock(keyFile) + if err != nil { + return nil, pkgerrors.Wrap(err, "Read Key File") + } + + tlsConfig.Certificates = make([]tls.Certificate, 1) + tlsConfig.Certificates[0], err = tls.X509KeyPair(certPEMBlk, keyPEMBlk) + if err != nil { + return nil, pkgerrors.Wrap(err, "Load x509 cert and key") + } + + tlsConfig.BuildNameToCertificate() + return tlsConfig, nil +} + +func readPEMBlock(filename string) ([]byte, error) { + + pemData, err := ioutil.ReadFile(filename) + if err != nil { + return nil, pkgerrors.Wrap(err, "Read PEM File") + } + + pemBlock, rest := pem.Decode(pemData) + if len(rest) > 0 { + log.Println("Pemfile has extra data") + } + + if x509.IsEncryptedPEMBlock(pemBlock) { + password, err := ioutil.ReadFile(filename + ".pass") + if err != nil { + return nil, pkgerrors.Wrap(err, "Read Password File") + } + + pByte, err := base64.StdEncoding.DecodeString(string(password)) + if err != nil { + return nil, pkgerrors.Wrap(err, "Decode PEM Password") + } + + pemData, err = x509.DecryptPEMBlock(pemBlock, pByte) + if err != nil { + return nil, pkgerrors.Wrap(err, "Decrypt PEM Data") + } + var newPEMBlock pem.Block + newPEMBlock.Type = pemBlock.Type + newPEMBlock.Bytes = pemData + // Converting back to PEM from DER data you get from + // DecryptPEMBlock + pemData = pem.EncodeToMemory(&newPEMBlock) + } + + return pemData, nil +} diff --git a/src/k8splugin/internal/auth/auth_test.go b/src/k8splugin/internal/auth/auth_test.go new file mode 100644 index 00000000..49494eee --- /dev/null +++ b/src/k8splugin/internal/auth/auth_test.go @@ -0,0 +1,47 @@ +/* +* Copyright 2018 TechMahindra +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. + */ + +package auth + +import ( + "crypto/tls" + "testing" +) + +//Unit test to varify GetTLSconfig func and varify the tls config min version to be 771 +//Assuming cert file name as auth_test.cert +func TestGetTLSConfig(t *testing.T) { + _, err := GetTLSConfig("filedoesnotexist.cert", "filedoesnotexist.cert", "filedoesnotexist.cert") + if err == nil { + t.Errorf("Test failed, expected error but got none") + } + tlsConfig, err := GetTLSConfig("../../mock_files/mock_certs/auth_test_certificate.pem", + "../../mock_files/mock_certs/auth_test_certificate.pem", + "../../mock_files/mock_certs/auth_test_key.pem") + if err != nil { + t.Fatal("Test Failed as GetTLSConfig returned error: " + err.Error()) + } + expected := tls.VersionTLS12 + actual := tlsConfig.MinVersion + if tlsConfig != nil { + if int(actual) != expected { + t.Errorf("Test Failed due to version mismatch") + } + if tlsConfig == nil { + t.Errorf("Test Failed due to GetTLSConfig returned nil") + } + } +} diff --git a/src/k8splugin/mock_files/mock_certs/auth_test_certificate.pem b/src/k8splugin/mock_files/mock_certs/auth_test_certificate.pem new file mode 100644 index 00000000..42e77491 --- /dev/null +++ b/src/k8splugin/mock_files/mock_certs/auth_test_certificate.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDXTCCAkWgAwIBAgIJAKAHJi8eUs73MA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTgwNTE1MDQ0MDQwWhcNMTkwNTE1MDQ0MDQwWjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEA5PHDk+RRFh5o3Xe2nZuLn0Vo+5BjnHp/ul2NNYSG00Slc8F86gW4xcNA +wm6xC8tYCSangV2lFG3E8H2L7SCEVaM5VDV2GCOpOoMihc+2Qenk/YbHwuYenwjo +OgTK4aCItqjcAJ2DB1KC7AxARxHBDu9Kif+M/pc49so+G9ObQuS8k2vmTTaRYkMK +ZvbTJcWsc0vbNnPhxcG5PVj4unlaREM+yQDm18/glUkkBNnYKMHABRvPnBrDktTT +BQWsqkbQTw7ZuLOrl9rCzVTstZX9wEXarrstIdQJj3KZjbFOp2opND8bjNIjcdVt +iRFnP1nHQYr7EgRqcx/YMJZ+gmCy3wIDAQABo1AwTjAdBgNVHQ4EFgQU9qPNwwIu +kZh41sJqFtnMC2blSYMwHwYDVR0jBBgwFoAU9qPNwwIukZh41sJqFtnMC2blSYMw +DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEA4+daLY1wE10IMPaOKurG +QBvfYeO/rgNXGeg0TisTIKAfx/We9Hmwo/37Bd2Nk5gxfy/DIJ4lMbrzXjgWITlm +XOrS5QfluwvaEcREtHFtPFa3NZqn2VzKNDFTR+rJj7I5o600NKdcPrGeQ1i/vny2 +K0g68ogw2jfufcuePvZTYGst8RclomPr7ZXxI24kIjcE1MbiViy68sQueTXBEr5s +Th6RsvPfVnLxjR/m/V6VJl31nn4T6hbmKzXCHo/X7aC3I8Isui4bQGKgfAxyBkhE +0T7tP+GgymiEKQ6qJ/1c4HFFSuFRUQjLnK7uJu9jM/HMKoLCPayx6birHZRIMF94 +pg== +-----END CERTIFICATE----- diff --git a/src/k8splugin/mock_files/mock_certs/auth_test_key.pem b/src/k8splugin/mock_files/mock_certs/auth_test_key.pem new file mode 100644 index 00000000..5f01f572 --- /dev/null +++ b/src/k8splugin/mock_files/mock_certs/auth_test_key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDk8cOT5FEWHmjd +d7adm4ufRWj7kGOcen+6XY01hIbTRKVzwXzqBbjFw0DCbrELy1gJJqeBXaUUbcTw +fYvtIIRVozlUNXYYI6k6gyKFz7ZB6eT9hsfC5h6fCOg6BMrhoIi2qNwAnYMHUoLs +DEBHEcEO70qJ/4z+lzj2yj4b05tC5LyTa+ZNNpFiQwpm9tMlxaxzS9s2c+HFwbk9 +WPi6eVpEQz7JAObXz+CVSSQE2dgowcAFG8+cGsOS1NMFBayqRtBPDtm4s6uX2sLN +VOy1lf3ARdquuy0h1AmPcpmNsU6naik0PxuM0iNx1W2JEWc/WcdBivsSBGpzH9gw +ln6CYLLfAgMBAAECggEAYB3H2D0QddLKf8AUoNJ+qZ1AV+zkhPtAyIMiF4fN+sBl +HdXrlWxViGFSvM4v8h2qlhzuUfd4qLz042ox5pmyNSnTlbDkJXpDP9dyFO+BOubx +Ribhksd9r5LTvBfq/RKikt0NkAyQx/AyGtuB2NRxUs3PY2QwU2o1dhauQIx0MH5/ +6D8PgQf6+5njKQaKa4e8Kp4kB+KjnALvt6JgYuNJUHWap+nnDbuuVy5dl1bKkAZ+ +qa7CITKWO4kE2EqaCb2asFc2w3538+w72UJZtwQCmOaxtKpRSl9fQXu54N8jIGoZ +1FvEj5x3X6QkglE+iVJYaX3RmiJ3uzZ2LICDr89vEQKBgQD7fquIw4p1idSxz3Cm +5o3Y5kD0CKm61ZaRJWKd+tNlSsxubmV9HROYW6vj2xEPSDvyp1na00pDXxMJQLLc +O5Awd1SaU+d45jnoi70fsEY8X0WH1rDTYfnU+zQBmpbGqX5qTIfpy4yoADiUD1CQ +EBdaSBWiKPx2jFSct58TwDP9YwKBgQDpC64TScZYz7uQq4gAbDso/7TjNwgt/Bw8 +JgLSdx1UdUclh81smTujsouyCFwJSvRjKik8e/Qt0f5patukFbFRINxUGUDhOKbA +7zqeNQbeYaP7Rvw+3z01CU2BTBmB/EWa2xWDam8B9xQvjiHSOrubqkt3sIQJb045 +hzuigdV7VQKBgQD7Gnd0nyCwyMSIIMGuswYv6X4y6i9lr3qdQ4GakOTe/vbsz+cf +K5f0CJuwbnszEgFg/zzVIx/D8rqUA3hSMlp+ObdMO7gi22Q4TsWvTRZjkxBeV7rH +48xJneNIMqyWgIcK5YzSn3y6BTZ4hm3+2UInz09iUJ/6UZTtwNzhIIgIVwKBgQCT +LxRHDE4gIzrT+PHRSonmr/DfnA8nc9WlS2B26lH02IkRs/5Su0iGb6p4y3zNRbCp +vKQElki2c60ZiSqlLCosEfP1jWmDlRMEQVMlPlpTMxmtBr0jPDzc9T4lDhoCFYEk +d3/T2vG3LQRrsHm92+hHPTuioTIS/2BJRxar4RIibQKBgQC8zoayoQ7zfEYxy3Ax +OSao8g85hj0EAJk/VKQP2POgz6KoPay3JE9D7P7OvkebTyv/pijAuTPby4XipCNI +K0JbFC2Kn7RW/ZV23UdnoO9crh2omOh+/52prStWXKoc+/pJe70Af+4rU7FUiI7F +y1mIE9krIoVis6iYsyFEmkP7iw== +-----END PRIVATE KEY----- diff --git a/src/k8splugin/plugins/generic/plugin.go b/src/k8splugin/plugins/generic/plugin.go new file mode 100644 index 00000000..b0cf609c --- /dev/null +++ b/src/k8splugin/plugins/generic/plugin.go @@ -0,0 +1,109 @@ +/* +Copyright 2018 Intel Corporation. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "log" + + pkgerrors "github.com/pkg/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/kubernetes/scheme" + + utils "k8splugin/internal" + "k8splugin/internal/app" +) + +type genericPlugin struct { +} + +var kindToGVRMap = map[string]schema.GroupVersionResource{ + "ConfigMap": schema.GroupVersionResource{Group: "", Version: "v1", Resource: "configmaps"}, + "StatefulSet": schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "statefulsets"}, + "Job": schema.GroupVersionResource{Group: "batch", Version: "v1", Resource: "jobs"}, + "Pod": schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}, + "DaemonSet": schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "daemonsets"}, + "CustomResourceDefinition": schema.GroupVersionResource{ + Group: "apiextensions.k8s.io", Version: "v1beta1", Resource: "customresourcedefinitions", + }, +} + +// Create deployment object in a specific Kubernetes cluster +func (g genericPlugin) Create(yamlFilePath string, namespace string, client *app.KubernetesClient) (string, error) { + if namespace == "" { + namespace = "default" + } + + //Decode the yaml file to create a runtime.Object + obj, err := utils.DecodeYAML(yamlFilePath, nil) + if err != nil { + return "", pkgerrors.Wrap(err, "Decode deployment object error") + } + + //Convert the runtime.Object to an unstructured Object + unstruct := &unstructured.Unstructured{} + err = scheme.Scheme.Convert(obj, unstruct, nil) + if err != nil { + return "", pkgerrors.Wrap(err, "Converting to unstructured object") + } + + dynClient := client.GetDynamicClient() + mapper := client.GetMapper() + + gvk := unstruct.GroupVersionKind() + mapping, err := mapper.RESTMapping(schema.GroupKind{Group: gvk.Group, Kind: gvk.Kind}, gvk.Version) + if err != nil { + return "", pkgerrors.Wrap(err, "Mapping kind to resource error") + } + + gvr := mapping.Resource + + createdObj, err := dynClient.Resource(gvr).Namespace(namespace).Create(unstruct, metav1.CreateOptions{}) + if err != nil { + return "", pkgerrors.Wrap(err, "Create object error") + } + + return createdObj.GetName(), nil +} + +// Delete an existing deployment hosted in a specific Kubernetes cluster +func (g genericPlugin) Delete(kind string, name string, namespace string, client *app.KubernetesClient) error { + if namespace == "" { + namespace = "default" + } + + deletePolicy := metav1.DeletePropagationForeground + opts := &metav1.DeleteOptions{ + PropagationPolicy: &deletePolicy, + } + + dynClient := client.GetDynamicClient() + gvr, ok := kindToGVRMap[kind] + if !ok { + return pkgerrors.New("GVR not found for: " + kind) + } + + log.Printf("Using gvr: %s, %s, %s", gvr.Group, gvr.Version, gvr.Resource) + + err := dynClient.Resource(gvr).Namespace(namespace).Delete(name, opts) + if err != nil { + return pkgerrors.Wrap(err, "Delete object error") + } + + return nil +} + +// ExportedVariable is what we will look for when calling the generic plugin +var ExportedVariable genericPlugin |