diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index f13d25e5..db9d763a 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -70,9 +70,9 @@ jobs: cd $KUBEPLUS_HOME/platform-operator ./build-artifact.sh latest - #echo "Building kubeconfiggenerator..." - #cd $KUBEPLUS_HOME/deploy - #./build-artifact-kubeconfiggenerator.sh latest + echo "Building kubeconfiggenerator..." + cd $KUBEPLUS_HOME/deploy + ./build-artifact-kubeconfiggenerator.sh latest #echo "Building webhook_init_container..." #./build-artifact.sh latest #echo "Building resource cleaner..." @@ -87,7 +87,7 @@ jobs: docker images echo "Installing KubePlus..." - helm install kubeplus ./deploy/kubeplus-chart --kubeconfig=kubeplus-saas-provider.json --set MUTATING_WEBHOOK=gcr.io/cloudark-kubeplus/pac-mutating-admission-webhook:latest --set PLATFORM_OPERATOR=gcr.io/cloudark-kubeplus/platform-operator:latest --set HELMER=gcr.io/cloudark-kubeplus/helm-pod:latest -n $KUBEPLUS_NS + helm install kubeplus ./deploy/kubeplus-chart --kubeconfig=kubeplus-saas-provider.json --set MUTATING_WEBHOOK=gcr.io/cloudark-kubeplus/pac-mutating-admission-webhook:latest --set PLATFORM_OPERATOR=gcr.io/cloudark-kubeplus/platform-operator:latest --set HELMER=gcr.io/cloudark-kubeplus/helm-pod:latest --set CRD_REGISTRATION_HELPER=gcr.io/cloudark-kubeplus/kubeconfiggenerator:latest -n $KUBEPLUS_NS kubectl get pods -A diff --git a/README.md b/README.md index 7824886a..a08c1e4a 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ The KubePlus Operator does not need any admin-level permissions on a cluster for ### Resource utilization -KubePlus provides controls to set per-namespace resource quotas. It also monitors usage of CPU, memory, storage, and network traffic at the application instance level. The collected metrics are available in different formats and can be pulled into Prometheus for historical usage tracking. +KubePlus provides controls to set per-namespace resource quotas. It also monitors usage of CPU, memory, storage, and network traffic at the application instance level. The collected metrics are available in different formats and can be pulled into Prometheus for historical usage tracking. KubePlus also supports ability to define licenses for the CRDs. A license defines the number of application instances that can be created for that CRD, and an expiry date. KubePlus prevents creation of application instances if the license terms are not met. ### Upgrades @@ -37,7 +37,8 @@ A new version of an application can be deployed by updating the application Helm ### Customization -The spec properties of the Kubernetes CRD wrapping the application Helm chart are the fields defined in the chart’s values.yaml file. Application deployments can be customized by specifying different values for these spec properties. +The spec properties of the Kubernetes CRD wrapping the application Helm chart are the fields defined in the chart’s values.yaml file. Application deployments can be customized by specifying different values for these spec properties. + ## Demo diff --git a/deploy/kubeconfiggenerator.py b/deploy/kubeconfiggenerator.py index bc3915a5..aee3140d 100644 --- a/deploy/kubeconfiggenerator.py +++ b/deploy/kubeconfiggenerator.py @@ -5,6 +5,7 @@ import os import yaml import time +from datetime import date from logging.config import dictConfig @@ -1181,6 +1182,66 @@ def create_network_policy(): return err_string +@app.route("/checklicense") +def check_license(): + app.logger.info("Inside checklicense..") + namespace = request.args.get("namespace").strip() + kind = request.args.get("kind").strip() + app.logger.info("Namespace:" + namespace + " kind:" + kind) + license_cfgmap = kind.lower() + "-license" + app.logger.info("License configmap:" + license_cfgmap) + cmd = "kubectl get configmap " + license_cfgmap + " -n " + namespace + " -o json " + out, err = run_command(cmd) + if "NotFound" in err: + app.logger.info(str(err)) + app.logger.info("License not defined. No checks are needed.") + return "" + + json_op = json.loads(out) + allowed_instances = "" + expiry = "" + if "allowed_instances" in json_op["metadata"]["annotations"]: + allowed_instances = json_op["metadata"]["annotations"]["allowed_instances"] + app.logger.info("Allowed instances:" + allowed_instances) + if "expiry" in json_op["metadata"]["annotations"]: + expiry = json_op["metadata"]["annotations"]["expiry"] + app.logger.info("Expiry:" + expiry) + + msg = "" + + if allowed_instances != "": + cmd1 = "kubectl get " + kind + out1, err1 = run_command(cmd1) + created_instances = 0 + for line in out1.split("\n"): + line = line.strip() + app.logger.info("Line:" + line + "\n") + if "NAME" not in line and "AGE" not in line and line != "": + created_instances = created_instances + 1 + app.logger.info("Already created instances:" + str(created_instances)) + + if created_instances >= int(allowed_instances): + msg = msg + "Allowed number of instances reached." + + if expiry != "": + parts = expiry.split("/") + if len(parts) == 3 : + month = int(parts[0].strip()) + day = int(parts[1].strip()) + year = int(parts[2].strip()) + + expiry_date = date(year, month, day) + today = date.today() + app.logger.info("Expiry date:" + str(expiry_date)) + app.logger.info("Today:" + str(today)) + if today > expiry_date: + msg = msg + " License expired (expiry date):" + str(expiry) + "." + + app.logger.info("License check message:" + msg) + + return msg + + @app.route("/resource_quota") def create_resource_quota(): app.logger.info("Inside create_resource_quota..") diff --git a/examples/multitenancy/hello-world/hs2.yaml b/examples/multitenancy/hello-world/hs2.yaml index b37167b5..a1dcc88e 100644 --- a/examples/multitenancy/hello-world/hs2.yaml +++ b/examples/multitenancy/hello-world/hs2.yaml @@ -3,5 +3,6 @@ kind: HelloWorldService metadata: name: hs2 spec: - greeting: Hello1 hello hello + greeting: Hello hello hello - hs2 + replicas: 1 diff --git a/mutating-webhook/utils.go b/mutating-webhook/utils.go index 47d44041..c59cf4c3 100644 --- a/mutating-webhook/utils.go +++ b/mutating-webhook/utils.go @@ -814,10 +814,10 @@ func GetPlural(kind, group string) []byte { } func CheckResource(kind, plural string) []byte { - fmt.Printf("Inside CheckResource...\n") + //fmt.Printf("Inside CheckResource...\n") args := fmt.Sprintf("kind=%s&plural=%s", kind, plural) url1 := fmt.Sprintf("http://%s:%s/apis/kubeplus/checkResource?%s", serviceHost, servicePort, args) - fmt.Printf("Url:%s\n", url1) + //fmt.Printf("Url:%s\n", url1) body := queryKubeDiscoveryService(url1) return body } @@ -862,14 +862,25 @@ func QueryDeployEndpoint(platformworkflow, customresource, namespace, overrides, CreateOverrides(platformworkflow, customresource) args := fmt.Sprintf("platformworkflow=%s&customresource=%s&namespace=%s&overrides=%s&cpu_req=%s&cpu_lim=%s&mem_req=%s&mem_lim=%s&labels=%s", platformworkflow, customresource, namespace, encodedOverrides, cpu_req, cpu_lim, mem_req, mem_lim,labels) - fmt.Printf("Inside QueryDeployEndpoint...\n") + //fmt.Printf("Inside QueryDeployEndpoint...\n") var url1 string url1 = fmt.Sprintf("http://%s:%s/apis/kubeplus/deploy?%s", serviceHost, servicePort, args) - fmt.Printf("Url:%s\n", url1) + //fmt.Printf("Url:%s\n", url1) body := queryKubeDiscoveryService(url1) return body } +func CheckLicense(kind, webhook_namespace string) string { + //fmt.Printf("Inside CheckLicense...\n") + args := fmt.Sprintf("kind=%s&namespace=%s", kind, webhook_namespace) + var url1 string + url1 = fmt.Sprintf("http://%s:%s/checklicense?%s", serviceHost, verificationServicePort, args) + //fmt.Printf("Url:%s\n", url1) + body := queryKubeDiscoveryService(url1) + bodyString := string(body) + return bodyString +} + func DryRunChart(platformworkflow, namespace string) []byte { args := fmt.Sprintf("platformworkflow=%s&namespace=%s&dryrun=true", platformworkflow, namespace) fmt.Printf("Inside DryRunChart...\n") @@ -892,25 +903,25 @@ func TestChartDeployment(kind, namespace, chartName, chartURL string) []byte { } func CreateOverrides(platformworkflow, customresource string) []byte { - fmt.Printf("Inside CreateOverrides...\n") + //fmt.Printf("Inside CreateOverrides...\n") args := fmt.Sprintf("platformworkflow=%s&customresource=%s", platformworkflow, customresource) var url1 string url1 = fmt.Sprintf("http://%s:%s/overrides?%s", serviceHost, verificationServicePort, args) - fmt.Printf("Url:%s\n", url1) + //fmt.Printf("Url:%s\n", url1) body := queryKubeDiscoveryService(url1) return body } func CheckClusterCapacity(cpuRequests, cpuLimits, memRequests, memLimits string) (bool, string) { - fmt.Printf("Inside CheckClusterCapacity...\n") + //fmt.Printf("Inside CheckClusterCapacity...\n") var url1 string url1 = fmt.Sprintf("http://%s:%s/cluster_capacity", serviceHost, verificationServicePort) - fmt.Printf("Url:%s\n", url1) + //fmt.Printf("Url:%s\n", url1) body := queryKubeDiscoveryService(url1) var clusterCapacity ClusterCapacity json.Unmarshal(body, &clusterCapacity) - fmt.Printf("Cluster Capacity:%v\n", clusterCapacity) + //fmt.Printf("Cluster Capacity:%v\n", clusterCapacity) bodyString := string(body) //jsonresp := bodyString.(map[string]string) @@ -926,9 +937,9 @@ func CheckClusterCapacity(cpuRequests, cpuLimits, memRequests, memLimits string) memParts := strings.Split(parts[1],":") totalAllocatableMemoryGB, _ := strconv.ParseFloat(strings.TrimSpace(memParts[1]), 64) - fmt.Printf("Total Allocatable CPU:%d\n", totalAllocatableCPU) + /*fmt.Printf("Total Allocatable CPU:%d\n", totalAllocatableCPU) fmt.Printf("Total Allocatable Memory:%f\n", totalAllocatableMemoryGB) - + */ cpuRequestsI, _ := strconv.Atoi(strings.TrimSpace(strings.ReplaceAll(cpuRequests, "m", ""))) cpuLimitsI, _ := strconv.Atoi(strings.TrimSpace(strings.ReplaceAll(cpuLimits, "m", ""))) @@ -962,23 +973,23 @@ func GetExistingResourceCompositions() []byte { } func CheckChartExists(chartURL string) []byte { - fmt.Printf("Inside CheckChartExists...\n") + //fmt.Printf("Inside CheckChartExists...\n") encodedChartURL := url.QueryEscape(chartURL) args := fmt.Sprintf("chartURL=%s", encodedChartURL) var url1 string url1 = fmt.Sprintf("http://%s:%s/checkchartexists?%s", serviceHost, verificationServicePort, args) - fmt.Printf("Url:%s\n", url1) + //fmt.Printf("Url:%s\n", url1) body := queryKubeDiscoveryService(url1) return body } func LintChart(chartURL string) []byte { - fmt.Printf("Inside LintChart...\n") + //fmt.Printf("Inside LintChart...\n") encodedChartURL := url.QueryEscape(chartURL) args := fmt.Sprintf("chartURL=%s", encodedChartURL) var url1 string url1 = fmt.Sprintf("http://%s:%s/dryrunchart?%s", serviceHost, verificationServicePort, args) - fmt.Printf("Url:%s\n", url1) + //fmt.Printf("Url:%s\n", url1) body := queryKubeDiscoveryService(url1) return body } @@ -1015,10 +1026,10 @@ func QueryCompositionEndpoint(kind, namespace, crdKindName string) []byte { func GetValuesYaml(platformworkflow, namespace string) []byte { args := fmt.Sprintf("platformworkflow=%s&namespace=%s", platformworkflow, namespace) - fmt.Printf("Inside GetValuesYaml...\n") + //fmt.Printf("Inside GetValuesYaml...\n") var url1 string url1 = fmt.Sprintf("http://%s:%s/apis/kubeplus/getchartvalues?%s", serviceHost, servicePort, args) - fmt.Printf("Url:%s\n", url1) + //fmt.Printf("Url:%s\n", url1) body := queryKubeDiscoveryService(url1) return body } @@ -1052,7 +1063,7 @@ func getServiceEndpoint(servicename string) (string, string) { // Rename this function to a more generic name since we use it to trigger Custom Resource deployment as well. func queryKubeDiscoveryService(url1 string) []byte { - fmt.Printf("..inside queryKubeDiscoveryService") + //fmt.Printf("..inside queryKubeDiscoveryService") u, err := url.Parse(url1) //fmt.Printf("URL:%s\n",u) if err != nil { @@ -1074,9 +1085,9 @@ func queryKubeDiscoveryService(url1 string) []byte { defer resp.Body.Close() resp_body, _ := ioutil.ReadAll(resp.Body) - fmt.Println("Response status:%s\n",resp.Status) - fmt.Println("Response body:%s\n",string(resp_body)) - fmt.Println("Exiting queryKubeDiscoveryService") + //fmt.Println("Response status:%s\n",resp.Status) + //fmt.Println("Response body:%s\n",string(resp_body)) + //fmt.Println("Exiting queryKubeDiscoveryService") return resp_body } diff --git a/mutating-webhook/webhook.go b/mutating-webhook/webhook.go index 9aacb173..55d656df 100644 --- a/mutating-webhook/webhook.go +++ b/mutating-webhook/webhook.go @@ -184,17 +184,17 @@ func setup() { func (whsvr *WebhookServer) mutate(ar *v1.AdmissionReview, httpMethod string) *v1.AdmissionResponse { req := ar.Request - fmt.Println("=== Request ===") - fmt.Println(req.Kind.Kind) - fmt.Println(req.Name) - fmt.Println(req.Namespace) - fmt.Println(httpMethod) + //fmt.Println("=== Request ===") + //fmt.Println(req.Kind.Kind) + //fmt.Println(req.Name) + //fmt.Println(req.Namespace) + //fmt.Println(httpMethod) //fmt.Printf("%s\n", string(req.Object.Raw)) - fmt.Println("=== Request ===") + //fmt.Println("=== Request ===") - fmt.Println("=== User ===") - fmt.Println(req.UserInfo.Username) - fmt.Println("=== User ===") + //fmt.Println("=== User ===") + //fmt.Println(req.UserInfo.Username) + //fmt.Println("=== User ===") user := req.UserInfo.Username @@ -266,7 +266,7 @@ func (whsvr *WebhookServer) mutate(ar *v1.AdmissionReview, httpMethod string) *v patchOperations = append(patchOperations, annotationPatch) - errResponse := handleCustomAPIs(ar) + errResponse := handleCustomAPIs(ar, httpMethod) if errResponse != nil { return errResponse } @@ -282,10 +282,10 @@ func (whsvr *WebhookServer) mutate(ar *v1.AdmissionReview, httpMethod string) *v allLabels, _, _, err := jsonparser.Get(req.Object.Raw, "metadata", "labels") if err == nil { json.Unmarshal(allLabels, &labels) - fmt.Printf("Pod all labels:%v\n", labels) + //fmt.Printf("Pod all labels:%v\n", labels) } labels["partof"] = strings.ToLower(rootKind + "-" + rootName) - fmt.Printf("All labels:%v\n", labels) + //fmt.Printf("All labels:%v\n", labels) podLabelPatch := patchOperation{ Op: "add", @@ -331,7 +331,7 @@ func (whsvr *WebhookServer) mutate(ar *v1.AdmissionReview, httpMethod string) *v } } - fmt.Printf("PatchOperations:%v\n", patchOperations) + //fmt.Printf("PatchOperations:%v\n", patchOperations) patchBytes, _ := json.Marshal(patchOperations) //fmt.Printf("---------------------------------\n") // marshal the struct into bytes to pass into AdmissionResponse @@ -346,14 +346,14 @@ func (whsvr *WebhookServer) mutate(ar *v1.AdmissionReview, httpMethod string) *v } func getStatusMessage(ar *v1.AdmissionReview) string { - fmt.Println("Inside getStatusMessage") + //fmt.Println("Inside getStatusMessage") req := ar.Request body := req.Object.Raw status, err := jsonparser.GetUnsafeString(body, "status","status") if err != nil { fmt.Errorf("Error:%s\n", err) } - fmt.Printf("getStatusMessage:%s\n", status) + //fmt.Printf("getStatusMessage:%s\n", status) return status } @@ -586,7 +586,7 @@ func getReleaseName(ar *v1.AdmissionReview) string { } func saveResource(ar *v1.AdmissionReview) { - fmt.Printf("Inside saveResource\n") + //fmt.Printf("Inside saveResource\n") kind, resName, _ := getObjectDetails(ar) //key := kind + "/" + namespace + "/" + resName key := kind + "-" + resName @@ -594,18 +594,20 @@ func saveResource(ar *v1.AdmissionReview) { _, ok := resourceNameObjMap[key] if !ok { resourceNameObjMap[key] = ar - } else { + } + /*else { fmt.Printf("Key %s already present in resourceNameObjMap\n", key) //fmt.Printf("%v\n", val) - } + }*/ } func saveResourcePolicy(ar *v1.AdmissionReview) { req := ar.Request body := req.Object.Raw - resPolicyName, _ := jsonparser.GetUnsafeString(body, "metadata", "name") + /*resPolicyName, _ := jsonparser.GetUnsafeString(body, "metadata", "name") fmt.Printf("Resource Policy Name:%s\n", resPolicyName) + */ var resourcePolicy platformworkflowv1alpha1.ResourcePolicy err = json.Unmarshal(body, &resourcePolicy) @@ -617,52 +619,52 @@ func saveResourcePolicy(ar *v1.AdmissionReview) { lowercaseKind := strings.ToLower(kind) group := resourcePolicy.Spec.Resource.Group version := resourcePolicy.Spec.Resource.Version - fmt.Printf("Kind:%s, Group:%s, Version:%s\n", kind, group, version) + //fmt.Printf("Kind:%s, Group:%s, Version:%s\n", kind, group, version) podPolicy := resourcePolicy.Spec.Policy - fmt.Printf("Pod Policy:%v\n", podPolicy) + //fmt.Printf("Pod Policy:%v\n", podPolicy) customAPI := group + "/" + version + "/" + lowercaseKind resourcePolicyMap[customAPI] = podPolicy - fmt.Printf("Resource Policy Map:%v\n", resourcePolicyMap) + //fmt.Printf("Resource Policy Map:%v\n", resourcePolicyMap) } func checkServiceLevelPolicyApplicability(ar *v1.AdmissionReview) (string, string, string, string) { - fmt.Printf("Inside checkServiceLevelPolicyApplicability") + //fmt.Printf("Inside checkServiceLevelPolicyApplicability") req := ar.Request body := req.Object.Raw //fmt.Printf("Body:%v\n", body) namespace := req.Namespace - fmt.Println("Namespace:%s\n",namespace) + //fmt.Println("Namespace:%s\n",namespace) // TODO: looks like we can just keep one - namespace or namespace1 - namespace1, _, _, err := jsonparser.Get(body, "metadata", "namespace") - if err == nil { + namespace1, _, _, _ := jsonparser.Get(body, "metadata", "namespace") + /*if err == nil { fmt.Printf("Namespace1:%s\n", namespace1) - } + }*/ ownerKind, _, _, err1 := jsonparser.Get(body, "metadata", "ownerReferences", "[0]", "kind") if err1 != nil { fmt.Printf("Error:%v\n", err1) - } else { + } /*else { fmt.Printf("ownerKind:%v\n", string(ownerKind)) - } + }*/ ownerName, _, _, err1 := jsonparser.Get(body, "metadata", "ownerReferences", "[0]", "name") if err1 != nil { fmt.Printf("Error:%v\n", err1) - } else { + } /*else { fmt.Printf("ownerName:%v\n", string(ownerName)) - } + }*/ ownerAPIVersion, _, _, err1 := jsonparser.Get(body, "metadata", "ownerReferences", "[0]", "apiVersion") if err1 != nil { fmt.Printf("Error:%v\n", err1) - } else { + } /*else { fmt.Printf("ownerAPIVersion:%v\n", string(ownerAPIVersion)) - } + }*/ ownerKindS := string(ownerKind) ownerNameS := string(ownerName) @@ -689,17 +691,17 @@ func checkServiceLevelPolicyApplicability(ar *v1.AdmissionReview) (string, strin rootKind, rootName, rootAPIVersion = findRoot(namespace, ownerKindS, ownerNameS, ownerAPIVersionS) } - fmt.Printf("Root Kind:%s\n", rootKind) + /*fmt.Printf("Root Kind:%s\n", rootKind) fmt.Printf("Root Name:%s\n", rootName) - fmt.Printf("Root API Version:%s\n", rootAPIVersion) + fmt.Printf("Root API Version:%s\n", rootAPIVersion)*/ lowercaseKind := strings.ToLower(rootKind) // Check if the rootKind, rootName, rootAPIVersion is registered to be applied policies on. customAPI := rootAPIVersion + "/" + lowercaseKind - fmt.Printf("Custom API:%s\n", customAPI) - if podPolicy, ok := resourcePolicyMap[customAPI]; ok { - fmt.Printf("Resource Policy:%v\n", podPolicy) + //fmt.Printf("Custom API:%s\n", customAPI) + if _, ok := resourcePolicyMap[customAPI]; ok { + //fmt.Printf("Resource Policy:%v\n", podPolicy) return customAPI, rootKind, rootName, string(namespace1) } return "", "", "", "" @@ -725,23 +727,23 @@ func findRoot(namespace, kind, name, apiVersion string) (string, string, string) time.Sleep(10) - group, version := getGroupVersion(apiVersion) + /*group, version := getGroupVersion(apiVersion) fmt.Printf("Group:%s\n", group) fmt.Printf("Version:%s\n", version) fmt.Printf("ResName:%s\n", name) fmt.Printf("Namespace:%s\n", namespace) - + */ ownerResKindPlural, _, ownerResApiVersion, ownerResGroup := getKindAPIDetails(kind) - fmt.Printf("ownerResKindPlural:%s\n", ownerResKindPlural) + /*fmt.Printf("ownerResKindPlural:%s\n", ownerResKindPlural) fmt.Printf("ownerResApiVersion:%s\n", ownerResApiVersion) fmt.Printf("ownerResGroup:%s\n", ownerResGroup) - + */ ownerRes := schema.GroupVersionResource{Group: ownerResGroup, Version: ownerResApiVersion, Resource: ownerResKindPlural} - fmt.Printf("OwnerRes:%v\n", ownerRes) + //fmt.Printf("OwnerRes:%v\n", ownerRes) dynamicClient, err1 := getDynamicClient1() if err1 != nil { fmt.Printf("Error 1:%v\n", err1) @@ -762,9 +764,9 @@ func findRoot(namespace, kind, name, apiVersion string) (string, string, string) // Reached the root // Jump of from the Helm annotation; should be of type - - fmt.Printf("Intermediate Root kind:%s\n", kind) + /*fmt.Printf("Intermediate Root kind:%s\n", kind) fmt.Printf("Intermediate Root name:%s\n", name) - fmt.Printf("Intermediate Root APIVersion:%s\n", apiVersion) + fmt.Printf("Intermediate Root APIVersion:%s\n", apiVersion)*/ annotations := instanceObj.GetAnnotations() releaseName := annotations["meta.helm.sh/release-name"] @@ -795,9 +797,9 @@ func getAPIDetailsFromHelmReleaseAnnotation(releaseName string) (string, string, } else if len(parts) == 2 { oinstance = parts[1] } - fmt.Printf("KindPluralMap2:%v\n", kindPluralMap) + //fmt.Printf("KindPluralMap2:%v\n", kindPluralMap) oplural := kindPluralMap[okindLowerCase] - fmt.Printf("OPlural:%s OInstance:%s\n", oplural, oinstance) + //fmt.Printf("OPlural:%s OInstance:%s\n", oplural, oinstance) customAPI := "" for k, v := range customKindPluralMap { if v == oplural { @@ -805,13 +807,13 @@ func getAPIDetailsFromHelmReleaseAnnotation(releaseName string) (string, string, break } } - fmt.Printf("CustomAPI:%s\n", customAPI) + //fmt.Printf("CustomAPI:%s\n", customAPI) if customAPI != "" { capiParts := strings.Split(customAPI, "/") capiGroup = capiParts[0] capiVersion = capiParts[1] capiKind = capiParts[2] - fmt.Printf("capiGroup:%s capiVersion:%s capiKind:%s\n", capiGroup, capiVersion, capiKind) + //fmt.Printf("capiGroup:%s capiVersion:%s capiKind:%s\n", capiGroup, capiVersion, capiKind) } } else { return "","","","" @@ -823,9 +825,9 @@ func applyPolicies(ar *v1.AdmissionReview, customAPI, rootKind, rootName, rootNa req := ar.Request body := req.Object.Raw + /* podName, _ := jsonparser.GetUnsafeString(req.Object.Raw, "metadata", "name") - - fmt.Printf("Pod Name:%s\n", podName) + fmt.Printf("Pod Name:%s\n", podName)*/ /* res1, _, _, _ := jsonparser.Get(body, "spec", "containers") @@ -837,22 +839,22 @@ func applyPolicies(ar *v1.AdmissionReview, customAPI, rootKind, rootName, rootNa }*/ // TODO: Defaulting to the first container. Take input for additional containers - res, dataType, _, err1 := jsonparser.Get(body, "spec", "containers", "[0]", "resources") + _, _, _, err1 := jsonparser.Get(body, "spec", "containers", "[0]", "resources") if err1 != nil { fmt.Printf("Error:%v\n", err1) - } else { - fmt.Printf("Resources:%v\n", string(res)) - } + } /*else { + //fmt.Printf("Resources:%v\n", string(res)) + }*/ var operation string - fmt.Printf("DataType:%s\n", dataType) + //fmt.Printf("DataType:%s\n", dataType) operation = "add" podPolicy := resourcePolicyMap[customAPI] - fmt.Printf("PodPolicy:%v\n", podPolicy) + //fmt.Printf("PodPolicy:%v\n", podPolicy) - xType := fmt.Sprintf("%T", podPolicy) - fmt.Printf("Pod Policy type:%s\n", xType) // "[]int" + /*xType := fmt.Sprintf("%T", podPolicy) + fmt.Printf("Pod Policy type:%s\n", xType) // "[]int"*/ patchOperations := make([]patchOperation, 0) @@ -862,17 +864,17 @@ func applyPolicies(ar *v1.AdmissionReview, customAPI, rootKind, rootName, rootNa cpuRequest := podPolicy1.PolicyResources.Requests.CPU memRequest := podPolicy1.PolicyResources.Requests.Memory if cpuRequest != "" && memRequest != "" { - fmt.Printf("CPU Request:%s\n", cpuRequest) + //fmt.Printf("CPU Request:%s\n", cpuRequest) if strings.Contains(cpuRequest, "values") { cpuRequest = getFieldValueFromInstance(cpuRequest,rootKind, rootName) } - fmt.Printf("CPU Request1:%s\n", cpuRequest) + //fmt.Printf("CPU Request1:%s\n", cpuRequest) - fmt.Printf("Mem Request:%s\n", memRequest) + //fmt.Printf("Mem Request:%s\n", memRequest) if strings.Contains(memRequest, "values") { memRequest = getFieldValueFromInstance(memRequest,rootKind, rootName) } - fmt.Printf("Mem Request1:%s\n", memRequest) + //fmt.Printf("Mem Request1:%s\n", memRequest) podResRequest := make(map[string]string,0) podResRequest["cpu"] = cpuRequest @@ -890,8 +892,8 @@ func applyPolicies(ar *v1.AdmissionReview, customAPI, rootKind, rootName, rootNa cpuLimit := podPolicy1.PolicyResources.Limits.CPU memLimit := podPolicy1.PolicyResources.Limits.Memory if cpuLimit != "" && memLimit != "" { - fmt.Printf("CPU Limit:%s\n", cpuLimit) - fmt.Printf("Mem Limit:%s\n", memLimit) + //fmt.Printf("CPU Limit:%s\n", cpuLimit) + //fmt.Printf("Mem Limit:%s\n", memLimit) podResLimits := make(map[string]string,0) podResLimits["cpu"] = cpuLimit @@ -908,7 +910,7 @@ func applyPolicies(ar *v1.AdmissionReview, customAPI, rootKind, rootName, rootNa // 3. Node Selector nodeSelector := podPolicy1.PolicyResources.NodeSelector if nodeSelector != "" { - fmt.Printf("Node Selector:%s\n", nodeSelector) + //fmt.Printf("Node Selector:%s\n", nodeSelector) fieldValueS := getFieldValueFromInstance(nodeSelector, rootKind, rootName) if fieldValueS != "" { podNodeSelector := make(map[string]string,0) @@ -930,24 +932,24 @@ func getFieldValueFromInstance(fieldName, rootKind, rootName string) string { parts := strings.Split(fieldName, ".") field := parts[1] field = strings.TrimSpace(field) - fmt.Printf("Field:%s\n", field) + //fmt.Printf("Field:%s\n", field) //kind, resName, namespace := getObjectDetails(ar) lowercaseRootKind := strings.ToLower(rootKind) //rootkey := lowercaseRootKind + "/" + rootNamespace + "/" + rootName rootkey := lowercaseRootKind + "-" + rootName - fmt.Printf("Root Key:%s\n", rootkey) + //fmt.Printf("Root Key:%s\n", rootkey) arSaved := resourceNameObjMap[rootkey].(*v1.AdmissionReview) reqObject := arSaved.Request reqspec := reqObject.Object.Raw - fieldValue, _, _, err2 := jsonparser.Get(reqspec, "spec", field) + fieldValue, _, _, _ := jsonparser.Get(reqspec, "spec", field) fieldValueS := string(fieldValue) - if err2 != nil { + /*if err2 != nil { fmt.Printf("Error:%v\n", err2) } else { fmt.Printf("Fields:%v\n", string(fieldValue)) - } + }*/ return fieldValueS } @@ -1041,7 +1043,7 @@ func trackCustomAPIs(ar *v1.AdmissionReview) *v1.AdmissionResponse { //lint_chart := os.Getenv("LINT_CHART") //if strings.EqualFold(lint_chart, "yes") { message1 := string(LintChart(chartURL)) - fmt.Printf("After LintChart - message:%s\n", message1) + //fmt.Printf("After LintChart - message:%s\n", message1) if !strings.Contains(message1, "Chart is good") { return &v1.AdmissionResponse{ Result: &metav1.Status{ @@ -1053,7 +1055,7 @@ func trackCustomAPIs(ar *v1.AdmissionReview) *v1.AdmissionResponse { parts := strings.Split(message1, "\n") kindString := parts[1] - fmt.Printf("Kind string:%s\n", kindString) + //fmt.Printf("Kind string:%s\n", kindString) chartKindMap[platformWorkflowName] = kindString check_kyverno_policies := os.Getenv("CHECK_KYVERNO_POLICIES") @@ -1071,16 +1073,16 @@ func trackCustomAPIs(ar *v1.AdmissionReview) *v1.AdmissionResponse { } quota_cpu_requests, _ := jsonparser.GetUnsafeString(body, "spec","respolicy","spec","policy","quota","requests.cpu") - fmt.Printf("CPU requests(quota):%s\n", quota_cpu_requests) + //fmt.Printf("CPU requests(quota):%s\n", quota_cpu_requests) quota_memory_requests, _ := jsonparser.GetUnsafeString(body, "spec","respolicy","spec","policy","quota","requests.memory") - fmt.Printf("Memory requests(quota):%s\n", quota_memory_requests) + //fmt.Printf("Memory requests(quota):%s\n", quota_memory_requests) quota_cpu_limits, _ := jsonparser.GetUnsafeString(body, "spec","respolicy","spec","policy","quota","limits.cpu") - fmt.Printf("CPU limits(quota):%s\n", quota_cpu_limits) + //fmt.Printf("CPU limits(quota):%s\n", quota_cpu_limits) quota_memory_limits, _ := jsonparser.GetUnsafeString(body, "spec","respolicy","spec","policy","quota","limits.memory") - fmt.Printf("Memory limits(quota):%s\n", quota_memory_limits) + //fmt.Printf("Memory limits(quota):%s\n", quota_memory_limits) empty_quota_fields := 0 if (quota_cpu_requests == "") { @@ -1108,7 +1110,7 @@ func trackCustomAPIs(ar *v1.AdmissionReview) *v1.AdmissionResponse { } _, message2 := CheckClusterCapacity(quota_cpu_requests, quota_cpu_limits, quota_memory_requests, quota_memory_limits) - fmt.Printf("After CheckClusterCapacity - message:%s\n", message2) + //fmt.Printf("After CheckClusterCapacity - message:%s\n", message2) if !strings.Contains(message2, "Quota is within limits.") { return &v1.AdmissionResponse{ Result: &metav1.Status{ @@ -1126,7 +1128,6 @@ func trackCustomAPIs(ar *v1.AdmissionReview) *v1.AdmissionResponse { customAPIQuotaMap[customAPI] = quota_map - fmt.Printf("10101010101\n") return nil } @@ -1169,7 +1170,7 @@ func registerManPage(kind, apiVersion, platformworkflow, namespace string) strin valuesYaml := prefix - fmt.Printf("Values YAML:%s\n",valuesYaml) + //fmt.Printf("Values YAML:%s\n",valuesYaml) configMapName := lowercaseKind + "-usage" @@ -1206,7 +1207,7 @@ func registerManPage(kind, apiVersion, platformworkflow, namespace string) strin } usageAnnotationValue := configMapName + ".spec" - fmt.Printf("Usage Annotation:%s\n", usageAnnotationValue) + //fmt.Printf("Usage Annotation:%s\n", usageAnnotationValue) return usageAnnotationValue } @@ -1292,7 +1293,7 @@ func getPaCAnnotation(ar *v1.AdmissionReview) map[string]string { func checkCRDNameValidity(ar *v1.AdmissionReview) string { - fmt.Printf("Inside checkCRDNameValidity...\n") + //fmt.Printf("Inside checkCRDNameValidity...\n") req := ar.Request body := req.Object.Raw @@ -1314,32 +1315,31 @@ func checkCRDNameValidity(ar *v1.AdmissionReview) string { func checkChartExists(ar *v1.AdmissionReview) string { - fmt.Printf("Inside checkChartExists...\n") + //fmt.Printf("Inside checkChartExists...\n") req := ar.Request body := req.Object.Raw - kind, err := jsonparser.GetUnsafeString(body, "kind") + _, err := jsonparser.GetUnsafeString(body, "kind") if err != nil { fmt.Errorf("Error:%s\n", err) } - crname, err := jsonparser.GetUnsafeString(req.Object.Raw, "metadata", "name") - fmt.Printf("CR Name:%s\n", crname) - fmt.Printf("Kind:%s\n", kind) + //_, err = jsonparser.GetUnsafeString(req.Object.Raw, "metadata", "name") + //fmt.Printf("CR Name:%s\n", crname) + //fmt.Printf("Kind:%s\n", kind) chartURL, err := jsonparser.GetUnsafeString(req.Object.Raw, "spec", "newResource", "chartURL") - fmt.Printf("%%%%% CHART URL:%s %%%%%\n", chartURL) + //fmt.Printf("%%%%% CHART URL:%s %%%%%\n", chartURL) message1 := string(CheckChartExists(chartURL)) - fmt.Printf("After CheckChartExists - message:%s\n", message1) + //fmt.Printf("After CheckChartExists - message:%s\n", message1) return message1 } -func handleCustomAPIs(ar *v1.AdmissionReview) *v1.AdmissionResponse { - fmt.Printf("Inside handleCustomAPIs...\n") +func handleCustomAPIs(ar *v1.AdmissionReview, httpMethod string) *v1.AdmissionResponse { req := ar.Request body := req.Object.Raw //fmt.Printf("%v\n", req) @@ -1358,9 +1358,6 @@ func handleCustomAPIs(ar *v1.AdmissionReview) *v1.AdmissionResponse { } //fmt.Printf("Namespace:%s\n", namespace) crname, err := jsonparser.GetUnsafeString(req.Object.Raw, "metadata", "name") - fmt.Printf("CR Name:%s\n", crname) - fmt.Printf("Kind:%s\n", kind) - //cruid, err := jsonparser.GetUnsafeString(req.Object.Raw, "metadata", "uid") // We have to generate a uid as when the request is received there is no uid yet. @@ -1371,20 +1368,14 @@ func handleCustomAPIs(ar *v1.AdmissionReview) *v1.AdmissionResponse { labelsBytes, _, _, _ := jsonparser.Get(req.Object.Raw, "metadata", "labels") labels := string(labelsBytes) - fmt.Printf("******\n") - fmt.Printf("labels:%s\n", labels) - fmt.Printf("******\n") overridesBytes, _, _, _ := jsonparser.Get(req.Object.Raw, "spec") overrides := string(overridesBytes) - fmt.Printf("******\n") - fmt.Printf("Overrides:%s\n", overrides) - fmt.Printf("******\n") nodeName, err := jsonparser.GetUnsafeString(req.Object.Raw, "spec", "nodeName") - fmt.Printf("nodeName in Spec:%s\n", nodeName) if nodeName != "" { + fmt.Printf("nodeName in Spec:%s\n", nodeName) validNodeName := CheckApplicationNodeName(nodeName) if !validNodeName { msg := fmt.Sprintf("Invalid node name specified %s\n", nodeName) @@ -1406,8 +1397,28 @@ func handleCustomAPIs(ar *v1.AdmissionReview) *v1.AdmissionResponse { platformWorkflowName := customAPIPlatformWorkflowMap[customAPI] - fmt.Printf("ResourceComposition:%s\n", platformWorkflowName) if platformWorkflowName != "" { + fmt.Printf("=========\n") + fmt.Printf("Inside handleCustomAPIs...\n") + fmt.Printf("ResourceComposition:%s\n", platformWorkflowName) + fmt.Printf("CR Name:%s\n", crname) + fmt.Printf("Kind:%s\n", kind) + fmt.Printf("labels:%s\n", labels) + fmt.Printf("Overrides:%s\n", overrides) + fmt.Printf("HTTP method:%v\n", httpMethod) + + // Check license + if httpMethod == "CREATE" { + license_ok := CheckLicense(kind, webhook_namespace) + if license_ok != "" { + msg := fmt.Sprintf("%s Update license for %s and then re-try.\n", license_ok, kind) + return &v1.AdmissionResponse{ + Result: &metav1.Status{ + Message: msg, + }, + } + } + } // Check if Namespace corresponding to crname is not in Terminating state config, err := rest.InClusterConfig() @@ -1459,17 +1470,17 @@ func handleCustomAPIs(ar *v1.AdmissionReview) *v1.AdmissionResponse { fmt.Errorf("Error:%s\n", err) } - kind := platformWorkflow1.Spec.NewResource.Resource.Kind + chartURL := platformWorkflow1.Spec.NewResource.ChartURL + /*kind := platformWorkflow1.Spec.NewResource.Resource.Kind group := platformWorkflow1.Spec.NewResource.Resource.Group version := platformWorkflow1.Spec.NewResource.Resource.Version plural := platformWorkflow1.Spec.NewResource.Resource.Plural - chartURL := platformWorkflow1.Spec.NewResource.ChartURL chartName := platformWorkflow1.Spec.NewResource.ChartName - fmt.Printf("Kind:%s, Group:%s, Version:%s, Plural:%s, ChartURL:%s, ChartName:%s\n", kind, group, version, plural, chartURL, chartName) + //fmt.Printf("Kind:%s, Group:%s, Version:%s, Plural:%s, ChartURL:%s, ChartName:%s\n", kind, group, version, plural, chartURL, chartName)*/ // If chart is local, check if it exists - it will not if KubePlus Pod has restarted due to cluster restart message1 := string(CheckChartExists(chartURL)) - fmt.Printf("After CheckChartExists - message:%s\n", message1) + //fmt.Printf("After CheckChartExists - message:%s\n", message1) if message1 != "" { return &v1.AdmissionResponse{ Result: &metav1.Status{ @@ -1490,7 +1501,7 @@ func handleCustomAPIs(ar *v1.AdmissionReview) *v1.AdmissionResponse { mem_requests_q = quota_map["requests.memory"] mem_limits_q = quota_map["limits.memory"] - fmt.Printf("cpu_req:%s cpu_lim:%s mem_req:%s mem_lim:%s\n", cpu_requests_q, cpu_limits_q, mem_requests_q, mem_limits_q) + //fmt.Printf("cpu_req:%s cpu_lim:%s mem_req:%s mem_lim:%s\n", cpu_requests_q, cpu_limits_q, mem_requests_q, mem_limits_q) } //Save raw bytes of the request; We will create overrides in kubeconfiggenerator @@ -1768,7 +1779,7 @@ func searchAnnotation(entries []Entry, instanceName, namespace, key string) (str // Serve method for webhook server func (whsvr *WebhookServer) serve(w http.ResponseWriter, r *http.Request) { - fmt.Print("## Received request ##") + //fmt.Print("## Received request ##") var body []byte if r.Body != nil { if data, err := ioutil.ReadAll(r.Body); err == nil { @@ -1800,8 +1811,8 @@ func (whsvr *WebhookServer) serve(w http.ResponseWriter, r *http.Request) { } } else { //fmt.Printf("%v\n", ar.Request) - fmt.Printf("####### METHOD:%s #######\n", ar.Request.Operation) - fmt.Println(r.URL.Path) + //fmt.Printf("####### METHOD:%s #######\n", ar.Request.Operation) + //fmt.Println(r.URL.Path) if r.URL.Path == "/mutate" { method := string(ar.Request.Operation) admissionResponse = whsvr.mutate(&ar, method) diff --git a/plugins/appresources.py b/plugins/appresources.py index 33d1360f..a6831b5a 100644 --- a/plugins/appresources.py +++ b/plugins/appresources.py @@ -8,7 +8,7 @@ class AppResourcesFinder(CRBase): - def _run_command(self, cmd): + def run_command(self, cmd): cmdOut = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).communicate() out = cmdOut[0].decode('utf-8') err = cmdOut[1].decode('utf-8') @@ -16,7 +16,7 @@ def _run_command(self, cmd): def _get_resources(self, kind, plural, targetNS, kubeconfig): cmd = "kubectl get " + plural + " -n " + targetNS + " " + kubeconfig - out, err = self._run_command(cmd) + out, err = self.run_command(cmd) resources = [] for line in out.split("\n"): res_details = {} @@ -33,7 +33,7 @@ def _get_resources(self, kind, plural, targetNS, kubeconfig): def get_kubeplus_ns(self, kubeconfig): cmd = 'kubectl get deployments -A ' + kubeconfig - out, err = self._run_command(cmd) + out, err = self.run_command(cmd) for line in out.split("\n"): if 'NAME' not in line: if 'kubeplus-deployment' in line: @@ -44,7 +44,7 @@ def get_kubeplus_ns(self, kubeconfig): def get_target_ns(self, kubeplus_ns, kind, instance, kubeconfig): cmd = 'kubectl get ' + kind + ' ' + instance + " -n " + kubeplus_ns + ' -o json ' + kubeconfig - out, err = self._run_command(cmd) + out, err = self.run_command(cmd) targetNS = '' releaseName = '' out = out.strip() @@ -61,7 +61,7 @@ def get_target_ns(self, kubeplus_ns, kind, instance, kubeconfig): def get_helm_resources(self, targetNS, helmrelease, kubeconfig): #print("Inside helm_resources") cmd = "helm get all " + helmrelease + " -n " + targetNS + ' ' + kubeconfig - out, err = self._run_command(cmd) + out, err = self.run_command(cmd) resources = [] kind = '' @@ -98,7 +98,7 @@ def get_pods(self, targetNS, kind, instance, kubeconfig): def check_res_exists(self, kind, instance, kubeconfig): cmd = 'kubectl get ' + kind + ' -A ' + kubeconfig - out, err = self._run_command(cmd) + out, err = self.run_command(cmd) for line in out.split("\n"): if instance in line: parts = line.split(" ") @@ -112,7 +112,7 @@ def verify_kind_is_consumerapi(self, kind, kubeconfig): return False cmd = 'kubectl get crds ' + kubeconfig - out, err = self._run_command(cmd) + out, err = self.run_command(cmd) for line in out.split("\n"): parts = line.split(" ") fqn = parts[0].strip() diff --git a/plugins/crlicense.py b/plugins/crlicense.py new file mode 100644 index 00000000..cd0e2c0e --- /dev/null +++ b/plugins/crlicense.py @@ -0,0 +1,85 @@ +import argparse +import os +import subprocess + +from crmetrics import CRBase + +class LicenseManager(CRBase): + + def create_license(self, kind, license_file, expiry, num_of_instances, kubeconfig): + kubeplus_ns = self.get_kubeplus_namespace(kubeconfig) + kind_lower = kind.lower() + license_configmap = kind_lower + "-license" + cmd = "kubectl create configmap " + license_configmap + " --from-file=license_file=" + license_file + " -n " + kubeplus_ns + " --kubeconfig=" + kubeconfig + out, err = self.run_command(cmd) + if out != '': + msg = "License for Kind {kind} created.".format(kind=kind) + print(msg) + if err != '': + msg = "License for Kind {kind} already exists.".format(kind=kind) + print(msg) + + if expiry != "": + cmd = "kubectl annotate configmap " + license_configmap + " expiry=" + expiry + " -n " + kubeplus_ns + " --kubeconfig=" + kubeconfig + self.run_command(cmd) + + if num_of_instances != "": + cmd = "kubectl annotate configmap " + license_configmap + " allowed_instances=" + num_of_instances + " -n " + kubeplus_ns + " --kubeconfig=" + kubeconfig + self.run_command(cmd) + + +if __name__ == '__main__': + license_mgr = LicenseManager() + + parser = argparse.ArgumentParser() + parser.add_argument("action", help="action to perform") + parser.add_argument("kind", help="Kind name") + parser.add_argument("licensefile", help="File with license contents.") + parser.add_argument("-k", "--kubeconfig", help="Provider kubeconfig file.") + parser.add_argument("-e", "--expiry", help="Expiry date for the license.") + parser.add_argument("-n", "--appinstances", help="Allowed number of app instances to create.") + + args = parser.parse_args() + + action = args.action + kind = args.kind + license_file = args.licensefile + + if not os.path.isfile(license_file): + print("License file " + license_file + " does not exist.") + exit(0) + + kubeconfig = '' + if args.kubeconfig: + kubeconfig = args.kubeconfig + + if not os.path.isfile(kubeconfig): + print("Provider kubeconfig file " + kubeconfig + " does not exist.") + exit(0) + + expiry = '' + if args.expiry: + expiry = args.expiry + parts = expiry.split("/") + if len(parts) < 3: + print("Required expiry date format: MM/DD/YYYY") + exit(0) + + appinstances = '' + if args.appinstances: + appinstances = args.appinstances + if int(appinstances) <= 0: + print("App instances should be > 0.") + exit(0) + + if not license_mgr.check_kind(kind, kubeconfig): + print("Kind " + kind + " does not exist in the cluster in the platformapi.kubeplus api group.") + exit(0) + + if action == "create": + if expiry == '' and appinstances == '': + print("Both expiry date and number of app instances to create is empty.") + print("Specify at least one criteria.") + exit(0) + + license_mgr.create_license(kind, license_file, expiry, appinstances, kubeconfig) diff --git a/plugins/crmetrics.py b/plugins/crmetrics.py index 721f0ff9..a8dc7b49 100644 --- a/plugins/crmetrics.py +++ b/plugins/crmetrics.py @@ -10,6 +10,29 @@ import utils class CRBase(object): + + def run_command(self, cmd): + cmdOut = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).communicate() + out = cmdOut[0].decode('utf-8') + err = cmdOut[1].decode('utf-8') + return out, err + + def check_kind(self, kind, kubeconfigfile): + cmd = "kubectl api-resources --api-group='platformapi.kubeplus' --no-headers --kubeconfig=" + kubeconfigfile + out, err = self.run_command(cmd) + if out != "": + available_kinds = [] + for line in out.split("\n"): + if "NAME" not in line: + line1 = ' '.join(line.split()) + parts = line1.split(' ') + available_kind = parts[-1].strip() + available_kinds.append(available_kind) + + if kind in available_kinds: + return True + else: + return False def parse_pod_details(self, out, instance): pod_list = [] @@ -71,9 +94,9 @@ def get_pods(self, kind, instance, kubeconfig): #print(pod_list) return pod_list - def _get_kubeplus_namespace(self): + def get_kubeplus_namespace(self, kubeconfig): kb_namespace = 'default' - cmd = "kubectl get pods -A | grep kubeplus-deployment | awk '{print $1}'" + cmd = "kubectl get pods -A --kubeconfig=" + kubeconfig + " | grep kubeplus-deployment | awk '{print $1}'" try: out = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).communicate()[0] @@ -96,7 +119,7 @@ def get_resources_connections(self, kind, instance, namespace, kubeconfig): else: print("OS not supported:" + platf) return json_output - kb_ns = self._get_kubeplus_namespace() + kb_ns = self.get_kubeplus_namespace(kubeconfig) cmd = cmd + kind + ' ' + instance + ' ' + namespace + ' --output=json ' + kubeconfig + ' --ignore=ServiceAccount:default,Namespace:' + kb_ns #print(cmd) out = '' @@ -572,7 +595,7 @@ def _get_pods_for_cr_connections(self, cr, cr_instance, namespace, kubeconfig, c platf = platform.system() kubeplus_home = os.getenv('KUBEPLUS_HOME', '/') cmd = '' - kb_ns = self._get_kubeplus_namespace() + kb_ns = self.get_kubeplus_namespace(kubeconfig) if platf == "Darwin": cmd = kubeplus_home + '/plugins/kubediscovery-macos connections ' + cr + ' ' + cr_instance + ' ' + namespace + ' --output=' + conn_op_format + ' --ignore=ServiceAccount:default,Namespace:' + kb_ns if platf == "Linux": diff --git a/plugins/kubectl-kubeplus-commands b/plugins/kubectl-kubeplus-commands index 43bcea13..1b1b4e05 100755 --- a/plugins/kubectl-kubeplus-commands +++ b/plugins/kubectl-kubeplus-commands @@ -17,6 +17,9 @@ print_help () { echo " kubectl upload chart" echo " kubectl show provider permissions" echo " kubectl show consumer permissions" + echo " kubectl license create" + echo " kubectl license get" + echo " kubectl license delete" echo "" echo "DESCRIPTION" echo " KubePlus provides a suite of kubectl plugins to discover, monitor and troubleshoot Kubernetes applications." @@ -47,6 +50,10 @@ print_help () { echo "" echo " - kubectl show provider permissions shows the permissions for kubeplus-saas-provider service account in the namespace where kubeplus is installed." echo " - kubectl show consumer permissions shows the permissions for kubeplus-saas-consumer service account in the namespace where kubeplus is installed." + echo " License Management." + echo " - kubectl license create - creates license for a Kind" + echo " - kubectl license get - gets license for a Kind" + echo " - kubectl license delete - deletes license for a Kind" exit 0 } diff --git a/plugins/kubectl-license-create b/plugins/kubectl-license-create new file mode 100755 index 00000000..847a47be --- /dev/null +++ b/plugins/kubectl-license-create @@ -0,0 +1,5 @@ +#!/bin/bash + +python3 $KUBEPLUS_HOME/plugins/crlicense.py create "$@" + + diff --git a/plugins/kubectl-license-delete b/plugins/kubectl-license-delete new file mode 100755 index 00000000..29a209a9 --- /dev/null +++ b/plugins/kubectl-license-delete @@ -0,0 +1,54 @@ +#!/bin/bash + +source utils.sh + +print_help () { + echo "NAME" + echo " kubectl license delete" + echo "" + echo "SYNOPSIS" + echo " kubectl license delete -k " + echo "" + echo "DESCRIPTION" + echo " kubectl license delete deletes the license registered for the Kind." + exit 0 +} + +if (( $# < 3 )); then + print_help +fi + +kind=$1 +shift; + +while getopts ":k:h" opt; do + case ${opt} in + k ) + kubeconfig=$OPTARG + if [ ! -f $kubeconfig ]; then + echo "Kubeconfig $kubeconfig does not exist." + exit 0 + fi;; + h ) print_help;; + ? ) + echo "Invalid option: ${1} " 1>&2 + print_help + exit 0 + ;; + esac +done + +check_kind $kind $kubeconfig + +lowercase_kind=`echo "$kind" | tr '[:upper:]' '[:lower:]'` +kubeplus_ns=`get_kubeplus_ns $kubeconfig` +kubectl delete configmap $lowercase_kind-license -n $kubeplus_ns --kubeconfig=$kubeconfig &> /dev/null + +if [[ $? == 0 ]]; then + echo "License for Kind $kind deleted." +else + echo "License for Kind $kind not found." +fi + + + diff --git a/plugins/kubectl-license-get b/plugins/kubectl-license-get new file mode 100755 index 00000000..973fe533 --- /dev/null +++ b/plugins/kubectl-license-get @@ -0,0 +1,56 @@ +#!/bin/bash + +source utils.sh + +print_help () { + echo "NAME" + echo " kubectl license get" + echo "" + echo "SYNOPSIS" + echo " kubectl license get -k " + echo "" + echo "DESCRIPTION" + echo " kubectl license get retrieves the license registered for the Kind." + exit 0 +} + +if (( $# < 1 )); then + print_help +fi + +kind=$1 +shift; + +while getopts ":k:h" opt; do + case ${opt} in + k ) + kubeconfig=$OPTARG + if [ ! -f $kubeconfig ]; then + echo "Kubeconfig $kubeconfig does not exist." + exit 0 + fi;; + h ) print_help;; + ? ) + echo "Invalid option: ${1} " 1>&2 + print_help + exit 0 + ;; + esac +done + + +check_kind $kind $kubeconfig + +lowercase_kind=`echo "$kind" | tr '[:upper:]' '[:lower:]'` +kubeplus_ns=`get_kubeplus_ns $kubeconfig` +op=`kubectl get configmaps $lowercase_kind-license -o custom-columns=EXPIRY:.metadata.annotations.expiry,ALLOWED_INSTANCES:.metadata.annotations.allowed_instances,LICENSE_FILE:.data.license_file 2> /dev/null` + +if [[ $? == 0 ]]; then + echo "$op" +else + echo "License for Kind $kind not found." +fi + + + + diff --git a/plugins/utils.sh b/plugins/utils.sh index dba8d91f..8d9970a8 100644 --- a/plugins/utils.sh +++ b/plugins/utils.sh @@ -1,3 +1,10 @@ +get_kubeplus_ns() { + local kubeconfig=$1 + kubeplus_ns=`kubectl get deployments --kubeconfig=$kubeconfig -A | grep kubeplus | awk '{print $1}'` + echo $kubeplus_ns +} + + check_namespace() { local ns=$1 local kubeconfg=$2 @@ -21,14 +28,14 @@ check_kind() { local kind=$1 local kubeconfg=$2 - canonicalKindPresent=`kubectl api-resources --kubeconfig=$kubeconfg | grep -w $kind` + canonicalKindPresent=`kubectl api-resources --api-group='platformapi.kubeplus' --kubeconfig=$kubeconfg | grep -w $kind` OLDIFS=$IFS IFS=' ' read -a canonicalKindPresentArr <<< "$canonicalKindPresent" IFS=$OLDIFS if [[ "${#canonicalKindPresentArr}" == 0 ]]; then - echo "Unknown Kind $kind" + echo "Unknown Kind $kind in the platformapi.kubeplus api group." exit 0 fi diff --git a/tests/license.txt b/tests/license.txt new file mode 100644 index 00000000..0777950f --- /dev/null +++ b/tests/license.txt @@ -0,0 +1 @@ +Test License file. diff --git a/tests/tests.py b/tests/tests.py index 2824decd..cabcf818 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -86,6 +86,79 @@ def test_create_res_comp_with_incomplete_resource_quota(self): self.assertTrue( 'If quota is specified, specify all four values: requests.cpu, requests.memory, limits.cpu, limits.memory' in err) + def test_license_plugin(self): + if not TestKubePlus._is_kubeplus_running(): + print("KubePlus is not running. Deploy KubePlus and then run tests") + sys.exit(0) + + start_clean = "kubectl delete ns hs1" + TestKubePlus.run_command(start_clean) + + kubeplus_home = os.getenv("KUBEPLUS_HOME") + path = os.getenv("PATH") + if kubeplus_home == '': + print("Skipping the test as KUBEPLUS_HOME is not set.") + return + + cmd = "kubectl upload chart ../examples/multitenancy/hello-world/hello-world-chart-0.0.3.tgz ../kubeplus-saas-provider.json" + out, err = TestKubePlus.run_command(cmd) + cmd = "kubectl create -f ../examples/multitenancy/hello-world/hello-world-service-composition-localchart.yaml --kubeconfig=../kubeplus-saas-provider.json" + out, err = TestKubePlus.run_command(cmd) + + crd = "helloworldservices.platformapi.kubeplus" + crd_installed = self._check_crd_installed(crd) + if not crd_installed: + print("CRD " + crd + " not installed. Exiting this test.") + return + + # Test with license that restricts number of instances (1) + cmd = "kubectl license create HelloWorldService license.txt -n 1 -k ../kubeplus-saas-provider.json" + TestKubePlus.run_command(cmd) + + cmd = "kubectl create -f ../examples/multitenancy/hello-world/hs1.yaml --kubeconfig=../kubeplus-saas-provider.json" + TestKubePlus.run_command(cmd) + + all_running = False + cmd = "kubectl get pods -n hs1" + + target_pod_count = 1 + pods, count, all_running = self._check_pod_status(cmd, target_pod_count) + if count == target_pod_count: + self.assertTrue(True) + else: + self.assertTrue(False) + + # Second instance creation should be denied + cmd = "kubectl create -f ../examples/multitenancy/hello-world/hs2.yaml --kubeconfig=../kubeplus-saas-provider.json" + out, err = TestKubePlus.run_command(cmd) + self.assertTrue("Allowed number of instances reached" in err) + cmd = "kubectl license delete HelloWorldService -k ../kubeplus-saas-provider.json" + TestKubePlus.run_command(cmd) + + # Test with expired license + cmd = "kubectl license create HelloWorldService license.txt -e 01/01/2024 -k ../kubeplus-saas-provider.json" + TestKubePlus.run_command(cmd) + cmd = "kubectl create -f ../examples/multitenancy/hello-world/hs2.yaml --kubeconfig=../kubeplus-saas-provider.json" + out, err = TestKubePlus.run_command(cmd) + self.assertTrue("License expired (expiry date):01/01/2024" in err) + cmd = "kubectl license delete HelloWorldService -k ../kubeplus-saas-provider.json" + TestKubePlus.run_command(cmd) + + # Test with expired license and restriction on number of instances + cmd = "kubectl license create HelloWorldService license.txt -n 1 -e 01/01/2024 -k ../kubeplus-saas-provider.json" + TestKubePlus.run_command(cmd) + cmd = "kubectl create -f ../examples/multitenancy/hello-world/hs2.yaml --kubeconfig=../kubeplus-saas-provider.json" + out, err = TestKubePlus.run_command(cmd) + self.assertTrue("License expired (expiry date):01/01/2024" in err) + self.assertTrue("Allowed number of instances reached" in err) + cmd = "kubectl license delete HelloWorldService -k ../kubeplus-saas-provider.json" + TestKubePlus.run_command(cmd) + + # cleanup + cmd = "kubectl delete -f ../examples/multitenancy/hello-world/hello-world-service-composition-localchart.yaml --kubeconfig=../kubeplus-saas-provider.json" + out, err = TestKubePlus.run_command(cmd) + + def test_application_update(self): if not TestKubePlus._is_kubeplus_running(): print("KubePlus is not running. Deploy KubePlus and then run tests") @@ -187,6 +260,20 @@ def count_users(response): num_users += 1 return num_users + def cleanup(): + cmd = 'kubectl delete -f ./application-upgrade/resource-composition-localchart.yaml --kubeconfig=./application-upgrade/provider.conf' + TestKubePlus.run_command(cmd) + + # restore chart + data = None + with open('./application-upgrade/resource-composition-localchart.yaml', 'r') as f: + data = yaml.safe_load(f) + + data['spec']['newResource']['chartURL'] = 'file:///resource-composition-0.0.1.tgz' + + with open('./application-upgrade/resource-composition-localchart.yaml', 'w') as f: + yaml.safe_dump(data, f, default_flow_style=False) + # preliminary checks if not TestKubePlus._is_kubeplus_running(): print("KubePlus is not running. Deploy KubePlus and then run tests") @@ -225,7 +312,8 @@ def count_users(response): port = 5000 # let the app pods come up - time.sleep(30) + wait_time = 60 + time.sleep(wait_time) # grab name of deployed pod cmd = "kubectl get pods -n %s" % namespace @@ -239,6 +327,12 @@ def count_users(response): name = part.strip() break + print("Pod name:" + name) + if name == None: + print("Pod did not come up even after waiting " + str(wait_time) + " seconds.") + print("Skipping rest of the test.") + cleanup() + # port forwarding # CLI: kubectl port-forward pod-name -n bwa-tenant1 5000:5000 config.load_kube_config() @@ -247,6 +341,7 @@ def count_users(response): Configuration.set_default(c) api_instance = core_v1_api.CoreV1Api() + # https://github.com/kubernetes-client/python/blob/master/examples/pod_portforward.py pf = portforward(api_instance.connect_get_namespaced_pod_portforward, name, @@ -292,19 +387,7 @@ def count_users(response): response = make_http_request(port).strip(" ") num_users_second = count_users(response) - # cleanup - cmd = 'kubectl delete -f ./application-upgrade/resource-composition-localchart.yaml --kubeconfig=./application-upgrade/provider.conf' - TestKubePlus.run_command(cmd) - - # restore chart - data = None - with open('./application-upgrade/resource-composition-localchart.yaml', 'r') as f: - data = yaml.safe_load(f) - - data['spec']['newResource']['chartURL'] = 'file:///resource-composition-0.0.1.tgz' - - with open('./application-upgrade/resource-composition-localchart.yaml', 'w') as f: - yaml.safe_dump(data, f, default_flow_style=False) + cleanup() # check if upgrade worked self.assertTrue(num_users_second > num_users_first)