diff options
author | Shashank Kumar Shankar <shashank.kumar.shankar@intel.com> | 2018-08-20 15:50:50 -0700 |
---|---|---|
committer | Victor Morales <victor.morales@intel.com> | 2018-08-24 15:51:16 -0700 |
commit | a1373742a2c3f980360e4980f3b23b0ff3480ae6 (patch) | |
tree | ce2fb583dea15b8a546d794d21786fdf0f666539 | |
parent | 6ff216219ccb4567baeb34c9dba73daabb60f629 (diff) |
Seed code for k8s multicloud plugin
This patch provides the initial seed code for the multicloud Kubernetes
plugin and also provides the plugin feature to add new Kubernetes
kinds.
Change-Id: Ie5ee414656665070cde2834c4855ac2ebc179a9a
Issue-ID: MULTICLOUD-301
Signed-off-by: Shashank Kumar Shankar <shashank.kumar.shankar@intel.com>
Signed-off-by: Victor Morales <victor.morales@intel.com>
34 files changed, 2933 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..f0b583fe --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# IDE +.DS_Store +.vscode +*-workspace + +# Directories +pkg +bin +target +src/github.com +src/golang.org +src/k8splugin/vendor +src/k8splugin/kubeconfig/* +deployments/k8plugin + +# Binaries +*.so +src/k8splugin/csar/mock_plugins/*.so +src/k8splugin/plugins/**/*.so + +# Tests +*.test +*.out diff --git a/.gitreview b/.gitreview new file mode 100644 index 00000000..aab7df3c --- /dev/null +++ b/.gitreview @@ -0,0 +1,4 @@ +[gerrit] +host=gerrit.onap.org +port=29418 +project=multicloud/k8s.git diff --git a/README.md b/README.md new file mode 100644 index 00000000..0e62378f --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +<!-- 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. --> + +# MultiCloud-k8-plugin + +MultiCloud Kubernetes plugin for ONAP multicloud. + +# Installation + +Requirements: +* Go 1.10 +* Dep + +Steps: + +* Clone repo in GOPATH src: + * `cd $GOPATH/src && git clone https://git.onap.org/multicloud/k8s` + +* Run unit tests: + * `make build` + +* Compile to build Binary: + * `make deploy` + +# Archietecture + +Create Virtual Network Function + +![Create VNF](./doc/create_vnf.png) + +Create Virtual Link + +![Create VL](./doc/create_vl.png) diff --git a/deployments/Dockerfile b/deployments/Dockerfile new file mode 100644 index 00000000..407af509 --- /dev/null +++ b/deployments/Dockerfile @@ -0,0 +1,21 @@ +FROM debian:jessie + +ARG HTTP_PROXY=${HTTP_PROXY} +ARG HTTPS_PROXY=${HTTPS_PROXY} + +ENV http_proxy $HTTP_PROXY +ENV https_proxy $HTTPS_PROXY +ENV no_proxy $NO_PROXY + +ENV CSAR_DIR "/opt/csar" +ENV KUBE_CONFIG_DIR "/opt/kubeconfig" +ENV DATABASE_TYPE "consul" +ENV DATABASE_IP "127.0.0.1" + +EXPOSE 8081 + +WORKDIR /opt/multicloud/k8s +ADD ./k8plugin ./ +ADD ./*.so ./ + +CMD ["./k8plugin"] diff --git a/deployments/build.sh b/deployments/build.sh new file mode 100755 index 00000000..667be5f5 --- /dev/null +++ b/deployments/build.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# SPDX-license-identifier: Apache-2.0 +############################################################################## +# Copyright (c) 2018 Intel Corporation +# 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 nounset +set -o pipefail +set -o xtrace + +function generate_binary { + GOPATH=$(go env GOPATH) + rm -f k8plugin + rm -f *.so + $GOPATH/bin/dep ensure -v + for plugin in deployment namespace service; do + CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -buildmode=plugin -a -tags netgo -o ./$plugin.so ../src/k8splugin/plugins/$plugin/plugin.go + done + CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -a -tags netgo -o ./k8plugin ../src/k8splugin/cmd/main.go +} + +function build_image { + echo "Start build docker image." + docker-compose build --no-cache +} + +generate_binary +build_image diff --git a/deployments/docker-compose.yml b/deployments/docker-compose.yml new file mode 100644 index 00000000..0d347b13 --- /dev/null +++ b/deployments/docker-compose.yml @@ -0,0 +1,41 @@ +# 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. + +version: '3' + +services: + multicloud-k8s: + image: nexus3.onap.org:10003/onap/multicloud/k8plugin + build: + context: ./ + args: + - HTTP_PROXY=$HTTP_PROXY + - HTTPS_PROXY=$HTTPS_PROXY + ports: + - "8081:8081" + environment: + - CSAR_DIR=/opt/csar + - KUBE_CONFIG_DIR=/opt/kubeconfig + - DATABASE_TYPE=consul + - DATABASE_IP=consul-svr + depends_on: + - "consul" + volumes: + - /opt/csar:/opt/csar + - /opt/kubeconfig:/opt/kubeconfig + consul: + image: consul + hostname: consul-svr + environment: + - CONSUL_LOCAL_CONFIG={"datacenter":"us_west","server":true} + command: ["agent", "-server", "-bootstrap-expect=1"] + volumes: + - /opt/consul/config:/consul/config diff --git a/doc/create_vl.png b/doc/create_vl.png Binary files differnew file mode 100644 index 00000000..803b6da8 --- /dev/null +++ b/doc/create_vl.png diff --git a/doc/create_vnf.png b/doc/create_vnf.png Binary files differnew file mode 100644 index 00000000..27b50d79 --- /dev/null +++ b/doc/create_vnf.png diff --git a/doc/sampleCommands.rst b/doc/sampleCommands.rst new file mode 100644 index 00000000..e8b53cf3 --- /dev/null +++ b/doc/sampleCommands.rst @@ -0,0 +1,71 @@ +# 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. + +# Sample Commands: + +* POST + URL:`localhost:8081/v1/vnf_instances/cloudregion1/namespacetest` + Request Body: + + ``` + { + "cloud_region_id": "region1", + "csar_id": "uuid", + "namespace": "test", + "oof_parameters": [{ + "key1": "value1", + "key2": "value2", + "key3": {} + }], + "network_parameters": { + "oam_ip_address": { + "connection_point": "string", + "ip_address": "string", + "workload_name": "string" + } + } + } + ``` + + Expected Response: + ``` + { + "response": "Created Deployment:nginx-deployment" + } + ``` + + The above POST request will download the following YAML file and run it on the Kubernetes cluster. + + ``` + apiVersion: apps/v1 + kind: Deployment + metadata: + name: nginx-deployment + labels: + app: nginx + spec: + replicas: 3 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.7.9 + ports: + - containerPort: 80 + ``` +* GET + URL: `localhost:8081/v1/vnf_instances` diff --git a/doc/swagger.yaml b/doc/swagger.yaml new file mode 100644 index 00000000..3b7e36ba --- /dev/null +++ b/doc/swagger.yaml @@ -0,0 +1,184 @@ +# 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. + +swagger: "2.0" +info: + description: "API reference for MultiCloud Kubernetes Plugin." + version: "1.0.0" + title: "API reference for MultiCloud Kubernetes Plugin." + contact: + url: "https://wiki.onap.org/display/DW/Support+for+K8S+%28Kubernetes%29+based+Cloud+regions" + license: + name: "Apache 2.0" + url: "http://www.apache.org/licenses/LICENSE-2.0.html" +basePath: "/v1.0" +schemes: +- "http" +paths: + /vnf_instances: + post: + tags: + - "Deployment of VNF Containers" + summary: "Create Kubernetes based VNFs." + description: "Endpoint to create Kubernetes based VNFs." + consumes: + - "application/json" + produces: + - "application/json" + parameters: + - in: "body" + name: "body" + description: "Create new VNF containers" + required: true + schema: + $ref: "#/definitions/POSTRequest" + responses: + 200: + description: "successful operation" + schema: + $ref: "#/definitions/POSTResponse" + /vnf_instances/: + get: + tags: + - "Deployment of VNF Containers" + summary: "List all Kubernetes based VNFs." + description: "Endpoint to list all Kubernetes based VNF." + produces: + - "application/json" + responses: + 200: + description: "successful operation" + schema: + $ref: "#/definitions/GETSResponse" + /vnf_instances/{name}: + get: + tags: + - "Deployment of VNF Containers" + summary: "Get details of a Kubernetes based VNFs." + description: "Endpoint to get details of a Kubernetes based VNFs." + produces: + - "application/json" + parameters: + - name: "name" + in: "path" + description: "Name used to query" + required: true + type: "string" + responses: + 200: + description: "successful operation" + schema: + $ref: "#/definitions/GETResponse" + patch: + tags: + - "Deployment of VNF Containers" + summary: "Update a Kubernetes based VNFs." + description: "Endp to update a Kubernetes based VNFs." + produces: + - "application/json" + parameters: + - name: "name" + in: "path" + description: "Name used to patch" + required: true + type: "string" + - name: "body" + in: "body" + description: "Patch an existing Kubernetes based VNFs." + required: true + schema: + $ref: "#/definitions/PATCHRequest" + responses: + 200: + description: "successful operation" + schema: + $ref: "#/definitions/PATCHResponse" + delete: + tags: + - "Deployment of VNF Containers" + summary: "Delete a Kubernetes based VNFs." + description: "Endpoint to delete a Kubernetes based VNFs." + produces: + - "application/json" + parameters: + - name: "name" + in: "path" + description: "Name used to delete" + required: true + type: "string" + responses: + 200: + description: "successful operation" + schema: + $ref: "#/definitions/DELETEResponse" +definitions: + POSTRequest: + type: "object" + properties: + cloud_region_id: + type: "string" + csar_id: + type: "string" + namespace: + type: "string" + oof_parameters: + items: + type: "object" + additionalProperties: true + example: + key1: value1 + key2: value2 + key3: {} + network_parameters: + type: "object" + properties: + oam_ip_address: + type: "object" + properties: + connection_point: + type: "string" + ip_address: + type: "string" + workload_name: + type: "string" + POSTResponse: + type: "object" + properties: + vnf_id: + type: "string" + name: + type: "string" + GETSResponse: + type: "object" + properties: + vnf_list: + items: + type: "string" + GETResponse: + type: "object" + properties: + response: + type: "string" + PATCHRequest: + type: "object" + properties: + name: + type: "string" + PATCHResponse: + type: "object" + properties: + response: + type: "string" + DELETEResponse: + type: "object" + properties: + response: + type: "string"
\ No newline at end of file diff --git a/src/k8splugin/Gopkg.lock b/src/k8splugin/Gopkg.lock new file mode 100644 index 00000000..e0276839 --- /dev/null +++ b/src/k8splugin/Gopkg.lock @@ -0,0 +1,353 @@ +# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. + + +[[projects]] + name = "github.com/ghodss/yaml" + packages = ["."] + revision = "0ca9ea5df5451ffdf184b4428c902747c2c11cd7" + version = "v1.0.0" + +[[projects]] + name = "github.com/gogo/protobuf" + packages = [ + "proto", + "sortkeys" + ] + revision = "1adfc126b41513cc696b209667c8656ea7aac67c" + version = "v1.0.0" + +[[projects]] + branch = "master" + name = "github.com/golang/glog" + packages = ["."] + revision = "23def4e6c14b4da8ac2ed8007337bc5eb5007998" + +[[projects]] + name = "github.com/golang/protobuf" + packages = [ + "proto", + "ptypes", + "ptypes/any", + "ptypes/duration", + "ptypes/timestamp" + ] + revision = "b4deda0973fb4c70b50d226b1af49f3da59f5265" + version = "v1.1.0" + +[[projects]] + branch = "master" + name = "github.com/google/gofuzz" + packages = ["."] + revision = "24818f796faf91cd76ec7bddd72458fbced7a6c1" + +[[projects]] + name = "github.com/googleapis/gnostic" + packages = [ + "OpenAPIv2", + "compiler", + "extensions" + ] + revision = "7c663266750e7d82587642f65e60bc4083f1f84e" + version = "v0.2.0" + +[[projects]] + name = "github.com/gorilla/context" + packages = ["."] + revision = "08b5f424b9271eedf6f9f0ce86cb9396ed337a42" + version = "v1.1.1" + +[[projects]] + name = "github.com/gorilla/handlers" + packages = ["."] + revision = "90663712d74cb411cbef281bc1e08c19d1a76145" + version = "v1.3.0" + +[[projects]] + name = "github.com/gorilla/mux" + packages = ["."] + revision = "e3702bed27f0d39777b0b37b664b6280e8ef8fbf" + version = "v1.6.2" + +[[projects]] + name = "github.com/hashicorp/consul" + packages = ["api"] + revision = "e716d1b5f8be252b3e53906c6d5632e0228f30fa" + version = "v1.2.2" + +[[projects]] + branch = "master" + name = "github.com/hashicorp/go-cleanhttp" + packages = ["."] + revision = "d5fe4b57a186c716b0e00b8c301cbd9b4182694d" + +[[projects]] + branch = "master" + name = "github.com/hashicorp/go-rootcerts" + packages = ["."] + revision = "6bb64b370b90e7ef1fa532be9e591a81c3493e00" + +[[projects]] + name = "github.com/hashicorp/serf" + packages = ["coordinate"] + revision = "d6574a5bb1226678d7010325fb6c985db20ee458" + version = "v0.8.1" + +[[projects]] + branch = "master" + name = "github.com/howeyc/gopass" + packages = ["."] + revision = "bf9dde6d0d2c004a008c27aaee91170c786f6db8" + +[[projects]] + name = "github.com/imdario/mergo" + packages = ["."] + revision = "9316a62528ac99aaecb4e47eadd6dc8aa6533d58" + version = "v0.3.5" + +[[projects]] + name = "github.com/json-iterator/go" + packages = ["."] + revision = "ca39e5af3ece67bbcda3d0f4f56a8e24d9f2dad4" + version = "1.1.3" + +[[projects]] + branch = "master" + name = "github.com/mitchellh/go-homedir" + packages = ["."] + revision = "58046073cbffe2f25d425fe1331102f55cf719de" + +[[projects]] + branch = "master" + name = "github.com/mitchellh/mapstructure" + packages = ["."] + revision = "f15292f7a699fcc1a38a80977f80a046874ba8ac" + +[[projects]] + name = "github.com/modern-go/concurrent" + packages = ["."] + revision = "bacd9c7ef1dd9b15be4a9909b8ac7a4e313eec94" + version = "1.0.3" + +[[projects]] + name = "github.com/modern-go/reflect2" + packages = ["."] + revision = "1df9eeb2bb81f327b96228865c5687bc2194af3f" + version = "1.0.0" + +[[projects]] + name = "github.com/pkg/errors" + packages = ["."] + revision = "645ef00459ed84a119197bfb8d8205042c6df63d" + version = "v0.8.0" + +[[projects]] + name = "github.com/spf13/pflag" + packages = ["."] + revision = "583c0c0531f06d5278b7d917446061adc344b5cd" + version = "v1.0.1" + +[[projects]] + branch = "master" + name = "golang.org/x/crypto" + packages = ["ssh/terminal"] + revision = "8ac0e0d97ce45cd83d1d7243c060cb8461dda5e9" + +[[projects]] + branch = "master" + name = "golang.org/x/net" + packages = [ + "context", + "http/httpguts", + "http2", + "http2/hpack", + "idna" + ] + revision = "db08ff08e8622530d9ed3a0e8ac279f6d4c02196" + +[[projects]] + branch = "master" + name = "golang.org/x/sys" + packages = [ + "unix", + "windows" + ] + revision = "bff228c7b664c5fce602223a05fb708fd8654986" + +[[projects]] + name = "golang.org/x/text" + packages = [ + "collate", + "collate/build", + "internal/colltab", + "internal/gen", + "internal/tag", + "internal/triegen", + "internal/ucd", + "language", + "secure/bidirule", + "transform", + "unicode/bidi", + "unicode/cldr", + "unicode/norm", + "unicode/rangetable" + ] + revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" + version = "v0.3.0" + +[[projects]] + branch = "master" + name = "golang.org/x/time" + packages = ["rate"] + revision = "fbb02b2291d28baffd63558aa44b4b56f178d650" + +[[projects]] + name = "gopkg.in/inf.v0" + packages = ["."] + revision = "d2d2541c53f18d2a059457998ce2876cc8e67cbf" + version = "v0.9.1" + +[[projects]] + name = "gopkg.in/yaml.v2" + packages = ["."] + revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183" + version = "v2.2.1" + +[[projects]] + branch = "master" + name = "k8s.io/api" + packages = [ + "admissionregistration/v1alpha1", + "admissionregistration/v1beta1", + "apps/v1", + "apps/v1beta1", + "apps/v1beta2", + "authentication/v1", + "authentication/v1beta1", + "authorization/v1", + "authorization/v1beta1", + "autoscaling/v1", + "autoscaling/v2beta1", + "batch/v1", + "batch/v1beta1", + "batch/v2alpha1", + "certificates/v1beta1", + "core/v1", + "events/v1beta1", + "extensions/v1beta1", + "networking/v1", + "policy/v1beta1", + "rbac/v1", + "rbac/v1alpha1", + "rbac/v1beta1", + "scheduling/v1alpha1", + "settings/v1alpha1", + "storage/v1", + "storage/v1alpha1", + "storage/v1beta1" + ] + revision = "72d6e4405f8143815cbd454ab04b38210a9f32fc" + +[[projects]] + name = "k8s.io/apimachinery" + packages = [ + "pkg/api/errors", + "pkg/api/meta", + "pkg/api/resource", + "pkg/apis/meta/v1", + "pkg/apis/meta/v1/unstructured", + "pkg/apis/meta/v1beta1", + "pkg/conversion", + "pkg/conversion/queryparams", + "pkg/fields", + "pkg/labels", + "pkg/runtime", + "pkg/runtime/schema", + "pkg/runtime/serializer", + "pkg/runtime/serializer/json", + "pkg/runtime/serializer/protobuf", + "pkg/runtime/serializer/recognizer", + "pkg/runtime/serializer/streaming", + "pkg/runtime/serializer/versioning", + "pkg/selection", + "pkg/types", + "pkg/util/clock", + "pkg/util/errors", + "pkg/util/framer", + "pkg/util/intstr", + "pkg/util/json", + "pkg/util/net", + "pkg/util/runtime", + "pkg/util/sets", + "pkg/util/validation", + "pkg/util/validation/field", + "pkg/util/wait", + "pkg/util/yaml", + "pkg/version", + "pkg/watch", + "third_party/forked/golang/reflect" + ] + revision = "31dade610c053669d8054bfd847da657251e8c1a" + version = "kubernetes-1.10.3" + +[[projects]] + name = "k8s.io/client-go" + packages = [ + "discovery", + "kubernetes", + "kubernetes/scheme", + "kubernetes/typed/admissionregistration/v1alpha1", + "kubernetes/typed/admissionregistration/v1beta1", + "kubernetes/typed/apps/v1", + "kubernetes/typed/apps/v1beta1", + "kubernetes/typed/apps/v1beta2", + "kubernetes/typed/authentication/v1", + "kubernetes/typed/authentication/v1beta1", + "kubernetes/typed/authorization/v1", + "kubernetes/typed/authorization/v1beta1", + "kubernetes/typed/autoscaling/v1", + "kubernetes/typed/autoscaling/v2beta1", + "kubernetes/typed/batch/v1", + "kubernetes/typed/batch/v1beta1", + "kubernetes/typed/batch/v2alpha1", + "kubernetes/typed/certificates/v1beta1", + "kubernetes/typed/core/v1", + "kubernetes/typed/events/v1beta1", + "kubernetes/typed/extensions/v1beta1", + "kubernetes/typed/networking/v1", + "kubernetes/typed/policy/v1beta1", + "kubernetes/typed/rbac/v1", + "kubernetes/typed/rbac/v1alpha1", + "kubernetes/typed/rbac/v1beta1", + "kubernetes/typed/scheduling/v1alpha1", + "kubernetes/typed/settings/v1alpha1", + "kubernetes/typed/storage/v1", + "kubernetes/typed/storage/v1alpha1", + "kubernetes/typed/storage/v1beta1", + "pkg/apis/clientauthentication", + "pkg/apis/clientauthentication/v1alpha1", + "pkg/version", + "plugin/pkg/client/auth/exec", + "rest", + "rest/watch", + "tools/auth", + "tools/clientcmd", + "tools/clientcmd/api", + "tools/clientcmd/api/latest", + "tools/clientcmd/api/v1", + "tools/metrics", + "tools/reference", + "transport", + "util/cert", + "util/flowcontrol", + "util/homedir", + "util/integer" + ] + revision = "23781f4d6632d88e869066eaebb743857aa1ef9b" + version = "v7.0.0" + +[solve-meta] + analyzer-name = "dep" + analyzer-version = 1 + inputs-digest = "75cc26f2e82e49abeff97709158caea7f0c088191d8d4eb7a00eea2c88d00297" + solver-name = "gps-cdcl" + solver-version = 1 diff --git a/src/k8splugin/Gopkg.toml b/src/k8splugin/Gopkg.toml new file mode 100644 index 00000000..219b502d --- /dev/null +++ b/src/k8splugin/Gopkg.toml @@ -0,0 +1,46 @@ +# Gopkg.toml example +# +# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md +# for detailed Gopkg.toml documentation. +# +# required = ["github.com/user/thing/cmd/thing"] +# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] +# +# [[constraint]] +# name = "github.com/user/project" +# version = "1.0.0" +# +# [[constraint]] +# name = "github.com/user/project2" +# branch = "dev" +# source = "github.com/myfork/project2" +# +# [[override]] +# name = "github.com/x/y" +# version = "2.4.0" +# +# [prune] +# non-go = false +# go-tests = true +# unused-packages = true + + +[[constraint]] + branch = "master" + name = "k8s.io/api" + +[[constraint]] + name = "k8s.io/apimachinery" + version = "kubernetes-1.10.3" + +[[constraint]] + name = "k8s.io/client-go" + version = "7.0.0" + +[prune] + go-tests = true + unused-packages = true + +[[constraint]] + name = "github.com/pkg/errors" + version = "0.8.0" diff --git a/src/k8splugin/Makefile b/src/k8splugin/Makefile new file mode 100644 index 00000000..586eca9c --- /dev/null +++ b/src/k8splugin/Makefile @@ -0,0 +1,43 @@ +# 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. + +GOPATH := $(shell realpath "$(PWD)/../../") +DEPENDENCIES := github.com/golang/dep/cmd/dep + +export GOPATH ... + +.PHONY: plugins + +build: clean dep plugins tests +deploy: clean dep plugins build_binary tests + +build_binary: + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -tags netgo -ldflags '-w' -o ./k8plugin ./cmd/main.go + +tests: + go test -v ./... -cover + +format: + go fmt ./... + +plugins: + go build -buildmode=plugin -o ./plugins/deployment/deployment.so ./plugins/deployment/plugin.go + go build -buildmode=plugin -o ./plugins/namespace/namespace.so ./plugins/namespace/plugin.go + go build -buildmode=plugin -o ./plugins/service/service.so ./plugins/service/plugin.go + go build -buildmode=plugin -o ./mock_files/mock_plugins/mockplugin.so ./mock_files/mock_plugins/mockplugin.go + +dep: + go get -u $(DEPENDENCIES) + $(GOPATH)/bin/dep ensure + +clean: + find . -name "*so" -delete + @rm -f k8plugin diff --git a/src/k8splugin/api/api.go b/src/k8splugin/api/api.go new file mode 100644 index 00000000..651d9311 --- /dev/null +++ b/src/k8splugin/api/api.go @@ -0,0 +1,120 @@ +/* +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 ( + "os" + "path/filepath" + "plugin" + "strings" + + "github.com/gorilla/mux" + pkgerrors "github.com/pkg/errors" + + "k8splugin/db" + "k8splugin/krd" +) + +// CheckEnvVariables checks for required Environment variables +func CheckEnvVariables() error { + envList := []string{"CSAR_DIR", "KUBE_CONFIG_DIR", "DATABASE_TYPE", "DATABASE_IP"} + for _, env := range envList { + if _, ok := os.LookupEnv(env); !ok { + return pkgerrors.New("environment variable " + env + " not set") + } + } + + return nil +} + +// CheckDatabaseConnection checks if the database is up and running and +// plugin can talk to it +func CheckDatabaseConnection() error { + err := db.CreateDBClient(os.Getenv("DATABASE_TYPE")) + if err != nil { + return pkgerrors.Cause(err) + } + + err = db.DBconn.InitializeDatabase() + if err != nil { + return pkgerrors.Cause(err) + } + + err = db.DBconn.CheckDatabase() + if err != nil { + return pkgerrors.Cause(err) + } + return nil +} + +// LoadPlugins loads all the compiled .so plugins +func LoadPlugins() error { + pluginsDir, ok := os.LookupEnv("PLUGINS_DIR") + if !ok { + pluginsDir, _ = filepath.Abs(filepath.Dir(os.Args[0])) + } + err := filepath.Walk(pluginsDir, + func(path string, info os.FileInfo, err error) error { + if strings.Contains(path, ".so") { + p, err := plugin.Open(path) + if err != nil { + return pkgerrors.Cause(err) + } + + krd.LoadedPlugins[info.Name()[:len(info.Name())-3]] = p + } + return err + }) + if err != nil { + return err + } + + return nil +} + +// CheckInitialSettings is used to check initial settings required to start api +func CheckInitialSettings() error { + err := CheckEnvVariables() + if err != nil { + return pkgerrors.Cause(err) + } + + err = CheckDatabaseConnection() + if err != nil { + return pkgerrors.Cause(err) + } + + err = LoadPlugins() + if err != nil { + return pkgerrors.Cause(err) + } + + return nil +} + +// NewRouter creates a router instance that serves the VNFInstance web methods +func NewRouter(kubeconfig string) *mux.Router { + router := mux.NewRouter() + + vnfInstanceHandler := router.PathPrefix("/v1/vnf_instances").Subrouter() + vnfInstanceHandler.HandleFunc("/", CreateHandler).Methods("POST").Name("VNFCreation") + vnfInstanceHandler.HandleFunc("/{cloudRegionID}/{namespace}", ListHandler).Methods("GET") + vnfInstanceHandler.HandleFunc("/{cloudRegionID}/{namespace}/{externalVNFID}", DeleteHandler).Methods("DELETE") + vnfInstanceHandler.HandleFunc("/{cloudRegionID}/{namespace}/{externalVNFID}", GetHandler).Methods("GET") + + // (TODO): Fix update method + // vnfInstanceHandler.HandleFunc("/{vnfInstanceId}", UpdateHandler).Methods("PUT") + + return router +} diff --git a/src/k8splugin/api/handler.go b/src/k8splugin/api/handler.go new file mode 100644 index 00000000..27d060aa --- /dev/null +++ b/src/k8splugin/api/handler.go @@ -0,0 +1,377 @@ +/* +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" + "errors" + "log" + "net/http" + "os" + "strings" + + "github.com/gorilla/mux" + pkgerrors "github.com/pkg/errors" + "k8s.io/client-go/kubernetes" + + "k8splugin/csar" + "k8splugin/db" + "k8splugin/krd" +) + +// GetVNFClient retrieves the client used to communicate with a Kubernetes Cluster +var GetVNFClient = func(kubeConfigPath string) (kubernetes.Clientset, error) { + client, err := krd.GetKubeClient(kubeConfigPath) + if err != nil { + return client, err + } + return client, nil +} + +func validateBody(body interface{}) error { + switch b := body.(type) { + case CreateVnfRequest: + if b.CloudRegionID == "" { + werr := pkgerrors.Wrap(errors.New("Invalid/Missing CloudRegionID in POST request"), "CreateVnfRequest bad request") + return werr + } + if b.CsarID == "" { + werr := pkgerrors.Wrap(errors.New("Invalid/Missing CsarID in POST request"), "CreateVnfRequest bad request") + return werr + } + if strings.Contains(b.CloudRegionID, "|") || strings.Contains(b.Namespace, "|") { + werr := pkgerrors.Wrap(errors.New("Character \"|\" not allowed in CSAR ID"), "CreateVnfRequest bad request") + return werr + } + case UpdateVnfRequest: + if b.CloudRegionID == "" || b.CsarID == "" { + werr := pkgerrors.Wrap(errors.New("Invalid/Missing Data in PUT request"), "UpdateVnfRequest bad request") + return werr + } + } + return nil +} + +// CreateHandler is the POST method creates a new VNF instance resource. +func CreateHandler(w http.ResponseWriter, r *http.Request) { + var resource CreateVnfRequest + + if r.Body == nil { + http.Error(w, "Body empty", http.StatusBadRequest) + return + } + + err := json.NewDecoder(r.Body).Decode(&resource) + if err != nil { + http.Error(w, err.Error(), http.StatusUnprocessableEntity) + return + } + + err = validateBody(resource) + if err != nil { + http.Error(w, err.Error(), http.StatusUnprocessableEntity) + return + } + + // (TODO): Read kubeconfig for specific Cloud Region from local file system + // if present or download it from AAI + // err := DownloadKubeConfigFromAAI(resource.CloudRegionID, os.Getenv("KUBE_CONFIG_DIR") + kubeclient, err := GetVNFClient(os.Getenv("KUBE_CONFIG_DIR") + "/" + resource.CloudRegionID) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + /* + uuid, + { + "deployment": ["cloud1-default-uuid-sisedeploy1", "cloud1-default-uuid-sisedeploy2", ... ] + "service": ["cloud1-default-uuid-sisesvc1", "cloud1-default-uuid-sisesvc2", ... ] + }, + nil + */ + externalVNFID, resourceNameMap, err := csar.CreateVNF(resource.CsarID, resource.CloudRegionID, resource.Namespace, &kubeclient) + if err != nil { + werr := pkgerrors.Wrap(err, "Read Kubernetes Data information error") + http.Error(w, werr.Error(), http.StatusInternalServerError) + return + } + + // cloud1-default-uuid + internalVNFID := resource.CloudRegionID + "-" + resource.Namespace + "-" + externalVNFID + + // Persist in AAI database. + log.Printf("Cloud Region ID: %s, Namespace: %s, VNF ID: %s ", resource.CloudRegionID, resource.Namespace, externalVNFID) + + // TODO: Uncomment when annotations are done + // krd.AddNetworkAnnotationsToPod(kubeData, resource.Networks) + + // "{"deployment":<>,"service":<>}" + out, err := json.Marshal(resourceNameMap) + if err != nil { + werr := pkgerrors.Wrap(err, "Create VNF deployment error") + http.Error(w, werr.Error(), http.StatusInternalServerError) + return + } + serializedResourceNameMap := string(out) + + // key: cloud1-default-uuid + // value: "{"deployment":<>,"service":<>}" + err = db.DBconn.CreateEntry(internalVNFID, serializedResourceNameMap) + if err != nil { + werr := pkgerrors.Wrap(err, "Create VNF deployment error") + http.Error(w, werr.Error(), http.StatusInternalServerError) + return + } + + resp := CreateVnfResponse{ + VNFID: externalVNFID, + CloudRegionID: resource.CloudRegionID, + Namespace: resource.Namespace, + VNFComponents: resourceNameMap, + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(resp) +} + +// ListHandler the existing VNF instances created in a given Kubernetes cluster +func ListHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + + cloudRegionID := vars["cloudRegionID"] + namespace := vars["namespace"] + prefix := cloudRegionID + "-" + namespace + + internalVNFIDs, err := db.DBconn.ReadAll(prefix) + if err != nil { + werr := pkgerrors.Wrap(err, "Get VNF list error") + http.Error(w, werr.Error(), http.StatusInternalServerError) + return + } + + if len(internalVNFIDs) == 0 { + w.WriteHeader(http.StatusNotFound) + return + } + + // TODO: There is an edge case where if namespace is passed but is missing some characters + // trailing, it will print the result with those excluding characters. This is because of + // the way I am trimming the Prefix. This fix is needed. + + var editedList []string + + for _, id := range internalVNFIDs { + if len(id) > 0 { + editedList = append(editedList, strings.TrimPrefix(id, prefix)[1:]) + } + } + + if len(editedList) == 0 { + editedList = append(editedList, "") + } + + resp := ListVnfsResponse{ + VNFs: editedList, + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(resp) +} + +// DeleteHandler method terminates an individual VNF instance. +func DeleteHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + + cloudRegionID := vars["cloudRegionID"] // cloud1 + namespace := vars["namespace"] // default + externalVNFID := vars["externalVNFID"] // uuid + + // cloud1-default-uuid + internalVNFID := cloudRegionID + "-" + namespace + "-" + externalVNFID + + // (TODO): Read kubeconfig for specific Cloud Region from local file system + // if present or download it from AAI + // err := DownloadKubeConfigFromAAI(resource.CloudRegionID, os.Getenv("KUBE_CONFIG_DIR") + kubeclient, err := GetVNFClient(os.Getenv("KUBE_CONFIG_DIR") + "/" + cloudRegionID) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // key: cloud1-default-uuid + // value: "{"deployment":<>,"service":<>}" + serializedResourceNameMap, found, err := db.DBconn.ReadEntry(internalVNFID) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if found == false { + w.WriteHeader(http.StatusNotFound) + return + } + + /* + { + "deployment": ["cloud1-default-uuid-sisedeploy1", "cloud1-default-uuid-sisedeploy2", ... ] + "service": ["cloud1-default-uuid-sisesvc1", "cloud1-default-uuid-sisesvc2", ... ] + }, + */ + deserializedResourceNameMap := make(map[string][]string) + err = json.Unmarshal([]byte(serializedResourceNameMap), &deserializedResourceNameMap) + if err != nil { + werr := pkgerrors.Wrap(err, "Delete VNF error") + http.Error(w, werr.Error(), http.StatusInternalServerError) + return + } + + err = csar.DestroyVNF(deserializedResourceNameMap, namespace, &kubeclient) + if err != nil { + werr := pkgerrors.Wrap(err, "Delete VNF error") + http.Error(w, werr.Error(), http.StatusInternalServerError) + return + } + + err = db.DBconn.DeleteEntry(internalVNFID) + if err != nil { + werr := pkgerrors.Wrap(err, "Delete VNF error") + http.Error(w, werr.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) +} + +// // UpdateHandler method to update a VNF instance. +// func UpdateHandler(w http.ResponseWriter, r *http.Request) { +// vars := mux.Vars(r) +// id := vars["vnfInstanceId"] + +// var resource UpdateVnfRequest + +// if r.Body == nil { +// http.Error(w, "Body empty", http.StatusBadRequest) +// return +// } + +// err := json.NewDecoder(r.Body).Decode(&resource) +// if err != nil { +// http.Error(w, err.Error(), http.StatusUnprocessableEntity) +// return +// } + +// err = validateBody(resource) +// if err != nil { +// http.Error(w, err.Error(), http.StatusUnprocessableEntity) +// return +// } + +// kubeData, err := utils.ReadCSARFromFileSystem(resource.CsarID) + +// if kubeData.Deployment == nil { +// werr := pkgerrors.Wrap(err, "Update VNF deployment error") +// http.Error(w, werr.Error(), http.StatusInternalServerError) +// return +// } +// kubeData.Deployment.SetUID(types.UID(id)) + +// if err != nil { +// werr := pkgerrors.Wrap(err, "Update VNF deployment information error") +// http.Error(w, werr.Error(), http.StatusInternalServerError) +// return +// } + +// // (TODO): Read kubeconfig for specific Cloud Region from local file system +// // if present or download it from AAI +// s, err := NewVNFInstanceService("../kubeconfig/config") +// if err != nil { +// http.Error(w, err.Error(), http.StatusInternalServerError) +// return +// } + +// err = s.Client.UpdateDeployment(kubeData.Deployment, resource.Namespace) +// if err != nil { +// werr := pkgerrors.Wrap(err, "Update VNF error") + +// http.Error(w, werr.Error(), http.StatusInternalServerError) +// return +// } + +// resp := UpdateVnfResponse{ +// DeploymentID: id, +// } + +// w.Header().Set("Content-Type", "application/json") +// w.WriteHeader(http.StatusCreated) + +// err = json.NewEncoder(w).Encode(resp) +// if err != nil { +// werr := pkgerrors.Wrap(err, "Parsing output of new VNF error") +// http.Error(w, werr.Error(), http.StatusInternalServerError) +// } +// } + +// GetHandler retrieves information about a VNF instance by reading an individual VNF instance resource. +func GetHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + + cloudRegionID := vars["cloudRegionID"] // cloud1 + namespace := vars["namespace"] // default + externalVNFID := vars["externalVNFID"] // uuid + + // cloud1-default-uuid + internalVNFID := cloudRegionID + "-" + namespace + "-" + externalVNFID + + // key: cloud1-default-uuid + // value: "{"deployment":<>,"service":<>}" + serializedResourceNameMap, found, err := db.DBconn.ReadEntry(internalVNFID) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if found == false { + w.WriteHeader(http.StatusNotFound) + return + } + + /* + { + "deployment": ["cloud1-default-uuid-sisedeploy1", "cloud1-default-uuid-sisedeploy2", ... ] + "service": ["cloud1-default-uuid-sisesvc1", "cloud1-default-uuid-sisesvc2", ... ] + }, + */ + deserializedResourceNameMap := make(map[string][]string) + err = json.Unmarshal([]byte(serializedResourceNameMap), &deserializedResourceNameMap) + if err != nil { + werr := pkgerrors.Wrap(err, "Get VNF error") + http.Error(w, werr.Error(), http.StatusInternalServerError) + return + } + + resp := GetVnfResponse{ + VNFID: externalVNFID, + CloudRegionID: cloudRegionID, + Namespace: namespace, + VNFComponents: deserializedResourceNameMap, + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(resp) +} diff --git a/src/k8splugin/api/handler_test.go b/src/k8splugin/api/handler_test.go new file mode 100644 index 00000000..df573d94 --- /dev/null +++ b/src/k8splugin/api/handler_test.go @@ -0,0 +1,316 @@ +/* +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" + "k8s.io/client-go/kubernetes" + "net/http" + "net/http/httptest" + "reflect" + "testing" + + "k8splugin/csar" + "k8splugin/db" +) + +type mockDB struct { + db.DatabaseConnection +} + +func (c *mockDB) InitializeDatabase() error { + return nil +} + +func (c *mockDB) CheckDatabase() error { + return nil +} + +func (c *mockDB) CreateEntry(key string, value string) error { + return nil +} + +func (c *mockDB) ReadEntry(key string) (string, bool, error) { + str := "{\"deployment\":[\"cloud1-default-uuid-sisedeploy\"],\"service\":[\"cloud1-default-uuid-sisesvc\"]}" + return str, true, nil +} + +func (c *mockDB) DeleteEntry(key string) error { + return nil +} + +func (c *mockDB) ReadAll(key string) ([]string, error) { + returnVal := []string{"cloud1-default-uuid1", "cloud1-default-uuid2"} + return returnVal, nil +} + +func executeRequest(req *http.Request) *httptest.ResponseRecorder { + router := NewRouter("") + recorder := httptest.NewRecorder() + router.ServeHTTP(recorder, req) + + return recorder +} + +func checkResponseCode(t *testing.T, expected, actual int) { + if expected != actual { + t.Errorf("Expected response code %d. Got %d\n", expected, actual) + } +} + +func TestVNFInstanceCreation(t *testing.T) { + t.Run("Succesful create a VNF", func(t *testing.T) { + payload := []byte(`{ + "cloud_region_id": "region1", + "namespace": "test", + "csar_id": "UUID-1", + "oof_parameters": [{ + "key1": "value1", + "key2": "value2", + "key3": {} + }], + "network_parameters": { + "oam_ip_address": { + "connection_point": "string", + "ip_address": "string", + "workload_name": "string" + } + } + }`) + + data := map[string][]string{ + "deployment": []string{"cloud1-default-uuid-sisedeploy"}, + "service": []string{"cloud1-default-uuid-sisesvc"}, + } + + expected := &CreateVnfResponse{ + VNFID: "test_UUID", + CloudRegionID: "region1", + Namespace: "test", + VNFComponents: data, + } + + var result CreateVnfResponse + + req, _ := http.NewRequest("POST", "/v1/vnf_instances/", bytes.NewBuffer(payload)) + + GetVNFClient = func(configPath string) (kubernetes.Clientset, error) { + return kubernetes.Clientset{}, nil + } + + csar.CreateVNF = func(id string, r string, n string, kubeclient *kubernetes.Clientset) (string, map[string][]string, error) { + return "externaluuid", data, nil + } + + db.DBconn = &mockDB{} + + response := executeRequest(req) + checkResponseCode(t, http.StatusCreated, response.Code) + + err := json.NewDecoder(response.Body).Decode(&result) + if err != nil { + t.Fatalf("TestVNFInstanceCreation returned:\n result=%v\n expected=%v", err, expected.VNFComponents) + } + }) + t.Run("Missing body failure", func(t *testing.T) { + req, _ := http.NewRequest("POST", "/v1/vnf_instances/", nil) + response := executeRequest(req) + + checkResponseCode(t, http.StatusBadRequest, response.Code) + }) + t.Run("Invalid JSON request format", func(t *testing.T) { + payload := []byte("invalid") + req, _ := http.NewRequest("POST", "/v1/vnf_instances/", bytes.NewBuffer(payload)) + response := executeRequest(req) + checkResponseCode(t, http.StatusUnprocessableEntity, response.Code) + }) + t.Run("Missing parameter failure", func(t *testing.T) { + payload := []byte(`{ + "csar_id": "testID", + "oof_parameters": { + "key_values": { + "key1": "value1", + "key2": "value2" + } + }, + "vnf_instance_name": "test", + "vnf_instance_description": "vRouter_test_description" + }`) + req, _ := http.NewRequest("POST", "/v1/vnf_instances/", bytes.NewBuffer(payload)) + response := executeRequest(req) + checkResponseCode(t, http.StatusUnprocessableEntity, response.Code) + }) +} + +func TestVNFInstancesRetrieval(t *testing.T) { + t.Run("Succesful get a list of VNF", func(t *testing.T) { + expected := &ListVnfsResponse{ + VNFs: []string{"uuid1", "uuid2"}, + } + var result ListVnfsResponse + + req, _ := http.NewRequest("GET", "/v1/vnf_instances/cloud1/default", nil) + + db.DBconn = &mockDB{} + + response := executeRequest(req) + checkResponseCode(t, http.StatusOK, response.Code) + + err := json.NewDecoder(response.Body).Decode(&result) + if err != nil { + t.Fatalf("TestVNFInstancesRetrieval returned:\n result=%v\n expected=list", err) + } + if !reflect.DeepEqual(*expected, result) { + t.Fatalf("TestVNFInstancesRetrieval returned:\n result=%v\n expected=%v", result, *expected) + } + }) + t.Run("Get empty list", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/v1/vnf_instances/cloudregion1/testnamespace", nil) + db.DBconn = &mockDB{} + response := executeRequest(req) + checkResponseCode(t, http.StatusOK, response.Code) + }) +} + +func TestVNFInstanceDeletion(t *testing.T) { + t.Run("Succesful delete a VNF", func(t *testing.T) { + req, _ := http.NewRequest("DELETE", "/v1/vnf_instances/cloudregion1/testnamespace/1", nil) + + GetVNFClient = func(configPath string) (kubernetes.Clientset, error) { + return kubernetes.Clientset{}, nil + } + + csar.DestroyVNF = func(d map[string][]string, n string, kubeclient *kubernetes.Clientset) error { + return nil + } + + db.DBconn = &mockDB{} + + response := executeRequest(req) + checkResponseCode(t, http.StatusAccepted, response.Code) + + if result := response.Body.String(); result != "" { + t.Fatalf("TestVNFInstanceDeletion returned:\n result=%v\n expected=%v", result, "") + } + }) + // t.Run("Malformed delete request", func(t *testing.T) { + // req, _ := http.NewRequest("DELETE", "/v1/vnf_instances/foo", nil) + // response := executeRqequest(req) + // checkResponseCode(t, http.StatusBadRequest, response.Code) + // }) +} + +// TODO: Update this test when the UpdateVNF endpoint is fixed. +/* +func TestVNFInstanceUpdate(t *testing.T) { + t.Run("Succesful update a VNF", func(t *testing.T) { + payload := []byte(`{ + "cloud_region_id": "region1", + "csar_id": "UUID-1", + "oof_parameters": [{ + "key1": "value1", + "key2": "value2", + "key3": {} + }], + "network_parameters": { + "oam_ip_address": { + "connection_point": "string", + "ip_address": "string", + "workload_name": "string" + } + } + }`) + expected := &UpdateVnfResponse{ + DeploymentID: "1", + } + + var result UpdateVnfResponse + + req, _ := http.NewRequest("PUT", "/v1/vnf_instances/1", bytes.NewBuffer(payload)) + + GetVNFClient = func(configPath string) (krd.VNFInstanceClientInterface, error) { + return &mockClient{ + update: func() error { + return nil + }, + }, nil + } + utils.ReadCSARFromFileSystem = func(csarID string) (*krd.KubernetesData, error) { + kubeData := &krd.KubernetesData{ + Deployment: &appsV1.Deployment{}, + Service: &coreV1.Service{}, + } + return kubeData, nil + } + + response := executeRequest(req) + checkResponseCode(t, http.StatusCreated, response.Code) + + err := json.NewDecoder(response.Body).Decode(&result) + if err != nil { + t.Fatalf("TestVNFInstanceUpdate returned:\n result=%v\n expected=%v", err, expected.DeploymentID) + } + + if result.DeploymentID != expected.DeploymentID { + t.Fatalf("TestVNFInstanceUpdate returned:\n result=%v\n expected=%v", result.DeploymentID, expected.DeploymentID) + } + }) +} +*/ + +func TestVNFInstanceRetrieval(t *testing.T) { + t.Run("Succesful get a VNF", func(t *testing.T) { + + data := map[string][]string{ + "deployment": []string{"cloud1-default-uuid-sisedeploy"}, + "service": []string{"cloud1-default-uuid-sisesvc"}, + } + + expected := GetVnfResponse{ + VNFID: "1", + CloudRegionID: "cloud1", + Namespace: "default", + VNFComponents: data, + } + + req, _ := http.NewRequest("GET", "/v1/vnf_instances/cloud1/default/1", nil) + + GetVNFClient = func(configPath string) (kubernetes.Clientset, error) { + return kubernetes.Clientset{}, nil + } + + db.DBconn = &mockDB{} + + response := executeRequest(req) + checkResponseCode(t, http.StatusOK, response.Code) + + var result GetVnfResponse + + err := json.NewDecoder(response.Body).Decode(&result) + if err != nil { + t.Fatalf("TestVNFInstanceRetrieval returned:\n result=%v\n expected=%v", err, expected) + } + + if !reflect.DeepEqual(expected, result) { + t.Fatalf("TestVNFInstanceRetrieval returned:\n result=%v\n expected=%v", result, expected) + } + }) + t.Run("VNF not found", func(t *testing.T) { + req, _ := http.NewRequest("GET", "/v1/vnf_instances/cloudregion1/testnamespace/1", nil) + response := executeRequest(req) + + checkResponseCode(t, http.StatusOK, response.Code) + }) +} diff --git a/src/k8splugin/api/model.go b/src/k8splugin/api/model.go new file mode 100644 index 00000000..0e4863c4 --- /dev/null +++ b/src/k8splugin/api/model.go @@ -0,0 +1,76 @@ +/* +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 + +// CreateVnfRequest contains the VNF creation request parameters +type CreateVnfRequest struct { + CloudRegionID string `json:"cloud_region_id"` + CsarID string `json:"csar_id"` + OOFParams []map[string]interface{} `json:"oof_parameters"` + NetworkParams NetworkParameters `json:"network_parameters"` + Namespace string `json:"namespace"` + Name string `json:"vnf_instance_name"` + Description string `json:"vnf_instance_description"` +} + +// CreateVnfResponse contains the VNF creation response parameters +type CreateVnfResponse struct { + VNFID string `json:"vnf_id"` + CloudRegionID string `json:"cloud_region_id"` + Namespace string `json:"namespace"` + VNFComponents map[string][]string `json:"vnf_components"` +} + +// ListVnfsResponse contains the list of VNFs response parameters +type ListVnfsResponse struct { + VNFs []string `json:"vnf_id_list"` +} + +// NetworkParameters contains the networking info required by the VNF instance +type NetworkParameters struct { + OAMI OAMIPParams `json:"oam_ip_address"` + // Add other network parameters if necessary. +} + +// OAMIPParams contains the management networking info required by the VNF instance +type OAMIPParams struct { + ConnectionPoint string `json:"connection_point"` + IPAddress string `json:"ip_address"` + WorkLoadName string `json:"workload_name"` +} + +// UpdateVnfRequest contains the VNF creation parameters +type UpdateVnfRequest struct { + CloudRegionID string `json:"cloud_region_id"` + CsarID string `json:"csar_id"` + OOFParams []map[string]interface{} `json:"oof_parameters"` + NetworkParams NetworkParameters `json:"network_parameters"` + Namespace string `json:"namespace"` + Name string `json:"vnf_instance_name"` + Description string `json:"vnf_instance_description"` +} + +// UpdateVnfResponse contains the VNF update response parameters +type UpdateVnfResponse struct { + DeploymentID string `json:"vnf_id"` + Name string `json:"name"` +} + +// GetVnfResponse returns information about a specific VNF instance +type GetVnfResponse struct { + VNFID string `json:"vnf_id"` + CloudRegionID string `json:"cloud_region_id"` + Namespace string `json:"namespace"` + VNFComponents map[string][]string `json:"vnf_components"` +} diff --git a/src/k8splugin/cmd/main.go b/src/k8splugin/cmd/main.go new file mode 100644 index 00000000..ee676549 --- /dev/null +++ b/src/k8splugin/cmd/main.go @@ -0,0 +1,64 @@ +/* +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 ( + "context" + "flag" + "log" + "net/http" + "os" + "os/signal" + "path/filepath" + + "github.com/gorilla/handlers" + "k8s.io/client-go/util/homedir" + + "k8splugin/api" +) + +func main() { + var kubeconfig string + + home := homedir.HomeDir() + if home != "" { + kubeconfig = *flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file") + } + flag.Parse() + + err := api.CheckInitialSettings() + if err != nil { + log.Fatal(err) + } + + httpRouter := api.NewRouter(kubeconfig) + loggedRouter := handlers.LoggingHandler(os.Stdout, httpRouter) + log.Println("Starting Kubernetes Multicloud API") + + httpServer := &http.Server{ + Handler: loggedRouter, + Addr: ":8081", // Remove hardcoded port number + } + + connectionsClose := make(chan struct{}) + go func() { + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + <-c + httpServer.Shutdown(context.Background()) + close(connectionsClose) + }() + + log.Fatal(httpServer.ListenAndServe()) +} diff --git a/src/k8splugin/csar/parser.go b/src/k8splugin/csar/parser.go new file mode 100644 index 00000000..abd6ad92 --- /dev/null +++ b/src/k8splugin/csar/parser.go @@ -0,0 +1,207 @@ +/* +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 csar + +import ( + "encoding/hex" + "io/ioutil" + "log" + "math/rand" + "os" + + "k8s.io/client-go/kubernetes" + + pkgerrors "github.com/pkg/errors" + "gopkg.in/yaml.v2" + + "k8splugin/krd" +) + +func generateExternalVNFID(charLen int) string { + b := make([]byte, charLen/2) + rand.Read(b) + return hex.EncodeToString(b) +} + +// CreateVNF reads the CSAR files from the files system and creates them one by one +var CreateVNF = func(csarID string, cloudRegionID string, namespace string, kubeclient *kubernetes.Clientset) (string, map[string][]string, error) { + namespacePlugin, ok := krd.LoadedPlugins["namespace"] + if !ok { + return "", nil, pkgerrors.New("No plugin for namespace resource found") + } + + symGetNamespaceFunc, err := namespacePlugin.Lookup("GetResource") + if err != nil { + return "", nil, pkgerrors.Wrap(err, "Error fetching namespace plugin") + } + + present, err := symGetNamespaceFunc.(func(string, *kubernetes.Clientset) (bool, error))( + namespace, kubeclient) + if err != nil { + return "", nil, pkgerrors.Wrap(err, "Error in plugin namespace plugin") + } + + if present == false { + symGetNamespaceFunc, err := namespacePlugin.Lookup("CreateResource") + if err != nil { + return "", nil, pkgerrors.Wrap(err, "Error fetching namespace plugin") + } + + err = symGetNamespaceFunc.(func(string, *kubernetes.Clientset) error)( + namespace, kubeclient) + if err != nil { + return "", nil, pkgerrors.Wrap(err, "Error creating "+namespace+" namespace") + } + } + + var path string + + // uuid + externalVNFID := generateExternalVNFID(8) + + // cloud1-default-uuid + internalVNFID := cloudRegionID + "-" + namespace + "-" + externalVNFID + + csarDirPath := os.Getenv("CSAR_DIR") + "/" + csarID + metadataYAMLPath := csarDirPath + "/metadata.yaml" + + seqFile, err := ReadMetadataFile(metadataYAMLPath) + if err != nil { + return "", nil, pkgerrors.Wrap(err, "Error while reading Metadata File: "+metadataYAMLPath) + } + + resourceYAMLNameMap := make(map[string][]string) + + for _, resource := range seqFile.ResourceTypePathMap { + for resourceName, resourceFileNames := range resource { + // Load/Use Deployment data/client + + var resourceNameList []string + + for _, filename := range resourceFileNames { + path = csarDirPath + "/" + filename + + _, err = os.Stat(path) + if os.IsNotExist(err) { + return "", nil, pkgerrors.New("File " + path + "does not exists") + } + + log.Println("Processing file: " + path) + + genericKubeData := &krd.GenericKubeResourceData{ + YamlFilePath: path, + Namespace: namespace, + InternalVNFID: internalVNFID, + } + + typePlugin, ok := krd.LoadedPlugins[resourceName] + if !ok { + return "", nil, pkgerrors.New("No plugin for resource " + resourceName + " found") + } + + symCreateResourceFunc, err := typePlugin.Lookup("CreateResource") + if err != nil { + return "", nil, pkgerrors.Wrap(err, "Error fetching "+resourceName+" plugin") + } + + // cloud1-default-uuid-sisedeploy + internalResourceName, err := symCreateResourceFunc.(func(*krd.GenericKubeResourceData, *kubernetes.Clientset) (string, error))( + genericKubeData, kubeclient) + if err != nil { + return "", nil, pkgerrors.Wrap(err, "Error in plugin "+resourceName+" plugin") + } + + // ["cloud1-default-uuid-sisedeploy1", "cloud1-default-uuid-sisedeploy2", ... ] + resourceNameList = append(resourceNameList, internalResourceName) + + /* + { + "deployment": ["cloud1-default-uuid-sisedeploy1", "cloud1-default-uuid-sisedeploy2", ... ] + } + */ + resourceYAMLNameMap[resourceName] = resourceNameList + } + } + } + + /* + uuid, + { + "deployment": ["cloud1-default-uuid-sisedeploy1", "cloud1-default-uuid-sisedeploy2", ... ] + "service": ["cloud1-default-uuid-sisesvc1", "cloud1-default-uuid-sisesvc2", ... ] + }, + nil + */ + return externalVNFID, resourceYAMLNameMap, nil +} + +// DestroyVNF deletes VNFs based on data passed +var DestroyVNF = func(data map[string][]string, namespace string, kubeclient *kubernetes.Clientset) error { + /* data: + { + "deployment": ["cloud1-default-uuid-sisedeploy1", "cloud1-default-uuid-sisedeploy2", ... ] + "service": ["cloud1-default-uuid-sisesvc1", "cloud1-default-uuid-sisesvc2", ... ] + }, + */ + + for resourceName, resourceList := range data { + typePlugin, ok := krd.LoadedPlugins[resourceName] + if !ok { + return pkgerrors.New("No plugin for resource " + resourceName + " found") + } + + symDeleteResourceFunc, err := typePlugin.Lookup("DeleteResource") + if err != nil { + return pkgerrors.Wrap(err, "Error fetching "+resourceName+" plugin") + } + + for _, resourceName := range resourceList { + + log.Println("Deleting resource: " + resourceName) + + err = symDeleteResourceFunc.(func(string, string, *kubernetes.Clientset) error)( + resourceName, namespace, kubeclient) + if err != nil { + return pkgerrors.Wrap(err, "Error destroying "+resourceName) + } + } + } + + return nil +} + +// MetadataFile stores the metadata of execution +type MetadataFile struct { + ResourceTypePathMap []map[string][]string `yaml:"resources"` +} + +// ReadMetadataFile reads the metadata yaml to return the order or reads +var ReadMetadataFile = func(yamlFilePath string) (MetadataFile, error) { + var seqFile MetadataFile + + if _, err := os.Stat(yamlFilePath); err == nil { + log.Println("Reading metadata YAML: " + yamlFilePath) + rawBytes, err := ioutil.ReadFile(yamlFilePath) + if err != nil { + return seqFile, pkgerrors.Wrap(err, "Metadata YAML file read error") + } + + err = yaml.Unmarshal(rawBytes, &seqFile) + if err != nil { + return seqFile, pkgerrors.Wrap(err, "Metadata YAML file read error") + } + } + + return seqFile, nil +} diff --git a/src/k8splugin/csar/parser_test.go b/src/k8splugin/csar/parser_test.go new file mode 100644 index 00000000..cec5395e --- /dev/null +++ b/src/k8splugin/csar/parser_test.go @@ -0,0 +1,130 @@ +/* +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 csar + +import ( + "io/ioutil" + "k8s.io/client-go/kubernetes" + "log" + "os" + "plugin" + "testing" + + pkgerrors "github.com/pkg/errors" + "gopkg.in/yaml.v2" + + "k8splugin/krd" +) + +func LoadMockPlugins(krdLoadedPlugins *map[string]*plugin.Plugin) error { + if _, err := os.Stat("../mock_files/mock_plugins/mockplugin.so"); os.IsNotExist(err) { + return pkgerrors.New("mockplugin.so does not exist. Please compile mockplugin.go to generate") + } + + mockPlugin, err := plugin.Open("../mock_files/mock_plugins/mockplugin.so") + if err != nil { + return pkgerrors.Cause(err) + } + + (*krdLoadedPlugins)["namespace"] = mockPlugin + (*krdLoadedPlugins)["deployment"] = mockPlugin + (*krdLoadedPlugins)["service"] = mockPlugin + + return nil +} + +func TestCreateVNF(t *testing.T) { + oldkrdPluginData := krd.LoadedPlugins + oldReadMetadataFile := ReadMetadataFile + + defer func() { + krd.LoadedPlugins = oldkrdPluginData + ReadMetadataFile = oldReadMetadataFile + }() + + err := LoadMockPlugins(&krd.LoadedPlugins) + if err != nil { + t.Fatalf("TestCreateVNF returned an error (%s)", err) + } + + ReadMetadataFile = func(yamlFilePath string) (MetadataFile, error) { + var seqFile MetadataFile + + if _, err := os.Stat(yamlFilePath); err == nil { + rawBytes, err := ioutil.ReadFile("../mock_files/mock_yamls/metadata.yaml") + if err != nil { + return seqFile, pkgerrors.Wrap(err, "Metadata YAML file read error") + } + + err = yaml.Unmarshal(rawBytes, &seqFile) + if err != nil { + return seqFile, pkgerrors.Wrap(err, "Metadata YAML file unmarshall error") + } + } + + return seqFile, nil + } + + kubeclient := kubernetes.Clientset{} + + t.Run("Successfully create VNF", func(t *testing.T) { + externaluuid, data, err := CreateVNF("uuid", "cloudregion1", "test", &kubeclient) + if err != nil { + t.Fatalf("TestCreateVNF returned an error (%s)", err) + } + + log.Println(externaluuid) + + if data == nil { + t.Fatalf("TestCreateVNF returned empty data (%s)", data) + } + }) + +} + +func TestDeleteVNF(t *testing.T) { + oldkrdPluginData := krd.LoadedPlugins + + defer func() { + krd.LoadedPlugins = oldkrdPluginData + }() + + err := LoadMockPlugins(&krd.LoadedPlugins) + if err != nil { + t.Fatalf("TestCreateVNF returned an error (%s)", err) + } + + kubeclient := kubernetes.Clientset{} + + t.Run("Successfully delete VNF", func(t *testing.T) { + data := map[string][]string{ + "deployment": []string{"cloud1-default-uuid-sisedeploy"}, + "service": []string{"cloud1-default-uuid-sisesvc"}, + } + + err := DestroyVNF(data, "test", &kubeclient) + if err != nil { + t.Fatalf("TestCreateVNF returned an error (%s)", err) + } + }) +} + +func TestReadMetadataFile(t *testing.T) { + t.Run("Successfully read Metadata YAML file", func(t *testing.T) { + _, err := ReadMetadataFile("../mock_files//mock_yamls/metadata.yaml") + if err != nil { + t.Fatalf("TestReadMetadataFile returned an error (%s)", err) + } + }) +} diff --git a/src/k8splugin/db/DB.go b/src/k8splugin/db/DB.go new file mode 100644 index 00000000..c8895088 --- /dev/null +++ b/src/k8splugin/db/DB.go @@ -0,0 +1,42 @@ +/* +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 db + +import ( + pkgerrors "github.com/pkg/errors" +) + +// DBconn interface used to talk a concrete Database connection +var DBconn DatabaseConnection + +// DatabaseConnection is an interface for accessing a database +type DatabaseConnection interface { + InitializeDatabase() error + CheckDatabase() error + CreateEntry(string, string) error + ReadEntry(string) (string, bool, error) + DeleteEntry(string) error + ReadAll(string) ([]string, error) +} + +// CreateDBClient creates the DB client +var CreateDBClient = func(dbType string) error { + switch dbType { + case "consul": + DBconn = &ConsulDB{} + return nil + default: + return pkgerrors.New(dbType + "DB not supported") + } +} diff --git a/src/k8splugin/db/consul.go b/src/k8splugin/db/consul.go new file mode 100644 index 00000000..9ab0d826 --- /dev/null +++ b/src/k8splugin/db/consul.go @@ -0,0 +1,112 @@ +/* +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 db + +import ( + consulapi "github.com/hashicorp/consul/api" + pkgerrors "github.com/pkg/errors" + "os" +) + +// ConsulDB is an implementation of the DatabaseConnection interface +type ConsulDB struct { + consulClient *consulapi.Client +} + +// InitializeDatabase initialized the initial steps +func (c *ConsulDB) InitializeDatabase() error { + if os.Getenv("DATABASE_IP") == "" { + return pkgerrors.New("DATABASE_IP environment variable not set.") + } + config := consulapi.DefaultConfig() + config.Address = os.Getenv("DATABASE_IP") + ":8500" + + client, err := consulapi.NewClient(config) + if err != nil { + return err + } + c.consulClient = client + return nil +} + +// CheckDatabase checks if the database is running +func (c *ConsulDB) CheckDatabase() error { + kv := c.consulClient.KV() + _, _, err := kv.Get("test", nil) + if err != nil { + return pkgerrors.New("[ERROR] Cannot talk to Datastore. Check if it is running/reachable.") + } + return nil +} + +// CreateEntry is used to create a DB entry +func (c *ConsulDB) CreateEntry(key string, value string) error { + kv := c.consulClient.KV() + + p := &consulapi.KVPair{Key: key, Value: []byte(value)} + + _, err := kv.Put(p, nil) + + if err != nil { + return err + } + + return nil +} + +// ReadEntry returns the internalID for a particular externalID is present in a namespace +func (c *ConsulDB) ReadEntry(key string) (string, bool, error) { + + kv := c.consulClient.KV() + + pair, _, err := kv.Get(key, nil) + + if pair == nil { + return string("No value found for ID: " + key), false, err + } + return string(pair.Value), true, err +} + +// DeleteEntry is used to delete an ID +func (c *ConsulDB) DeleteEntry(key string) error { + + kv := c.consulClient.KV() + + _, err := kv.Delete(key, nil) + + if err != nil { + return err + } + + return nil +} + +// ReadAll is used to get all ExternalIDs in a namespace +func (c *ConsulDB) ReadAll(prefix string) ([]string, error) { + kv := c.consulClient.KV() + + pairs, _, err := kv.List(prefix, nil) + + if len(pairs) == 0 { + return []string{""}, err + } + + var res []string + + for _, keypair := range pairs { + res = append(res, keypair.Key) + } + + return res, err +} diff --git a/src/k8splugin/db/db_test.go b/src/k8splugin/db/db_test.go new file mode 100644 index 00000000..7ad252f5 --- /dev/null +++ b/src/k8splugin/db/db_test.go @@ -0,0 +1,40 @@ +/* +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 db + +import ( + "reflect" + "testing" +) + +func TestCreateDBClient(t *testing.T) { + oldDBconn := DBconn + + defer func() { + DBconn = oldDBconn + }() + + t.Run("Successfully create DB client", func(t *testing.T) { + expectedDB := ConsulDB{} + + err := CreateDBClient("consul") + if err != nil { + t.Fatalf("TestCreateDBClient returned an error (%s)", err) + } + + if !reflect.DeepEqual(DBconn, &expectedDB) { + t.Fatalf("TestCreateDBClient set DBconn as:\n result=%v\n expected=%v", DBconn, expectedDB) + } + }) +} diff --git a/src/k8splugin/krd/krd.go b/src/k8splugin/krd/krd.go new file mode 100644 index 00000000..2d06e104 --- /dev/null +++ b/src/k8splugin/krd/krd.go @@ -0,0 +1,44 @@ +/* +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 krd + +import ( + "errors" + + pkgerrors "github.com/pkg/errors" + + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" +) + +// GetKubeClient loads the Kubernetes configuation values stored into the local configuration file +var GetKubeClient = func(configPath string) (kubernetes.Clientset, error) { + var clientset *kubernetes.Clientset + + if configPath == "" { + return *clientset, errors.New("config not passed and is not found in ~/.kube. ") + } + + config, err := clientcmd.BuildConfigFromFlags("", configPath) + if err != nil { + return kubernetes.Clientset{}, pkgerrors.Wrap(err, "setConfig: Build config from flags raised an error") + } + + clientset, err = kubernetes.NewForConfig(config) + if err != nil { + return *clientset, err + } + + return *clientset, nil +} diff --git a/src/k8splugin/krd/krd_test.go b/src/k8splugin/krd/krd_test.go new file mode 100644 index 00000000..7047a74c --- /dev/null +++ b/src/k8splugin/krd/krd_test.go @@ -0,0 +1,34 @@ +/* +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 krd + +import ( + "reflect" + "testing" +) + +func TestGetKubeClient(t *testing.T) { + t.Run("Successfully create Kube Client", func(t *testing.T) { + + clientset, err := GetKubeClient("../mock_files/mock_configs/mock_config") + if err != nil { + t.Fatalf("TestGetKubeClient returned an error (%s)", err) + } + + if reflect.TypeOf(clientset).Name() != "Clientset" { + t.Fatalf("TestGetKubeClient returned :\n result=%v\n expected=%v", clientset, "Clientset") + } + + }) +} diff --git a/src/k8splugin/krd/plugins.go b/src/k8splugin/krd/plugins.go new file mode 100644 index 00000000..612e3f6b --- /dev/null +++ b/src/k8splugin/krd/plugins.go @@ -0,0 +1,44 @@ +/* +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 krd + +import ( + "plugin" + + appsV1 "k8s.io/api/apps/v1" + coreV1 "k8s.io/api/core/v1" + "k8s.io/client-go/kubernetes" +) + +// LoadedPlugins stores references to the stored plugins +var LoadedPlugins = map[string]*plugin.Plugin{} + +// KubeResourceClient has the signature methods to create Kubernetes reources +type KubeResourceClient interface { + CreateResource(GenericKubeResourceData, *kubernetes.Clientset) (string, error) + ListResources(string, string) (*[]string, error) + DeleteResource(string, string, *kubernetes.Clientset) error + GetResource(string, string, *kubernetes.Clientset) (string, error) +} + +// GenericKubeResourceData stores all supported Kubernetes plugin types +type GenericKubeResourceData struct { + YamlFilePath string + Namespace string + InternalVNFID string + + // Add additional Kubernetes plugins below kinds + DeploymentData *appsV1.Deployment + ServiceData *coreV1.Service +} diff --git a/src/k8splugin/mock_files/mock_configs/mock_config b/src/k8splugin/mock_files/mock_configs/mock_config new file mode 100644 index 00000000..9b86ff15 --- /dev/null +++ b/src/k8splugin/mock_files/mock_configs/mock_config @@ -0,0 +1,29 @@ +# 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. + +apiVersion: v1 +kind: Config +clusters: +- name: local + cluster: + insecure-skip-tls-verify: true + server: https://192.168.43.66:6443 +contexts: +- context: + cluster: local + user: admin + name: kubelet-context +current-context: kubelet-context +users: +- name: admin + user: + password: admin + username: admin diff --git a/src/k8splugin/mock_files/mock_plugins/mockplugin.go b/src/k8splugin/mock_files/mock_plugins/mockplugin.go new file mode 100644 index 00000000..9ceec342 --- /dev/null +++ b/src/k8splugin/mock_files/mock_plugins/mockplugin.go @@ -0,0 +1,43 @@ +/* +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 ( + "k8s.io/client-go/kubernetes" + + "k8splugin/krd" +) + +func main() {} + +// CreateResource object in a specific Kubernetes resource +func CreateResource(kubedata *krd.GenericKubeResourceData, kubeclient *kubernetes.Clientset) (string, error) { + return "externalUUID", nil +} + +// ListResources of existing resources +func ListResources(limit int64, namespace string, kubeclient *kubernetes.Clientset) (*[]string, error) { + returnVal := []string{"cloud1-default-uuid1", "cloud1-default-uuid2"} + return &returnVal, nil +} + +// DeleteResource existing resources +func DeleteResource(name string, namespace string, kubeclient *kubernetes.Clientset) error { + return nil +} + +// GetResource existing resource host +func GetResource(namespace string, client *kubernetes.Clientset) (bool, error) { + return true, nil +} diff --git a/src/k8splugin/mock_files/mock_yamls/deployment.yaml b/src/k8splugin/mock_files/mock_yamls/deployment.yaml new file mode 100644 index 00000000..eff2fc5a --- /dev/null +++ b/src/k8splugin/mock_files/mock_yamls/deployment.yaml @@ -0,0 +1,24 @@ +# 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. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: sise-deploy +spec: + template: + metadata: + labels: + app: sise + spec: + containers: + - name: sise + image: mhausenblas/simpleservice:0.5.0
\ No newline at end of file diff --git a/src/k8splugin/mock_files/mock_yamls/metadata.yaml b/src/k8splugin/mock_files/mock_yamls/metadata.yaml new file mode 100644 index 00000000..dcc1c32e --- /dev/null +++ b/src/k8splugin/mock_files/mock_yamls/metadata.yaml @@ -0,0 +1,16 @@ +# 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. + +resources: + - deployment: + - deployment.yaml + - service: + - service.yaml diff --git a/src/k8splugin/mock_files/mock_yamls/service.yaml b/src/k8splugin/mock_files/mock_yamls/service.yaml new file mode 100644 index 00000000..297ab1b7 --- /dev/null +++ b/src/k8splugin/mock_files/mock_yamls/service.yaml @@ -0,0 +1,21 @@ +# 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. + +apiVersion: v1 +kind: Service +metadata: + name: sise-svc +spec: + ports: + - port: 80 + protocol: TCP + selector: + app: sise
\ No newline at end of file diff --git a/src/k8splugin/plugins/deployment/plugin.go b/src/k8splugin/plugins/deployment/plugin.go new file mode 100644 index 00000000..2b4c7cb7 --- /dev/null +++ b/src/k8splugin/plugins/deployment/plugin.go @@ -0,0 +1,136 @@ +/* +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 ( + "io/ioutil" + "log" + "os" + + "k8s.io/client-go/kubernetes" + + pkgerrors "github.com/pkg/errors" + + appsV1 "k8s.io/api/apps/v1" + metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/scheme" + + "k8splugin/krd" +) + +// CreateResource object in a specific Kubernetes Deployment +func CreateResource(kubedata *krd.GenericKubeResourceData, kubeclient *kubernetes.Clientset) (string, error) { + if kubedata.Namespace == "" { + kubedata.Namespace = "default" + } + + if _, err := os.Stat(kubedata.YamlFilePath); err != nil { + return "", pkgerrors.New("File " + kubedata.YamlFilePath + " not found") + } + + log.Println("Reading deployment YAML") + rawBytes, err := ioutil.ReadFile(kubedata.YamlFilePath) + if err != nil { + return "", pkgerrors.Wrap(err, "Deployment YAML file read error") + } + + log.Println("Decoding deployment YAML") + decode := scheme.Codecs.UniversalDeserializer().Decode + obj, _, err := decode(rawBytes, nil, nil) + if err != nil { + return "", pkgerrors.Wrap(err, "Deserialize deployment error") + } + + switch o := obj.(type) { + case *appsV1.Deployment: + kubedata.DeploymentData = o + default: + return "", pkgerrors.New(kubedata.YamlFilePath + " contains another resource different than Deployment") + } + + kubedata.DeploymentData.Namespace = kubedata.Namespace + kubedata.DeploymentData.Name = kubedata.InternalVNFID + "-" + kubedata.DeploymentData.Name + + result, err := kubeclient.AppsV1().Deployments(kubedata.Namespace).Create(kubedata.DeploymentData) + if err != nil { + return "", pkgerrors.Wrap(err, "Create Deployment error") + } + + return result.GetObjectMeta().GetName(), nil +} + +// ListResources of existing deployments hosted in a specific Kubernetes Deployment +func ListResources(limit int64, namespace string, kubeclient *kubernetes.Clientset) (*[]string, error) { + if namespace == "" { + namespace = "default" + } + + opts := metaV1.ListOptions{ + Limit: limit, + } + opts.APIVersion = "apps/v1" + opts.Kind = "Deployment" + + list, err := kubeclient.AppsV1().Deployments(namespace).List(opts) + if err != nil { + return nil, pkgerrors.Wrap(err, "Get Deployment list error") + } + + result := make([]string, 0, limit) + if list != nil { + for _, deployment := range list.Items { + result = append(result, deployment.Name) + } + } + + return &result, nil +} + +// DeleteResource existing deployments hosting in a specific Kubernetes Deployment +func DeleteResource(name string, namespace string, kubeclient *kubernetes.Clientset) error { + if namespace == "" { + namespace = "default" + } + + log.Println("Deleting deployment: " + name) + + deletePolicy := metaV1.DeletePropagationForeground + err := kubeclient.AppsV1().Deployments(namespace).Delete(name, &metaV1.DeleteOptions{ + PropagationPolicy: &deletePolicy, + }) + + if err != nil { + return pkgerrors.Wrap(err, "Delete Deployment error") + } + + return nil +} + +// GetResource existing deployment hosting in a specific Kubernetes Deployment +func GetResource(name string, namespace string, kubeclient *kubernetes.Clientset) (string, error) { + if namespace == "" { + namespace = "default" + } + + opts := metaV1.GetOptions{} + opts.APIVersion = "apps/v1" + opts.Kind = "Deployment" + + deployment, err := kubeclient.AppsV1().Deployments(namespace).Get(name, opts) + if err != nil { + return "", pkgerrors.Wrap(err, "Get Deployment error") + } + + return deployment.Name, nil +} diff --git a/src/k8splugin/plugins/namespace/plugin.go b/src/k8splugin/plugins/namespace/plugin.go new file mode 100644 index 00000000..986de863 --- /dev/null +++ b/src/k8splugin/plugins/namespace/plugin.go @@ -0,0 +1,68 @@ +/* +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 ( + pkgerrors "github.com/pkg/errors" + + coreV1 "k8s.io/api/core/v1" + metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +// CreateResource is used to create a new Namespace +func CreateResource(namespace string, client *kubernetes.Clientset) error { + namespaceStruct := &coreV1.Namespace{ + ObjectMeta: metaV1.ObjectMeta{ + Name: namespace, + }, + } + _, err := client.CoreV1().Namespaces().Create(namespaceStruct) + if err != nil { + return pkgerrors.Wrap(err, "Create Namespace error") + } + return nil +} + +// GetResource is used to check if a given namespace actually exists in Kubernetes +func GetResource(namespace string, client *kubernetes.Clientset) (bool, error) { + opts := metaV1.ListOptions{} + + namespaceList, err := client.CoreV1().Namespaces().List(opts) + if err != nil { + return false, pkgerrors.Wrap(err, "Get Namespace list error") + } + + for _, ns := range namespaceList.Items { + if namespace == ns.Name { + return true, nil + } + } + + return false, nil +} + +// DeleteResource is used to delete a namespace +func DeleteResource(namespace string, client *kubernetes.Clientset) error { + deletePolicy := metaV1.DeletePropagationForeground + + err := client.CoreV1().Namespaces().Delete(namespace, &metaV1.DeleteOptions{ + PropagationPolicy: &deletePolicy, + }) + + if err != nil { + return pkgerrors.Wrap(err, "Delete Namespace error") + } + return nil +} diff --git a/src/k8splugin/plugins/service/plugin.go b/src/k8splugin/plugins/service/plugin.go new file mode 100644 index 00000000..36ef24f6 --- /dev/null +++ b/src/k8splugin/plugins/service/plugin.go @@ -0,0 +1,131 @@ +/* +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 ( + "io/ioutil" + "log" + "os" + + "k8s.io/client-go/kubernetes" + + pkgerrors "github.com/pkg/errors" + + coreV1 "k8s.io/api/core/v1" + metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/scheme" + + "k8splugin/krd" +) + +// CreateResource object in a specific Kubernetes Deployment +func CreateResource(kubedata *krd.GenericKubeResourceData, kubeclient *kubernetes.Clientset) (string, error) { + if kubedata.Namespace == "" { + kubedata.Namespace = "default" + } + + if _, err := os.Stat(kubedata.YamlFilePath); err != nil { + return "", pkgerrors.New("File " + kubedata.YamlFilePath + " not found") + } + + log.Println("Reading service YAML") + rawBytes, err := ioutil.ReadFile(kubedata.YamlFilePath) + if err != nil { + return "", pkgerrors.Wrap(err, "Service YAML file read error") + } + + log.Println("Decoding service YAML") + decode := scheme.Codecs.UniversalDeserializer().Decode + obj, _, err := decode(rawBytes, nil, nil) + if err != nil { + return "", pkgerrors.Wrap(err, "Deserialize service error") + } + + switch o := obj.(type) { + case *coreV1.Service: + kubedata.ServiceData = o + default: + return "", pkgerrors.New(kubedata.YamlFilePath + " contains another resource different than Service") + } + + kubedata.ServiceData.Namespace = kubedata.Namespace + kubedata.ServiceData.Name = kubedata.InternalVNFID + "-" + kubedata.ServiceData.Name + + result, err := kubeclient.CoreV1().Services(kubedata.Namespace).Create(kubedata.ServiceData) + if err != nil { + return "", pkgerrors.Wrap(err, "Create Service error") + } + return result.GetObjectMeta().GetName(), nil +} + +// ListResources of existing deployments hosted in a specific Kubernetes Deployment +func ListResources(limit int64, namespace string, kubeclient *kubernetes.Clientset) (*[]string, error) { + if namespace == "" { + namespace = "default" + } + opts := metaV1.ListOptions{ + Limit: limit, + } + opts.APIVersion = "apps/v1" + opts.Kind = "Service" + + list, err := kubeclient.CoreV1().Services(namespace).List(opts) + if err != nil { + return nil, pkgerrors.Wrap(err, "Get Service list error") + } + result := make([]string, 0, limit) + if list != nil { + for _, service := range list.Items { + result = append(result, service.Name) + } + } + return &result, nil +} + +// DeleteResource deletes an existing Kubernetes service +func DeleteResource(name string, namespace string, kubeclient *kubernetes.Clientset) error { + if namespace == "" { + namespace = "default" + } + + log.Println("Deleting service: " + name) + + deletePolicy := metaV1.DeletePropagationForeground + err := kubeclient.CoreV1().Services(namespace).Delete(name, &metaV1.DeleteOptions{ + PropagationPolicy: &deletePolicy, + }) + if err != nil { + return pkgerrors.Wrap(err, "Delete Service error") + } + + return nil +} + +// GetResource existing service hosting in a specific Kubernetes Service +func GetResource(name string, namespace string, kubeclient *kubernetes.Clientset) (string, error) { + if namespace == "" { + namespace = "default" + } + + opts := metaV1.GetOptions{} + opts.APIVersion = "apps/v1" + opts.Kind = "Service" + + service, err := kubeclient.CoreV1().Services(namespace).Get(name, opts) + if err != nil { + return "", pkgerrors.Wrap(err, "Get Deployment error") + } + + return service.Name, nil +} |