diff options
author | vikaskumar <vkumar@aarnanetworks.com> | 2020-11-26 13:11:45 +0530 |
---|---|---|
committer | vikaskumar <vkumar@aarnanetworks.com> | 2020-11-27 12:46:26 +0530 |
commit | e7f3bf3050608edec03aa9d52cf8de79d56dfbd9 (patch) | |
tree | e95a91ade123f2724ff2283fef989976cf074eb0 /src/tools | |
parent | 7e06fbaa3d1293ca9b25aeb7ea7cb7be2179e30a (diff) |
MULTICLOUD-1257 updated gui flow
Issue-ID: MULTICLOUD-1257
Change-Id: I5c1432c037952abeed6066cb067192076031f9cd
Signed-off-by: vikaskumar <vkumar@aarnanetworks.com>
Diffstat (limited to 'src/tools')
64 files changed, 8310 insertions, 1853 deletions
diff --git a/src/tools/emcoui/Dockerfile b/src/tools/emcoui/Dockerfile index 6f0cc2fa..8224d92c 100644 --- a/src/tools/emcoui/Dockerfile +++ b/src/tools/emcoui/Dockerfile @@ -1,3 +1,18 @@ +#======================================================================= +# Copyright (c) 2017-2020 Aarna Networks, Inc. +# All rights reserved. +# ====================================================================== +# 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. +# ======================================================================== + # => Build container FROM node:alpine as builder WORKDIR /app @@ -7,11 +22,14 @@ RUN npm install COPY src ./src COPY public ./public # => Pass the reuired version -RUN REACT_APP_VERSION=v1.0.0 npm run build +RUN REACT_APP_VERSION=v1.2.0 npm run build # => Run container FROM nginx:1.15.2-alpine +# Nginx config +COPY default.conf /etc/nginx/conf.d/ + # Static build COPY --from=builder /app/build /usr/share/nginx/html/ diff --git a/src/tools/emcoui/README.md b/src/tools/emcoui/README.md index 05a43237..945dde0c 100644 --- a/src/tools/emcoui/README.md +++ b/src/tools/emcoui/README.md @@ -1,30 +1,11 @@ -# EMCOUI - -This is a web app for EMCO V2 api's. This is a reactjs based UI app created using google material ui library. - ## Local setup -for running the app in a local setup first install the dependencies by running - -```bash -npm install -``` - -Then run - -```bash -startup.sh -``` +for running the app in a local setup first install the dependencies by running `npm install`. +Then run `startup.sh` ## Production build -for creating a production build, run - -```bash -npm run build -``` - -A production ready build will be available at /build directory +for creating a production build, run `npm run build`. A production ready build will be available at /build directory ## Available scripts @@ -48,17 +29,4 @@ The build is minified and the filenames include the hashes.<br /> ## Building docker image To build a docker image run the below command - -```bash -docker build -t <image_name>:<image_version> . -``` - -## Installing with helm - -All the helm chars are in `helm` directory. - -Update values.yaml in `helm` directory with the required emcoui image and then run - -```bash -helm install --name <app name> --namespace < namespace > -``` +`docker build -t image_name:version .` diff --git a/src/tools/emcoui/default.conf b/src/tools/emcoui/default.conf new file mode 100644 index 00000000..4387a613 --- /dev/null +++ b/src/tools/emcoui/default.conf @@ -0,0 +1,34 @@ +server { + listen 9080; + server_name localhost; + location / { + root /usr/share/nginx/html; + index index.html; + try_files $uri $uri/ /index.html; + } + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } + + location /middleend { + proxy_pass http://middleend.onap4k8s.svc.cluster.local:9081; + } + location /v2/controllers { + proxy_pass http://orchestrator.onap4k8s.svc.cluster.local:9015; + } + location /v2/projects { + proxy_pass http://orchestrator.onap4k8s.svc.cluster.local:9015; + } + location /v2/cluster-providers { + proxy_pass http://clm.onap4k8s.svc.cluster.local:9061; + } + location /v2/ovnaction { + rewrite ^/v2/ovnaction/(.*) /v2/projects/$1 break; + proxy_pass http://ovnaction.onap4k8s.svc.cluster.local:9051; + } + location /v2/ncm { + rewrite ^/v2/ncm/(.*) /v2/cluster-providers/$1 break; + proxy_pass http://ncm.onap4k8s.svc.cluster.local:9031; + } +} diff --git a/src/tools/emcoui/helm/emcoui/templates/configmap.yaml b/src/tools/emcoui/helm/emcoui/templates/configmap.yaml index a9ba34a5..b62f35dc 100644 --- a/src/tools/emcoui/helm/emcoui/templates/configmap.yaml +++ b/src/tools/emcoui/helm/emcoui/templates/configmap.yaml @@ -11,7 +11,27 @@ # 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. -# ======================================================================== +# ======================================================================== +# middleend config +apiVersion: v1 +kind: ConfigMap +metadata: + name: middleend-config +data: + middleend.conf: |- + { + "ownport": "{{ .Values.middleend.service.internalPort }}", + "orchestrator": "orchestrator.{{ .Values.namespace }}.svc.cluster.local:9015", + "clm": "clm.{{ .Values.namespace }}.svc.cluster.local:9061", + "ovnaction": "ovnaction.{{ .Values.namespace }}.svc.cluster.local:9051", + "issuer": "{{ .Values.authproxy.issuer }}", + "redirect_uri": "{{ .Values.authproxy.redirect_uri }}", + "client_id": "{{ .Values.authproxy.client_id }}", + "mongo": "mongo.{{ .Values.namespace }}.svc.cluster.local:27017" + } + +--- +# emcoui config apiVersion: v1 kind: ConfigMap metadata: @@ -19,7 +39,7 @@ metadata: data: my-nginx-config.conf: | server { - listen {{ .Values.service.internalPort }}; + listen {{ .Values.emcoui.service.internalPort }}; server_name localhost; location / { root /usr/share/nginx/html; @@ -30,6 +50,9 @@ data: location = /50x.html { root /usr/share/nginx/html; } + location /middleend { + proxy_pass http://middleend.{{ .Values.namespace }}.svc.cluster.local:9081; + } location /v2/controllers { proxy_pass http://orchestrator.{{ .Values.namespace }}.svc.cluster.local:9015; } diff --git a/src/tools/emcoui/helm/emcoui/templates/deployment.yaml b/src/tools/emcoui/helm/emcoui/templates/deployment.yaml index 11ab6f52..f1609cd3 100644 --- a/src/tools/emcoui/helm/emcoui/templates/deployment.yaml +++ b/src/tools/emcoui/helm/emcoui/templates/deployment.yaml @@ -12,27 +12,58 @@ # See the License for the specific language governing permissions and # limitations under the License. # ======================================================================== +# middleend Deployment +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Values.middleend.service.name }} +spec: + replicas: 1 + selector: + matchLabels: + app: {{ .Values.middleend.service.label }} + template: + metadata: + labels: + app: {{ .Values.middleend.service.label }} + spec: + containers: + - name: {{ .Values.middleend.service.name }} + image: "{{ .Values.middleend.image.repository }}:{{ .Values.middleend.image.tag }}" + imagePullPolicy: Always + ports: + - containerPort: {{ .Values.middleend.service.internalPort }} + volumeMounts: + - mountPath: /opt/emco/config + readOnly: true + name: config + volumes: + - name: config + configMap: + name: middleend-config + +--- # GUI Deployment apiVersion: apps/v1 kind: Deployment metadata: - name: {{ .Values.service.name }} + name: {{ .Values.emcoui.service.name }} spec: replicas: 1 selector: matchLabels: - app: {{ .Values.service.label }} + app: {{ .Values.emcoui.service.label }} template: metadata: labels: - app: {{ .Values.service.label }} + app: {{ .Values.emcoui.service.label }} spec: containers: - - name: {{ .Values.service.name }} - image: {{ .Values.image }} + - name: {{ .Values.emcoui.service.name }} + image: "{{ .Values.emcoui.image.repository }}:{{ .Values.emcoui.image.tag }}" imagePullPolicy: Always ports: - - containerPort: {{ .Values.service.internalPort }} + - containerPort: {{ .Values.emcoui.service.internalPort }} volumeMounts: - mountPath: /etc/nginx/conf.d readOnly: true diff --git a/src/tools/emcoui/helm/emcoui/templates/service.yaml b/src/tools/emcoui/helm/emcoui/templates/service.yaml index 2c09a7de..9c88b8f7 100644 --- a/src/tools/emcoui/helm/emcoui/templates/service.yaml +++ b/src/tools/emcoui/helm/emcoui/templates/service.yaml @@ -12,24 +12,48 @@ # See the License for the specific language governing permissions and # limitations under the License. # ======================================================================== -# GUI Service +# middleend Service apiVersion: v1 kind: Service metadata: - name: {{ .Values.service.name}} + name: {{ .Values.middleend.service.name}} labels: - app: {{ .Values.service.label }} + app: {{ .Values.middleend.service.label }} spec: selector: - app: {{ .Values.service.name }} - type: {{ .Values.service.type }} + app: {{ .Values.middleend.service.name }} + type: {{ .Values.middleend.service.type }} ports: - - name: {{ .Values.service.PortName }} - {{if eq .Values.service.type "NodePort" -}} - port: {{ .Values.service.internalPort }} - nodePort: {{ .Values.global.nodePortPrefixExt | default "302" }}{{ .Values.service.nodePort }} + - name: {{ .Values.middleend.service.PortName }} + {{if eq .Values.middleend.service.type "NodePort" -}} + port: {{ .Values.middleend.service.internalPort }} + nodePort: {{ .Values.global.nodePortPrefixExt | default "302" }}{{ .Values.middleend.service.nodePort }} {{- else -}} - port: {{ .Values.service.externalPort }} - targetPort: {{ .Values.service.internalPort }} + port: {{ .Values.middleend.externalPort }} + targetPort: {{ .Values.middleend.internalPort }} + {{- end}} + protocol: TCP + + +--- +# emcoui service +apiVersion: v1 +kind: Service +metadata: + name: {{ .Values.emcoui.service.name}} + labels: + app: {{ .Values.emcoui.service.label }} +spec: + selector: + app: {{ .Values.emcoui.service.name }} + type: {{ .Values.emcoui.service.type }} + ports: + - name: {{ .Values.emcoui.service.PortName }} + {{if eq .Values.emcoui.service.type "NodePort" -}} + port: {{ .Values.emcoui.service.internalPort }} + nodePort: {{ .Values.global.nodePortPrefixExt | default "302" }}{{ .Values.emcoui.service.nodePort }} + {{- else -}} + port: {{ .Values.emcoui.service.externalPort }} + targetPort: {{ .Values.emcoui.service.internalPort }} {{- end}} protocol: TCP diff --git a/src/tools/emcoui/helm/emcoui/values.yaml b/src/tools/emcoui/helm/emcoui/values.yaml index f52c0719..d764f722 100644 --- a/src/tools/emcoui/helm/emcoui/values.yaml +++ b/src/tools/emcoui/helm/emcoui/values.yaml @@ -11,7 +11,7 @@ # 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. -# ======================================================================== +# ======================================================================== global: nodePortPrefixExt: 304 @@ -19,11 +19,6 @@ global: ################################################################# # Application configuration defaults. ################################################################# -# application image -repository: registry.hub.docker.com -image: emcov2/emcoui:stable -pullPolicy: Always - # default number of instances replicaCount: 1 @@ -44,14 +39,43 @@ readiness: initialDelaySeconds: 10 periodSeconds: 30 -service: - type: NodePort - name: emcoui - portName: emcoui - internalPort: 9080 - externalPort: 9080 - nodePort: 80 - label: emcoui +middleend: + service: + type: NodePort + name: middleend + portName: middleend + internalPort: 9081 + externalPort: 9081 + nodePort: 81 + label: middleend + + image: + registry: registry.hub.docker.com + repository: amcop/middleend + tag: master + pullPolicy: Always + +emcoui: + service: + type: NodePort + name: emcoui + portName: emcoui + internalPort: 9080 + externalPort: 9080 + nodePort: 80 + label: emcoui + + image: + registry: registry.hub.docker.com + repository: amcop/emcoui + tag: master + pullPolicy: Always + +authproxy: + # These values should be updated at the time of deployment + issuer: http://192.168.122.224:31064/auth/realms/EMCO/ + redirect_uri: http://192.168.122.224:30481/middleend/callback + client_id: emcoapp ingress: enabled: false diff --git a/src/tools/emcoui/middle_end/Dockerfile b/src/tools/emcoui/middle_end/Dockerfile new file mode 100644 index 00000000..4e35322c --- /dev/null +++ b/src/tools/emcoui/middle_end/Dockerfile @@ -0,0 +1,32 @@ +#======================================================================= +# Copyright (c) 2017-2020 Aarna Networks, Inc. +# All rights reserved. +# ====================================================================== +# 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. +# ======================================================================== + +FROM golang:1.14.1 + +# Set the Current Working Directory inside the container +WORKDIR /src +COPY ./ ./ +RUN make all + +# Build the Go app +FROM ubuntu:16.04 +WORKDIR /opt/emco +RUN groupadd -r emco && useradd -r -g emco emco +RUN chown emco:emco /opt/emco -R +RUN mkdir ./config +COPY --chown=emco --from=0 /src/middleend ./ + +# Command to run the executable +CMD ["./middleend"] diff --git a/src/tools/emcoui/middle_end/Makefile b/src/tools/emcoui/middle_end/Makefile new file mode 100644 index 00000000..a44fc785 --- /dev/null +++ b/src/tools/emcoui/middle_end/Makefile @@ -0,0 +1,24 @@ +#======================================================================= +# Copyright (c) 2017-2020 Aarna Networks, Inc. +# All rights reserved. +# ====================================================================== +# 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. +# ======================================================================== + +export GO111MODULE=on + +all: clean + CGO_ENABLED=1 GOOS=linux GOARCH=amd64 + @go build -tags netgo -o ./middleend ./main/main.go + +clean: + @find . -name "*so" -delete + @rm -f middleend diff --git a/src/tools/emcoui/middle_end/app/app.go b/src/tools/emcoui/middle_end/app/app.go new file mode 100644 index 00000000..a8511698 --- /dev/null +++ b/src/tools/emcoui/middle_end/app/app.go @@ -0,0 +1,1014 @@ +/* +======================================================================= +Copyright (c) 2017-2020 Aarna Networks, Inc. +All rights reserved. +====================================================================== +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 app + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "log" + "mime/multipart" + "net/http" + + "github.com/gorilla/mux" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" +) + +type deployServiceData struct { + Name string `json:"name"` + Description string `json:"description"` + Spec struct { + ProjectName string `json:"projectName"` + Apps []appsData `json:"appsData"` + } `json:"spec"` +} + +type deployDigData struct { + Name string `json:"name"` + Description string `json:"description"` + CompositeAppName string `json:"compositeApp"` + CompositeProfile string `json:"compositeProfile"` + DigVersion string `json:"version"` + CompositeAppVersion string `json:"compositeAppVersion"` + Spec struct { + ProjectName string `json:"projectName"` + Apps []appsData `json:"appsData"` + } `json:"spec"` +} + +// Exists is for mongo $exists filter +type Exists struct { + Exists string `json:"$exists"` +} + +// This is the json payload that the orchesration API expexts. +type appsData struct { + Metadata struct { + Name string `json:"name"` + Description string `json:"description"` + FileName string `json:"filename"` + } `json:"metadata"` + ProfileMetadata struct { + Name string `json:"name"` + FileName string `json:"filename"` + } `json:"profileMetadata"` + Clusters []struct { + Provider string `json:"provider"` + SelectedClusters []struct { + Name string `json:"name"` + Interfaces []struct { + NetworkName string `json:"networkName"` + IP string `json:"ip"` + Subnet string `json:"subnet"` + } `json:"interfaces"` + } `json:"selectedClusters"` + } `json:"clusters"` +} + +type DigsInProject struct { + Metadata struct { + Name string `json:"name"` + CompositeAppName string `json:"compositeAppName"` + CompositeAppVersion string `json:"compositeAppVersion"` + Description string `json:"description"` + UserData1 string `userData1:"userData1"` + UserData2 string `userData2:"userData2"` + } `json:"metadata"` + Spec struct { + DigIntentsData []DigDeployedIntents `json:"deployedIntents"` + Profile string `json:"profile"` + Version string `json:"version"` + Lcloud string `json:"logicalCloud"` + OverrideValuesObj []OverrideValues `json:"overrideValues"` + GpintArray []*DigsGpint `json:"GenericPlacementIntents,omitempty"` + NwintArray []*DigsNwint `json:"networkCtlIntents,omitempty"` + } `json:"spec"` +} + +type DigsGpint struct { + Metadata apiMetaData `json:"metadata,omitempty"` + Spec struct { + AppIntentArray []PlacementIntentExport `json:"placementIntent,omitempty"` + } `json:"spec,omitempty"` +} + +type DigsNwint struct { + Metadata apiMetaData `json:"metadata,omitempty"` + Spec struct { + WorkloadIntentsArray []*WorkloadIntents `json:"WorkloadIntents,omitempty"` + } `json:"spec,omitempty"` +} +type WorkloadIntents struct { + Metadata apiMetaData `json:"metadata,omitempty"` + Spec struct { + Interfaces []NwInterface `json:"interfaces,omitempty"` + } `json:"spec,omitempty"` +} + +// Project Tree +type ProjectTree struct { + Metadata ProjectMetadata + compositeAppMap map[string]*CompositeAppTree +} + +type treeTraverseFilter struct { + compositeAppName string + compositeAppVersion string + digName string +} + +// Composite app tree +type CompositeAppTree struct { + Metadata CompositeApp + AppsDataArray map[string]*AppsData + ProfileDataArray map[string]*ProfilesData + DigMap map[string]*DigReadData +} + +type DigReadData struct { + DigpData DeploymentIGP + DigIntentsData DigpIntents + GpintMap map[string]*GpintData + NwintMap map[string]*NwintData +} + +type GpintData struct { + Gpint GenericPlacementIntent + AppIntentArray []PlacementIntent +} + +type NwintData struct { + Nwint NetworkCtlIntent + WrkintMap map[string]*WrkintData +} + +type WrkintData struct { + Wrkint NetworkWlIntent + Interfaces []NwInterface +} + +type AppsData struct { + App CompositeApp + CompositeProfile ProfileMeta +} + +type ProfilesData struct { + Profile ProfileMeta + AppProfiles []ProfileMeta +} + +type ClusterMetadata struct { + Metadata apiMetaData `json:"Metadata"` +} + +type apiMetaData struct { + Name string `json:"name"` + Description string `json:"description"` + UserData1 string `userData1:"userData1"` + UserData2 string `userData2:"userData2"` +} + +// The interface +type orchWorkflow interface { + createAnchor() interface{} + createObject() interface{} + getObject() (interface{}, interface{}) + getAnchor() (interface{}, interface{}) + deleteObject() interface{} + deleteAnchor() interface{} +} + +// MiddleendConfig The configmap of the middleent +type MiddleendConfig struct { + OwnPort string `json:"ownport"` + Clm string `json:"clm"` + OrchService string `json:"orchestrator"` + OvnService string `json:"ovnaction"` + Mongo string `json:"mongo"` +} + +// OrchestrationHandler interface, handling the composite app APIs +type OrchestrationHandler struct { + MiddleendConf MiddleendConfig + client http.Client + compositeAppName string + compositeAppDesc string + AppName string + meta []appsData + DigData deployDigData + file map[string]*multipart.FileHeader + dataRead *ProjectTree + treeFilter *treeTraverseFilter + DigpReturnJson []DigsInProject + projectName string + projectDesc string + version string + response struct { + payload map[string][]byte + status map[string]int + } + digpIntents map[string]string + nwCtlIntents map[string]string +} + +// NewAppHandler interface implementing REST callhandler +func NewAppHandler() *OrchestrationHandler { + return &OrchestrationHandler{} +} + +// GetHealth to check connectivity +func (h OrchestrationHandler) GetHealth(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + +} + +func (h OrchestrationHandler) apiGet(url string, statusKey string) (interface{}, []byte, error) { + // prepare and DEL API + request, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, nil, err + } + resp, err := h.client.Do(request) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + + // Prepare the response + data, _ := ioutil.ReadAll(resp.Body) + h.response.payload[statusKey] = data + h.response.status[statusKey] = resp.StatusCode + + return resp.StatusCode, data, nil +} + +func (h OrchestrationHandler) apiDel(url string, statusKey string) (interface{}, error) { + // prepare and DEL API + request, err := http.NewRequest("DELETE", url, nil) + if err != nil { + return nil, err + } + resp, err := h.client.Do(request) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + // Prepare the response + data, _ := ioutil.ReadAll(resp.Body) + h.response.payload[statusKey] = data + h.response.status[statusKey] = resp.StatusCode + + return resp.StatusCode, nil +} + +func (h OrchestrationHandler) apiPost(jsonLoad []byte, url string, statusKey string) (interface{}, error) { + // prepare and POST API + request, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonLoad)) + if err != nil { + return nil, err + } + resp, err := h.client.Do(request) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + // Prepare the response + data, _ := ioutil.ReadAll(resp.Body) + h.response.payload[statusKey] = data + h.response.status[statusKey] = resp.StatusCode + + return resp.StatusCode, nil +} + +func (h OrchestrationHandler) apiPostMultipart(jsonLoad []byte, + fh *multipart.FileHeader, url string, statusKey string, fileName string) (interface{}, error) { + // Open the file + file, err := fh.Open() + if err != nil { + return nil, err + } + // Close the file later + defer file.Close() + // Buffer to store our request body as bytes + var requestBody bytes.Buffer + // Create a multipart writer + multiPartWriter := multipart.NewWriter(&requestBody) + // Initialize the file field. Arguments are the field name and file name + // It returns io.Writer + fileWriter, err := multiPartWriter.CreateFormFile("file", fileName) + if err != nil { + return nil, err + } + // Copy the actual file content to the field field's writer + _, err = io.Copy(fileWriter, file) + if err != nil { + return nil, err + } + // Populate other fields + fieldWriter, err := multiPartWriter.CreateFormField("metadata") + if err != nil { + return nil, err + } + + _, err = fieldWriter.Write([]byte(jsonLoad)) + if err != nil { + return nil, err + } + + // We completed adding the file and the fields, let's close the multipart writer + // So it writes the ending boundary + multiPartWriter.Close() + + // By now our original request body should have been populated, + // so let's just use it with our custom request + req, err := http.NewRequest("POST", url, &requestBody) + if err != nil { + return nil, err + } + // We need to set the content type from the writer, it includes necessary boundary as well + req.Header.Set("Content-Type", multiPartWriter.FormDataContentType()) + + // Do the request + resp, err := h.client.Do(req) + if err != nil { + log.Fatalln(err) + return nil, err + } + defer resp.Body.Close() + // Prepare the response + data, _ := ioutil.ReadAll(resp.Body) + h.response.payload[statusKey] = data + h.response.status[statusKey] = resp.StatusCode + + return resp.StatusCode, nil +} +func (h *OrchestrationHandler) prepTreeReq(vars map[string]string) { + // Initialise the project tree with target composite application. + h.treeFilter = &treeTraverseFilter{} + h.treeFilter.compositeAppName = vars["composite-app-name"] + h.treeFilter.compositeAppVersion = vars["version"] + h.treeFilter.digName = vars["deployment-intent-group-name"] +} + +func (h *OrchestrationHandler) DelDig(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + h.projectName = vars["project-name"] + h.treeFilter = nil + + dataPoints := []string{"projectHandler", "compAppHandler", + "digpHandler", + "placementIntentHandler", + "networkIntentHandler"} + h.response.status = make(map[string]int) + h.response.payload = make(map[string][]byte) + + // Initialise the project tree with target composite application. + h.prepTreeReq(vars) + + h.dataRead = &ProjectTree{} + retcode := h.constructTree(dataPoints) + if retcode != nil { + if intval, ok := retcode.(int); ok { + w.WriteHeader(intval) + } else { + w.WriteHeader(500) + } + return + } + + // 1. Call DIG delte workflow + fmt.Printf("Delete wflow start") + deleteDataPoints := []string{"networkIntentHandler", + "placementIntentHandler", + "digpHandler"} + retcode = h.deleteTree(deleteDataPoints) + if retcode != nil { + if intval, ok := retcode.(int); ok { + w.WriteHeader(intval) + } else { + w.WriteHeader(500) + } + return + } + w.WriteHeader(204) +} + +// Delete service workflow +func (h *OrchestrationHandler) DelSvc(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + h.projectName = vars["project-name"] + h.treeFilter = nil + + dataPoints := []string{"projectHandler", "compAppHandler", + "digpHandler", + "ProfileHandler"} + h.response.status = make(map[string]int) + h.response.payload = make(map[string][]byte) + + // Initialise the project tree with target composite application. + h.prepTreeReq(vars) + + h.dataRead = &ProjectTree{} + retcode := h.constructTree(dataPoints) + if retcode != nil { + if intval, ok := retcode.(int); ok { + w.WriteHeader(intval) + } else { + w.WriteHeader(500) + } + return + } + fmt.Printf("tree %+v\n", h.dataRead) + // Check if a dig is present in this composite application + if len(h.dataRead.compositeAppMap[vars["composite-app-name"]].DigMap) != 0 { + w.WriteHeader(409) + w.Write([]byte("Non emtpy DIG in service\n")) + return + } + + // 1. Call delte workflow + fmt.Printf("Delete wflow start") + deleteDataPoints := []string{"ProfileHandler", + "compAppHandler"} + retcode = h.deleteTree(deleteDataPoints) + if retcode != nil { + if intval, ok := retcode.(int); ok { + w.WriteHeader(intval) + } else { + w.WriteHeader(500) + } + return + } + w.WriteHeader(204) +} + +func (h *OrchestrationHandler) getData(I orchWorkflow) (interface{}, interface{}) { + _, retcode := I.getAnchor() + if retcode != 200 { + return nil, retcode + } + dataPointData, retcode := I.getObject() + if retcode != 200 { + return nil, retcode + } + return dataPointData, retcode +} + +func (h *OrchestrationHandler) deleteData(I orchWorkflow) (interface{}, interface{}) { + _ = I.deleteObject() + _ = I.deleteAnchor() + return nil, 204 //FIXME +} + +func (h *OrchestrationHandler) deleteTree(dataPoints []string) interface{} { + //1. Fetch App data + var I orchWorkflow + for _, dataPoint := range dataPoints { + switch dataPoint { + case "projectHandler": + temp := &projectHandler{} + temp.orchInstance = h + I = temp + _, retcode := h.deleteData(I) + if retcode != 204 { + return retcode + } + break + case "compAppHandler": + temp := &compAppHandler{} + temp.orchInstance = h + I = temp + _, retcode := h.deleteData(I) + if retcode != 204 { + return retcode + } + break + case "ProfileHandler": + temp := &ProfileHandler{} + temp.orchInstance = h + I = temp + _, retcode := h.deleteData(I) + if retcode != 204 { + return retcode + } + break + case "digpHandler": + temp := &digpHandler{} + temp.orchInstance = h + I = temp + fmt.Printf("delete digp\n") + _, retcode := h.deleteData(I) + if retcode != 204 { + return retcode + } + break + case "placementIntentHandler": + temp := &placementIntentHandler{} + temp.orchInstance = h + I = temp + _, retcode := h.deleteData(I) + if retcode != 204 { + return retcode + } + break + case "networkIntentHandler": + temp := &networkIntentHandler{} + temp.orchInstance = h + I = temp + _, retcode := h.deleteData(I) + if retcode != 204 { + return retcode + } + break + default: + fmt.Printf("%s\n", dataPoint) + } + } + return nil +} + +func (h *OrchestrationHandler) constructTree(dataPoints []string) interface{} { + //1. Fetch App data + var I orchWorkflow + for _, dataPoint := range dataPoints { + switch dataPoint { + case "projectHandler": + temp := &projectHandler{} + temp.orchInstance = h + I = temp + _, retcode := h.getData(I) + if retcode != 200 { + return retcode + } + break + case "compAppHandler": + temp := &compAppHandler{} + temp.orchInstance = h + I = temp + _, retcode := h.getData(I) + if retcode != 200 { + return retcode + } + break + case "ProfileHandler": + temp := &ProfileHandler{} + temp.orchInstance = h + I = temp + _, retcode := h.getData(I) + if retcode != 200 { + return retcode + } + break + case "digpHandler": + temp := &digpHandler{} + temp.orchInstance = h + I = temp + _, retcode := h.getData(I) + if retcode != 200 { + return retcode + } + break + case "placementIntentHandler": + temp := &placementIntentHandler{} + temp.orchInstance = h + I = temp + _, retcode := h.getData(I) + if retcode != 200 { + return retcode + } + break + case "networkIntentHandler": + temp := &networkIntentHandler{} + temp.orchInstance = h + I = temp + _, retcode := h.getData(I) + if retcode != 200 { + return retcode + } + break + default: + fmt.Printf("%s\n", dataPoint) + } + } + return nil +} + +// This function partest he compositeapp tree read and populates the +// Dig tree +func (h *OrchestrationHandler) copyDigTree() { + dataRead := h.dataRead + h.DigpReturnJson = nil + + for compositeAppName, value := range dataRead.compositeAppMap { + for _, digValue := range dataRead.compositeAppMap[compositeAppName].DigMap { + Dig := DigsInProject{} + SourceDigMetadata := digValue.DigpData.Metadata + + // Copy the metadata + Dig.Metadata.Name = SourceDigMetadata.Name + Dig.Metadata.CompositeAppName = compositeAppName + Dig.Metadata.CompositeAppVersion = value.Metadata.Spec.Version + Dig.Metadata.Description = SourceDigMetadata.Description + Dig.Metadata.UserData1 = SourceDigMetadata.UserData1 + Dig.Metadata.UserData2 = SourceDigMetadata.UserData2 + + // Populate the Spec of dig + SourceDigSpec := digValue.DigpData.Spec + Dig.Spec.DigIntentsData = digValue.DigIntentsData.Intent + Dig.Spec.Profile = SourceDigSpec.Profile + Dig.Spec.Version = SourceDigSpec.Version + Dig.Spec.Lcloud = SourceDigSpec.Lcloud + Dig.Spec.OverrideValuesObj = SourceDigSpec.OverrideValuesObj + + // Pupolate the generic placement intents + SourceGpintMap := digValue.GpintMap + for t, gpintValue := range SourceGpintMap { + fmt.Printf("gpName value %s\n", t) + localGpint := DigsGpint{} + localGpint.Metadata = gpintValue.Gpint.Metadata + //localGpint.Spec.AppIntentArray = gpintValue.AppIntentArray + localGpint.Spec.AppIntentArray = make([]PlacementIntentExport, len(gpintValue.AppIntentArray)) + for k, _ := range gpintValue.AppIntentArray { + localGpint.Spec.AppIntentArray[k].Metadata = gpintValue.AppIntentArray[k].Metadata + localGpint.Spec.AppIntentArray[k].Spec.AppName = + gpintValue.AppIntentArray[k].Spec.AppName + localGpint.Spec.AppIntentArray[k].Spec.Intent.AllofCluster = + make([]AllofExport, len(gpintValue.AppIntentArray[k].Spec.Intent.AllofCluster)) + for i, _ := range gpintValue.AppIntentArray[k].Spec.Intent.AllofCluster { + localGpint.Spec.AppIntentArray[k].Spec.Intent.AllofCluster[i].ProviderName = + gpintValue.AppIntentArray[k].Spec.Intent.AllofCluster[i].ProviderName + localGpint.Spec.AppIntentArray[k].Spec.Intent.AllofCluster[i].ClusterName = + gpintValue.AppIntentArray[k].Spec.Intent.AllofCluster[i].ClusterName + } + } + + Dig.Spec.GpintArray = append(Dig.Spec.GpintArray, &localGpint) + } + // Populate the Nwint intents + SourceNwintMap := digValue.NwintMap + for _, nwintValue := range SourceNwintMap { + localNwint := DigsNwint{} + localNwint.Metadata = nwintValue.Nwint.Metadata + for _, wrkintValue := range nwintValue.WrkintMap { + localWrkint := WorkloadIntents{} + localWrkint.Metadata = wrkintValue.Wrkint.Metadata + localWrkint.Spec.Interfaces = wrkintValue.Interfaces + localNwint.Spec.WorkloadIntentsArray = append(localNwint.Spec.WorkloadIntentsArray, + &localWrkint) + } + Dig.Spec.NwintArray = append(Dig.Spec.NwintArray, &localNwint) + } + h.DigpReturnJson = append(h.DigpReturnJson, Dig) + } + } +} + +// GetSvc get the entrire tree under project/<composite app>/<version> +func (h *OrchestrationHandler) GetAllDigs(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + h.version = vars["version"] + h.projectName = vars["project-name"] + h.response.status = make(map[string]int) + h.response.payload = make(map[string][]byte) + dataPoints := []string{"projectHandler", "compAppHandler", + "digpHandler", + "placementIntentHandler", + "networkIntentHandler"} + + h.dataRead = &ProjectTree{} + h.treeFilter = nil + retcode := h.constructTree(dataPoints) + if retcode != nil { + if intval, ok := retcode.(int); ok { + w.WriteHeader(intval) + } else { + w.WriteHeader(500) + } + return + } + // copy dig tree + h.copyDigTree() + retval, _ := json.Marshal(h.DigpReturnJson) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + w.Write(retval) +} + +// GetSvc get the entrire tree under project/<composite app>/<version> +func (h *OrchestrationHandler) GetSvc(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + h.treeFilter = nil + h.compositeAppName = vars["composite-app-name"] + h.version = vars["version"] + h.projectName = vars["project-name"] + h.response.status = make(map[string]int) + h.response.payload = make(map[string][]byte) + + dataPoints := []string{"compAppHandler", "ProfileHandler", + "digpHandler", + "placementIntentHandler", + "networkIntentHandler"} + h.dataRead = &ProjectTree{} + retcode := h.constructTree(dataPoints) + if retcode != nil { + if intval, ok := retcode.(int); ok { + w.WriteHeader(intval) + } else { + w.WriteHeader(500) + } + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) +} + +// CreateApp exported function which creates the composite application +func (h *OrchestrationHandler) CreateDig(w http.ResponseWriter, r *http.Request) { + var jsonData deployDigData + + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(&jsonData) + if err != nil { + log.Printf("Failed to parse json") + log.Fatalln(err) + } + + h.DigData = jsonData + + if len(h.DigData.Spec.Apps) == 0 { + w.WriteHeader(400) + w.Write([]byte("Bad request, no app metadata\n")) + return + } + + h.client = http.Client{} + + // These maps will get populated by the return status and respones of each V2 API + // that is called during the execution of the workflow. + h.response.payload = make(map[string][]byte) + h.response.status = make(map[string]int) + + // 4. Create DIG + h.digpIntents = make(map[string]string) + h.nwCtlIntents = make(map[string]string) + igHandler := &digpHandler{} + igHandler.orchInstance = h + igpStatus := createDInents(igHandler) + if igpStatus != nil { + if intval, ok := igpStatus.(int); ok { + w.WriteHeader(intval) + } else { + w.WriteHeader(500) + } + w.Write(h.response.payload[h.compositeAppName+"_digp"]) + return + } + + // 3. Create intents + intentHandler := &placementIntentHandler{} + intentHandler.orchInstance = h + intentStatus := addPlacementIntent(intentHandler) + if intentStatus != nil { + if intval, ok := intentStatus.(int); ok { + w.WriteHeader(intval) + } else { + w.WriteHeader(500) + } + w.Write(h.response.payload[h.compositeAppName+"_gpint"]) + return + } + + // If the metadata contains network interface request then call the + // network intent related part of the workflow. + if len(h.DigData.Spec.Apps[0].Clusters[0].SelectedClusters[0].Interfaces) != 0 { + nwHandler := &networkIntentHandler{} + nwHandler.orchInstance = h + nwIntentStatus := addNetworkIntent(nwHandler) + if nwIntentStatus != nil { + if intval, ok := nwIntentStatus.(int); ok { + w.WriteHeader(intval) + } else { + w.WriteHeader(500) + } + w.Write(h.response.payload[h.compositeAppName+"_nwctlint"]) + return + } + } + + w.WriteHeader(201) + w.Write(h.response.payload[h.DigData.Name]) +} + +func (h *OrchestrationHandler) CreateApp(w http.ResponseWriter, r *http.Request) { + var jsonData deployServiceData + + err := r.ParseMultipartForm(16777216) + if err != nil { + log.Fatalln(err) + } + + // Populate the multipart.FileHeader MAP. The key will be the + // filename itself. The metadata Map will be keyed on the application + // name. The metadata has a field file name, so later we can parse the metadata + // Map, and fetch the file headers from this file Map with keys as the filename. + h.file = make(map[string]*multipart.FileHeader) + for _, v := range r.MultipartForm.File { + fh := v[0] + h.file[fh.Filename] = fh + } + + jsn := ([]byte(r.FormValue("servicePayload"))) + err = json.Unmarshal(jsn, &jsonData) + if err != nil { + log.Printf("Failed to parse json") + log.Fatalln(err) + } + + h.compositeAppName = jsonData.Name + h.compositeAppDesc = jsonData.Description + h.projectName = jsonData.Spec.ProjectName + h.meta = jsonData.Spec.Apps + + // Sanity check. For each metadata there should be a + // corresponding file in the multipart request. If it + // not found we fail this API call. + for i := range h.meta { + switch { + case h.file[h.meta[i].Metadata.FileName] == nil: + t := fmt.Sprintf("File %s not in request", h.meta[i].Metadata.FileName) + w.WriteHeader(400) + w.Write([]byte(t)) + fmt.Printf("app file not found\n") + return + case h.file[h.meta[i].ProfileMetadata.FileName] == nil: + t := fmt.Sprintf("File %s not in request", h.meta[i].ProfileMetadata.FileName) + w.WriteHeader(400) + w.Write([]byte(t)) + fmt.Printf("profile file not found\n") + return + default: + fmt.Println("Good request") + } + } + + if len(h.meta) == 0 { + w.WriteHeader(400) + w.Write([]byte("Bad request, no app metadata\n")) + return + } + + h.client = http.Client{} + + // These maps will get populated by the return status and respones of each V2 API + // that is called during the execution of the workflow. + h.response.payload = make(map[string][]byte) + h.response.status = make(map[string]int) + + // 1. create the composite application. the compAppHandler implements the + // orchWorkflow interface. + appHandler := &compAppHandler{} + appHandler.orchInstance = h + appStatus := createCompositeapp(appHandler) + if appStatus != nil { + if intval, ok := appStatus.(int); ok { + w.WriteHeader(intval) + } else { + w.WriteHeader(500) + } + w.Write(h.response.payload[h.compositeAppName]) + return + } + + // 2. create the composite application profiles + profileHandler := &ProfileHandler{} + profileHandler.orchInstance = h + profileStatus := createProfile(profileHandler) + if profileStatus != nil { + if intval, ok := profileStatus.(int); ok { + w.WriteHeader(intval) + } else { + w.WriteHeader(500) + } + w.Write(h.response.payload[h.compositeAppName+"_profile"]) + return + } + + w.WriteHeader(201) + w.Write(h.response.payload[h.compositeAppName]) +} + +func (h *OrchestrationHandler) createCluster(filename string, fh *multipart.FileHeader, clusterName string, + jsonData ClusterMetadata) interface{} { + url := "http://" + h.MiddleendConf.Clm + "/v2/cluster-providers/" + clusterName + "/clusters" + + jsonLoad, _ := json.Marshal(jsonData) + + status, err := h.apiPostMultipart(jsonLoad, fh, url, clusterName, filename) + if err != nil { + return err + } + if status != 201 { + return status + } + fmt.Printf("cluster creation %s status %s\n", clusterName, status) + return nil +} + +func (h *OrchestrationHandler) CheckConnection(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + + parse_err := r.ParseMultipartForm(16777216) + if parse_err != nil { + fmt.Printf("multipart error: %s", parse_err.Error()) + w.WriteHeader(500) + return + } + + var fh *multipart.FileHeader + for _, v := range r.MultipartForm.File { + fh = v[0] + } + file, err := fh.Open() + if err != nil { + fmt.Printf("Failed to open the file: %s", err.Error()) + w.WriteHeader(500) + return + } + defer file.Close() + + // Read the kconfig + kubeconfig, _ := ioutil.ReadAll(file) + + jsonData := ClusterMetadata{} + jsn := ([]byte(r.FormValue("metadata"))) + err = json.Unmarshal(jsn, &jsonData) + if err != nil { + fmt.Printf("Failed to parse json") + w.WriteHeader(500) + return + } + fmt.Printf("metadata %+v\n", jsonData) + + // RESTConfigFromKubeConfig is a convenience method to give back + // a restconfig from your kubeconfig bytes. + config, err := clientcmd.RESTConfigFromKubeConfig(kubeconfig) + if err != nil { + fmt.Printf("Error while reading the kubeconfig: %s", err.Error()) + w.WriteHeader(500) + return + } + + // create the clientset + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + fmt.Printf("Failed to create clientset: %s", err.Error()) + w.WriteHeader(500) + return + } + + _, err = clientset.CoreV1().Pods("").List(context.TODO(), metav1.ListOptions{}) + if err != nil { + fmt.Printf("Failed to establish the connection: %s", err.Error()) + w.WriteHeader(403) + w.Write([]byte("Cluster connectivity failed\n")) + return + } + + fmt.Printf("Successfully established the connection\n") + h.client = http.Client{} + h.response.status = make(map[string]int) + h.response.payload = make(map[string][]byte) + + status := h.createCluster(fh.Filename, fh, vars["cluster-provider-name"], jsonData) + if status != nil { + w.WriteHeader(500) + return + } + + w.WriteHeader(200) + w.Write(h.response.payload[vars["cluster-provider-name"]]) + return +} diff --git a/src/tools/emcoui/middle_end/app/compositeapp.go b/src/tools/emcoui/middle_end/app/compositeapp.go new file mode 100644 index 00000000..daae5b00 --- /dev/null +++ b/src/tools/emcoui/middle_end/app/compositeapp.go @@ -0,0 +1,247 @@ +/* +======================================================================= +Copyright (c) 2017-2020 Aarna Networks, Inc. +All rights reserved. +====================================================================== +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 app + +import ( + "encoding/json" + "fmt" +) + +// CompositeApp application structure +type CompositeApp struct { + Metadata apiMetaData `json:"metadata"` + Spec compositeAppSpec `json:"spec"` +} + +type compositeAppSpec struct { + Version string `json:"version"` +} + +// compAppHandler , This implements the orchworkflow interface +type compAppHandler struct { + orchURL string + orchInstance *OrchestrationHandler +} + +// CompositeAppKey is the mongo key to fetch apps in a composite app +type CompositeAppKey struct { + Cname string `json:"compositeapp"` + Project string `json:"project"` + Cversion string `json:"compositeappversion"` + App interface{} `json:"app"` +} + +func (h *compAppHandler) getObject() (interface{}, interface{}) { + orch := h.orchInstance + respcode := 200 + dataRead := h.orchInstance.dataRead + for _, compositeAppValue := range dataRead.compositeAppMap { + compositeAppMetadata := compositeAppValue.Metadata.Metadata + compositeAppSpec := compositeAppValue.Metadata.Spec + h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" + + orch.projectName + "/composite-apps/" + compositeAppMetadata.Name + + "/" + compositeAppSpec.Version + "/apps" + + respcode, respdata, err := orch.apiGet(h.orchURL, orch.compositeAppName+"_getapps") + if err != nil { + return nil, 500 + } + if respcode != 200 { + return nil, respcode + } + fmt.Printf("Get app status %s\n", respcode) + compositeAppValue.AppsDataArray = make(map[string]*AppsData, len(respdata)) + var appList []CompositeApp + json.Unmarshal(respdata, &appList) + for _, value := range appList { + var appsDataInstance AppsData + appName := value.Metadata.Name + appsDataInstance.App = value + compositeAppValue.AppsDataArray[appName] = &appsDataInstance + } + } + return nil, respcode +} + +func (h *compAppHandler) getAnchor() (interface{}, interface{}) { + orch := h.orchInstance + dataRead := h.orchInstance.dataRead + respcode := 200 + for _, compositeAppValue := range dataRead.compositeAppMap { + compositeAppMetadata := compositeAppValue.Metadata.Metadata + compositeAppSpec := compositeAppValue.Metadata.Spec + h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" + + orch.projectName + "/composite-apps/" + compositeAppMetadata.Name + + "/" + compositeAppSpec.Version + + respcode, _, err := orch.apiGet(h.orchURL, orch.compositeAppName+"_getcompositeapp") + if err != nil { + return nil, 500 + } + if respcode != 200 { + return nil, respcode + } + fmt.Printf("Get composite App %s\n", respcode) + //json.Unmarshal(respdata, &dataRead.CompositeApp) + } + return nil, respcode +} + +func (h *compAppHandler) deleteObject() interface{} { + orch := h.orchInstance + dataRead := h.orchInstance.dataRead + for _, compositeAppValue := range dataRead.compositeAppMap { + compositeAppMetadata := compositeAppValue.Metadata.Metadata + compositeAppSpec := compositeAppValue.Metadata.Spec + h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" + + orch.projectName + "/composite-apps/" + compositeAppMetadata.Name + + "/" + compositeAppSpec.Version + appList := compositeAppValue.AppsDataArray + for _, value := range appList { + url := h.orchURL + "/apps/" + value.App.Metadata.Name + fmt.Printf("Delete app %s\n", url) + resp, err := orch.apiDel(url, compositeAppMetadata.Name+"_delapp") + if err != nil { + return err + } + if resp != 204 { + return resp + } + fmt.Printf("Delete app status %s\n", resp) + } + } + return nil +} + +func (h *compAppHandler) deleteAnchor() interface{} { + orch := h.orchInstance + dataRead := h.orchInstance.dataRead + for _, compositeAppValue := range dataRead.compositeAppMap { + compositeAppMetadata := compositeAppValue.Metadata.Metadata + compositeAppSpec := compositeAppValue.Metadata.Spec + h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" + + orch.projectName + "/composite-apps/" + compositeAppMetadata.Name + + "/" + compositeAppSpec.Version + fmt.Printf("Delete composite app %s\n", h.orchURL) + resp, err := orch.apiDel(h.orchURL, compositeAppMetadata.Name+"_delcompapp") + if err != nil { + return err + } + if resp != 204 { + return resp + } + fmt.Printf("Delete compapp status %s\n", resp) + } + return nil +} + +// CreateAnchor creates the anchor point for composite applications, +// profiles, intents etc. For example Anchor for the composite application +// will create the composite application resource in the the DB, and all apps +// will get created and uploaded under this anchor point. +func (h *compAppHandler) createAnchor() interface{} { + orch := h.orchInstance + + compAppCreate := CompositeApp{ + Metadata: apiMetaData{ + Name: orch.compositeAppName, + Description: orch.compositeAppDesc, + UserData1: "data 1", + UserData2: "data 2"}, + Spec: compositeAppSpec{ + Version: "v1"}, + } + + jsonLoad, _ := json.Marshal(compAppCreate) + tem := CompositeApp{} + json.Unmarshal(jsonLoad, &tem) + h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" + + orch.projectName + "/composite-apps" + resp, err := orch.apiPost(jsonLoad, h.orchURL, orch.compositeAppName) + if err != nil { + return err + } + if resp != 201 { + return resp + } + orch.version = "v1" + fmt.Printf("compAppHandler resp %s\n", resp) + + return nil +} + +func (h *compAppHandler) createObject() interface{} { + orch := h.orchInstance + for i := range orch.meta { + fileName := orch.meta[i].Metadata.FileName + appName := orch.meta[i].Metadata.Name + appDesc := orch.meta[i].Metadata.Description + + // Upload the application helm chart + fh := orch.file[fileName] + compAppAdd := CompositeApp{ + Metadata: apiMetaData{ + Name: appName, + Description: appDesc, + UserData1: "data 1", + UserData2: "data2"}, + } + url := h.orchURL + "/" + orch.compositeAppName + "/" + orch.version + "/apps" + + jsonLoad, _ := json.Marshal(compAppAdd) + + status, err := orch.apiPostMultipart(jsonLoad, fh, url, appName, fileName) + if err != nil { + return err + } + if status != 201 { + return status + } + fmt.Printf("Composite app %s createObject status %s\n", appName, status) + } + + return nil +} + +func createCompositeapp(I orchWorkflow) interface{} { + // 1. Create the Anchor point + err := I.createAnchor() + if err != nil { + return err + } + // 2. Create the Objects + err = I.createObject() + if err != nil { + return err + } + return nil +} + +func delCompositeapp(I orchWorkflow) interface{} { + // 1. Delete the object + err := I.deleteObject() + if err != nil { + return err + } + // 2. Delete the Anchor + err = I.deleteAnchor() + if err != nil { + return err + } + return nil +} diff --git a/src/tools/emcoui/middle_end/app/digp.go b/src/tools/emcoui/middle_end/app/digp.go new file mode 100644 index 00000000..b4c83e15 --- /dev/null +++ b/src/tools/emcoui/middle_end/app/digp.go @@ -0,0 +1,322 @@ +/* +======================================================================= +Copyright (c) 2017-2020 Aarna Networks, Inc. +All rights reserved. +====================================================================== +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 app + +import ( + "encoding/json" + "fmt" + "log" +) + +type DeploymentIGP struct { + Metadata apiMetaData `json:"metadata"` + Spec DigpSpec `json:"spec"` +} + +type DigpSpec struct { + Profile string `json:"profile"` + Version string `json:"version"` + Lcloud string `json:"logical-cloud"` + OverrideValuesObj []OverrideValues `json:"override-values"` +} + +// OverrideValues ... +type OverrideValues struct { + AppName string `json:"app-name"` + ValuesObj map[string]string `json:"values"` +} + +type IgpIntents struct { + Metadata apiMetaData `json:"metadata"` + Spec AppIntents `json:"spec"` +} + +type AppIntents struct { + Intent map[string]string `json:"intent"` +} + +type DigpIntents struct { + Intent []DigDeployedIntents `json:"intent"` +} +type DigDeployedIntents struct { + GenericPlacementIntent string `json:"genericPlacementIntent"` + Ovnaction string `json:"ovnaction"` +} + +// digpHandler implements the orchworkflow interface +type digpHandler struct { + orchURL string + orchInstance *OrchestrationHandler +} + +func (h *digpHandler) getAnchor() (interface{}, interface{}) { + orch := h.orchInstance + dataRead := h.orchInstance.dataRead + retcode := 200 + for _, compositeAppValue := range dataRead.compositeAppMap { + compositeAppMetadata := compositeAppValue.Metadata.Metadata + compositeAppSpec := compositeAppValue.Metadata.Spec + var digpList []DeploymentIGP + // This for the cases where the dig name is in the URL + if orch.treeFilter != nil && orch.treeFilter.digName != ""{ + temp:=DeploymentIGP{} + h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" + + orch.projectName + "/composite-apps/" + compositeAppMetadata.Name + + "/" + compositeAppSpec.Version + + "/deployment-intent-groups/" + orch.treeFilter.digName + retcode, retval, err := orch.apiGet(h.orchURL, orch.compositeAppName+"_digp") + fmt.Printf("Get Digp in composite app %s status %d\n", compositeAppMetadata.Name, retcode) + if err != nil { + fmt.Printf("Failed to read digp") + return nil, 500 + } + if retcode != 200 { + fmt.Printf("Failed to read digp") + return nil, retcode + } + json.Unmarshal(retval, &temp) + digpList = append(digpList, temp) + } else { + h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" + + orch.projectName + "/composite-apps/" + compositeAppMetadata.Name + + "/" + compositeAppSpec.Version + + "/deployment-intent-groups" + retcode, retval, err := orch.apiGet(h.orchURL, orch.compositeAppName+"_digp") + fmt.Printf("Get Digp in composite app %s status %d\n", compositeAppMetadata.Name, retcode) + if err != nil { + fmt.Printf("Failed to read digp") + return nil, 500 + } + if retcode != 200 { + fmt.Printf("Failed to read digp") + return nil, retcode + } + json.Unmarshal(retval, &digpList) + } + + compositeAppValue.DigMap = make(map[string]*DigReadData, len(digpList)) + for _, digpValue := range digpList { + var Dig DigReadData + Dig.DigpData = digpValue + compositeAppValue.DigMap[digpValue.Metadata.Name] = &Dig + } + } + return nil, retcode +} + +func (h *digpHandler) getObject() (interface{}, interface{}) { + orch := h.orchInstance + dataRead := h.orchInstance.dataRead + for _, compositeAppValue := range dataRead.compositeAppMap { + compositeAppMetadata := compositeAppValue.Metadata.Metadata + compositeAppSpec := compositeAppValue.Metadata.Spec + h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" + + orch.projectName + "/composite-apps/" + compositeAppMetadata.Name + + "/" + compositeAppSpec.Version + + "/deployment-intent-groups/" + digpList := compositeAppValue.DigMap + for digName, digValue := range digpList { + url := h.orchURL + digName + "/intents" + retcode, retval, err := orch.apiGet(url, compositeAppMetadata.Name+"_digpIntents") + fmt.Printf("Get Dig int composite app %s Dig %s status %d \n", orch.compositeAppName, + digName, retcode) + if err != nil { + fmt.Printf("Failed to read digp intents") + return nil, 500 + } + if retcode != 200 { + fmt.Printf("Failed to read digp intents") + return nil, retcode + + } + err = json.Unmarshal(retval, &digValue.DigIntentsData) + if err != nil { + fmt.Printf("Failed to read intents %s\n", err) + } + } + } + return nil, 200 +} + +func (h *digpHandler) deleteObject() interface{} { + orch := h.orchInstance + dataRead := h.orchInstance.dataRead + for _, compositeAppValue := range dataRead.compositeAppMap { + compositeAppMetadata := compositeAppValue.Metadata.Metadata + compositeAppSpec := compositeAppValue.Metadata.Spec + digpList := compositeAppValue.DigMap + h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" + + orch.projectName + "/composite-apps/" + compositeAppMetadata.Name + + "/" + compositeAppSpec.Version + + "/deployment-intent-groups/" + + for digName, _ := range digpList { + url := h.orchURL + digName + "/intents/PlacementIntent" + fmt.Printf("dlete intents %s\n", url) + resp, err := orch.apiDel(url, orch.compositeAppName+"_deldigintents") + if err != nil { + return err + } + if resp != 204 { + return resp + } + fmt.Printf("Delete dig intents resp %s\n", resp) + } + } + return nil +} + +func (h *digpHandler) deleteAnchor() interface{} { + orch := h.orchInstance + dataRead := h.orchInstance.dataRead + for _, compositeAppValue := range dataRead.compositeAppMap { + compositeAppMetadata := compositeAppValue.Metadata.Metadata + compositeAppSpec := compositeAppValue.Metadata.Spec + digpList := compositeAppValue.DigMap + h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" + + orch.projectName + "/composite-apps/" + compositeAppMetadata.Name + + "/" + compositeAppSpec.Version + + "/deployment-intent-groups/" + + // loop through all the intents in the dig + for digName, _ := range digpList { + url := h.orchURL + digName + turl := h.orchURL + digName + "/terminate" + fmt.Printf("delete intents %s\n", url) + jsonLoad, _ := json.Marshal("{}") + resp, err := orch.apiPost(jsonLoad, turl, orch.compositeAppName+"_terminatedig") + //Not checking the status of terminate FIXME + resp, err = orch.apiDel(url, orch.compositeAppName+"_deldig") + if err != nil { + return err + } + if resp != 204 { + return resp + } + fmt.Printf("Delete dig resp %s\n", resp) + } + } + return nil +} + +func (h *digpHandler) createAnchor() interface{} { + digData := h.orchInstance.DigData + orch := h.orchInstance + + digp := DeploymentIGP{ + Metadata: apiMetaData{ + Name: digData.Name, + Description: digData.Description, + UserData1: "data 1", + UserData2: "data2"}, + Spec: DigpSpec{ + Profile: digData.CompositeProfile, + Version: digData.DigVersion, + Lcloud: "unused_logical_cloud", + OverrideValuesObj: make([]OverrideValues, len(digData.Spec.Apps)), + }, + } + overrideVals := digp.Spec.OverrideValuesObj + for i, value := range digData.Spec.Apps { + overrideVals[i].ValuesObj = make(map[string]string) + overrideVals[i].AppName = value.Metadata.Name + overrideVals[i].ValuesObj["Values.global.dcaeCollectorIp"] = "1.8.0" + } + + jsonLoad, _ := json.Marshal(digp) + + // POST the generic placement intent + h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" + digData.Spec.ProjectName + + "/composite-apps/" + digData.CompositeAppName + "/" + digData.CompositeAppVersion + + "/deployment-intent-groups" + resp, err := orch.apiPost(jsonLoad, h.orchURL, digData.Name) + if err != nil { + return err + } + if resp != 201 { + return resp + } + orch.digpIntents["generic-placement-intent"] = digData.CompositeAppName + "_gpint" + orch.nwCtlIntents["network-controller-intent"] = digData.CompositeAppName + "_nwctlint" + fmt.Printf("Deloyment intent group resp %s\n", resp) + + return nil +} + +func (h *digpHandler) createObject() interface{} { + digData := h.orchInstance.DigData + orch := h.orchInstance + intentName := "PlacementIntent" + igp := IgpIntents{ + Metadata: apiMetaData{ + Name: intentName, + Description: "NA", + UserData1: "data 1", + UserData2: "data2"}, + } + if len(digData.Spec.Apps[0].Clusters[0].SelectedClusters[0].Interfaces) != 0 { + igp.Spec.Intent = make(map[string]string) + igp.Spec.Intent["genericPlacementIntent"] = orch.digpIntents["generic-placement-intent"] + igp.Spec.Intent["ovnaction"] = orch.nwCtlIntents["network-controller-intent"] + } else { + igp.Spec.Intent = make(map[string]string) + igp.Spec.Intent["genericPlacementIntent"] = orch.digpIntents["generic-placement-intent"] + } + + url := h.orchURL + "/" + digData.Name + "/intents" + jsonLoad, _ := json.Marshal(igp) + status, err := orch.apiPost(jsonLoad, url, intentName) + fmt.Printf("DIG name req %s", string(jsonLoad)) + if err != nil { + log.Fatalln(err) + } + if status != 201 { + return status + } + fmt.Printf("Placement intent %s status %s %s\n", intentName, status, url) + + return nil +} + +func createDInents(I orchWorkflow) interface{} { + // 1. Create the Anchor point + err := I.createAnchor() + if err != nil { + return err + } + // 2. Create the Objects + err = I.createObject() + if err != nil { + return err + } + return nil +} + +func delDigp(I orchWorkflow) interface{} { + // 1. Delete the object + err := I.deleteObject() + if err != nil { + return err + } + // 2. Delete the Anchor + err = I.deleteAnchor() + if err != nil { + return err + } + return nil +} diff --git a/src/tools/emcoui/middle_end/app/intents.go b/src/tools/emcoui/middle_end/app/intents.go new file mode 100644 index 00000000..992f7b66 --- /dev/null +++ b/src/tools/emcoui/middle_end/app/intents.go @@ -0,0 +1,664 @@ +/* +======================================================================= +Copyright (c) 2017-2020 Aarna Networks, Inc. +All rights reserved. +====================================================================== +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 app + +import ( + "encoding/json" + "fmt" + "log" + "strconv" +) + +type GenericPlacementIntent struct { + Metadata apiMetaData `json:"metadata"` +} + +type PlacementIntent struct { + Metadata apiMetaData `json:"metadata"` + Spec AppPlacementIntentSpec `json:"spec"` +} +type PlacementIntentExport struct { + Metadata apiMetaData `json:"metadata"` + Spec AppPlacementIntentSpecExport `json:"spec"` +} + +// appPlacementIntentSpec is the spec for per app intent +type AppPlacementIntentSpec struct { + AppName string `json:"app-name"` + Intent arrayIntent `json:"intent"` +} +type arrayIntent struct { + AllofCluster []Allof `json:"allof"` +} +type Allof struct { + ProviderName string `json:"provider-name"` + ClusterName string `json:"cluster-name"` +} +type AppPlacementIntentSpecExport struct { + AppName string `json:"appName"` + Intent arrayIntentExport `json:"intent"` +} +type arrayIntentExport struct { + AllofCluster []AllofExport `json:"allof"` +} +type AllofExport struct { + ProviderName string `json:"providerName"` + ClusterName string `json:"clusterName"` +} + +// plamcentIntentHandler implements the orchworkflow interface +type placementIntentHandler struct { + orchURL string + orchInstance *OrchestrationHandler +} + +type NetworkCtlIntent struct { + Metadata apiMetaData `json:"metadata"` +} + +type NetworkWlIntent struct { + Metadata apiMetaData `json:"metadata"` + Spec WorkloadIntentSpec `json:"spec"` +} + +type WorkloadIntentSpec struct { + AppName string `json:"application-name"` + Resource string `json:"workload-resource"` + Type string `json:"type"` +} +type WorkloadIntentSpecExport struct { + AppName string `json:"applicationName"` + Resource string `json:"workloadResource"` + Type string `json:"type"` +} + +type NwInterface struct { + Metadata apiMetaData `json:"metadata"` + Spec InterfaceSpec `json:"spec"` +} + +type InterfaceSpec struct { + Interface string `json:"interface"` + Name string `json:"name"` + DefaultGateway string `json:"defaultGateway"` + IPAddress string `json:"ipAddress"` + MacAddress string `json:"macAddress"` +} + +// networkIntentHandler implements the orchworkflow interface +type networkIntentHandler struct { + ovnURL string + orchInstance *OrchestrationHandler +} + +func (h *placementIntentHandler) getObject() (interface{}, interface{}) { + orch := h.orchInstance + retcode := 200 + dataRead := h.orchInstance.dataRead + for _, compositeAppValue := range dataRead.compositeAppMap { + compositeAppMetadata := compositeAppValue.Metadata.Metadata + compositeAppSpec := compositeAppValue.Metadata.Spec + Dig := compositeAppValue.DigMap + Apps := compositeAppValue.AppsDataArray + for digName, digValue := range Dig { + h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" + + orch.projectName + "/composite-apps/" + compositeAppMetadata.Name + + "/" + compositeAppSpec.Version + + "/deployment-intent-groups/" + digName + "/generic-placement-intents" + for gpintName, gpintValue := range digValue.GpintMap { + for appName, _ := range Apps { + var appPint PlacementIntent + url := h.orchURL + "/" + gpintName + "/app-intents/" + appName + "_pint" + retcode, retval, err := orch.apiGet(url, compositeAppMetadata.Name+"_getappPint") + fmt.Printf("Get Gpint App intent in Composite app %s dig %s Gpint %s status %s\n", + orch.compositeAppName, digName, gpintName, retcode) + if err != nil { + fmt.Printf("Failed to read app pint\n") + return nil, 500 + } + if retcode != 200 { + fmt.Printf("Failed to read app pint\n") + return nil, 200 + } + err = json.Unmarshal(retval, &appPint) + if err != nil { + fmt.Printf("Failed to unmarshal json %s\n", err) + return nil, 500 + } + gpintValue.AppIntentArray = append(gpintValue.AppIntentArray, appPint) + } + } + } + } + return nil, retcode +} + +func (h *placementIntentHandler) getAnchor() (interface{}, interface{}) { + orch := h.orchInstance + retcode := 200 + dataRead := h.orchInstance.dataRead + for _, compositeAppValue := range dataRead.compositeAppMap { + compositeAppMetadata := compositeAppValue.Metadata.Metadata + compositeAppSpec := compositeAppValue.Metadata.Spec + Dig := compositeAppValue.DigMap + for digName, digValue := range Dig { + var gpintList []GenericPlacementIntent + h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" + + orch.projectName + "/composite-apps/" + compositeAppMetadata.Name + + "/" + compositeAppSpec.Version + + "/deployment-intent-groups/" + digName + "/generic-placement-intents" + retcode, retval, err := orch.apiGet(h.orchURL, compositeAppMetadata.Name+"_getgpint") + fmt.Printf("Get Gpint in Composite app %s dig %s status %s\n", orch.compositeAppName, + digName, retcode) + if err != nil { + fmt.Printf("Failed to read gpint\n") + return nil, 500 + } + if retcode != 200 { + fmt.Printf("Failed to read gpint\n") + return nil, retcode + } + json.Unmarshal(retval, &gpintList) + digValue.GpintMap = make(map[string]*GpintData, len(gpintList)) + for _, value := range gpintList { + var GpintDataInstance GpintData + GpintDataInstance.Gpint = value + digValue.GpintMap[value.Metadata.Name] = &GpintDataInstance + } + } + } + return nil, retcode +} + +func (h *placementIntentHandler) deleteObject() interface{} { + orch := h.orchInstance + dataRead := h.orchInstance.dataRead + for _, compositeAppValue := range dataRead.compositeAppMap { + compositeAppMetadata := compositeAppValue.Metadata.Metadata + compositeAppSpec := compositeAppValue.Metadata.Spec + Dig := compositeAppValue.DigMap + Apps := compositeAppValue.AppsDataArray + + // loop through all app intens in the gpint + for digName, digValue := range Dig { + h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" + + orch.projectName + "/composite-apps/" + compositeAppMetadata.Name + + "/" + compositeAppSpec.Version + + "/deployment-intent-groups/" + digName + "/generic-placement-intents/" + for gpintName, _ := range digValue.GpintMap { + for appName, _ := range Apps { + url := h.orchURL + gpintName + + "/app-intents/" + appName + "_pint" // FIXME when query API works, change this API call to + // query based on app name. + fmt.Printf("Delete gping app intents %s\n", url) + resp, err := orch.apiDel(url, orch.compositeAppName+"_delgpintintents") + if err != nil { + return err + } + if resp != 204 { + return resp + } + fmt.Printf("Delete gpint intents resp %s\n", resp) + } + } + } + } + return nil +} + +func (h placementIntentHandler) deleteAnchor() interface{} { + orch := h.orchInstance + dataRead := h.orchInstance.dataRead + for _, compositeAppValue := range dataRead.compositeAppMap { + compositeAppMetadata := compositeAppValue.Metadata.Metadata + compositeAppSpec := compositeAppValue.Metadata.Spec + Dig := compositeAppValue.DigMap + + // loop through all app intens in the gpint + for digName, digValue := range Dig { + for gpintName, _ := range digValue.GpintMap { + h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" + + orch.projectName + "/composite-apps/" + compositeAppMetadata.Name + + "/" + compositeAppSpec.Version + + "/deployment-intent-groups/" + digName + "/generic-placement-intents/" + + gpintName + fmt.Printf("Delete gpint %s\n", h.orchURL) + resp, err := orch.apiDel(h.orchURL, compositeAppMetadata.Name+"_delgpints") + if err != nil { + return err + } + if resp != 204 { + return resp + } + fmt.Printf("Delete gpint resp %s\n", resp) + } + } + } + return nil +} + +func (h *placementIntentHandler) createAnchor() interface{} { + orch := h.orchInstance + intentData := h.orchInstance.DigData + + gpi := GenericPlacementIntent{ + Metadata: apiMetaData{ + Name: intentData.CompositeAppName + "_gpint", + Description: "Generic placement intent created from middleend", + UserData1: "data 1", + UserData2: "data2"}, + } + + jsonLoad, _ := json.Marshal(gpi) + // POST the generic placement intent + h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" + intentData.Spec.ProjectName + + "/composite-apps/" + intentData.CompositeAppName + "/" + intentData.CompositeAppVersion + + "/deployment-intent-groups/" + intentData.Name + url := h.orchURL + "/generic-placement-intents" + resp, err := orch.apiPost(jsonLoad, url, orch.digpIntents["generic-placement-intent"]) + if err != nil { + return err + } + if resp != 201 { + return resp + } + fmt.Printf("Generic placement intent resp %s\n", resp) + + return nil +} + +func (h *placementIntentHandler) createObject() interface{} { + orch := h.orchInstance + intentData := h.orchInstance.DigData + + for _, value := range intentData.Spec.Apps { + appName := value.Metadata.Name + intentName := appName + "_pint" + genericAppIntentName := intentData.CompositeAppName + "_gpint" + providerName := value.Clusters[0].Provider + clusterName := value.Clusters[0].SelectedClusters[0].Name + + pint := PlacementIntent{ + Metadata: apiMetaData{ + Name: intentName, + Description: "NA", + UserData1: "data 1", + UserData2: "data2"}, + Spec: AppPlacementIntentSpec{ + AppName: appName, + Intent: arrayIntent{ + AllofCluster: []Allof{ // FIXME: the logic requires to handle allof/anyof and multi cluster. + Allof{ + ProviderName: providerName, + ClusterName: clusterName}, + }, + }, + }, + } + + url := h.orchURL + "/generic-placement-intents/" + genericAppIntentName + "/app-intents" + jsonLoad, _ := json.Marshal(pint) + status, err := orch.apiPost(jsonLoad, url, intentName) + if err != nil { + log.Fatalln(err) + } + if status != 201 { + return status + } + fmt.Printf("Placement intent %s status %s %s\n", intentName, status, url) + } + + return nil +} + +func addPlacementIntent(I orchWorkflow) interface{} { + // 1. Create the Anchor point + err := I.createAnchor() + if err != nil { + return err + } + // 2. Create the Objects + err = I.createObject() + if err != nil { + return err + } + return nil +} + +func delGpint(I orchWorkflow) interface{} { + // 1. Create the Anchor point + err := I.deleteObject() + if err != nil { + return err + } + // 2. Create the Objects + err = I.deleteAnchor() + if err != nil { + return err + } + return nil +} + +func (h *networkIntentHandler) createAnchor() interface{} { + orch := h.orchInstance + intentData := h.orchInstance.DigData + + nwIntent := NetworkCtlIntent{ + Metadata: apiMetaData{ + Name: intentData.CompositeAppName + "_nwctlint", + Description: "Network Controller created from middleend", + UserData1: "data 1", + UserData2: "data2"}, + } + jsonLoad, _ := json.Marshal(nwIntent) + // POST the network controller intent + h.ovnURL = "http://" + orch.MiddleendConf.OvnService + "/v2/projects/" + intentData.Spec.ProjectName + + "/composite-apps/" + intentData.CompositeAppName + "/" + intentData.CompositeAppVersion + + "/deployment-intent-groups/" + intentData.Name + url := h.ovnURL + "/network-controller-intent" + resp, err := orch.apiPost(jsonLoad, url, orch.nwCtlIntents["network-controller-intent"]) + if err != nil { + return err + } + if resp != 201 { + return resp + } + fmt.Printf("Network contoller intent resp %s\n", resp) + + return nil +} + +func (h *networkIntentHandler) createObject() interface{} { + orch := h.orchInstance + intentData := h.orchInstance.DigData + + for _, value := range intentData.Spec.Apps { + + appName := value.Metadata.Name + intentName := value.Metadata.Name + "_wnwlint" + genericAppIntentName := intentData.CompositeAppName + "_nwctlint" + + wlIntent := NetworkWlIntent{ + Metadata: apiMetaData{ + Name: intentName, + Description: "NA", + UserData1: "data 1", + UserData2: "data2"}, + Spec: WorkloadIntentSpec{ + AppName: appName, + Resource: appName, + Type: "deployment", + }, + } + + url := h.ovnURL + "/network-controller-intent/" + genericAppIntentName + "/workload-intents" + jsonLoad, _ := json.Marshal(wlIntent) + status, err := orch.apiPost(jsonLoad, url, intentName) + if err != nil { + log.Fatalln(err) + } + if status != 201 { + return status + } + fmt.Printf("Workload intent %s status %s %s\n", intentName, status, url) + } + + // Add interfaces for to each application + for _, value := range intentData.Spec.Apps { + interfaces := value.Clusters[0].SelectedClusters[0].Interfaces + for j := range interfaces { + interfaceNum := strconv.Itoa(j) + interfaceName := value.Metadata.Name + "_interface" + interfaceNum + genericAppIntentName := intentData.CompositeAppName + "_nwctlint" + workloadIntent := value.Metadata.Name + "_wnwlint" + + iface := NwInterface{ + Metadata: apiMetaData{ + Name: interfaceName, + Description: "NA", + UserData1: "data 1", + UserData2: "data2"}, + Spec: InterfaceSpec{ + Interface: "eth" + interfaceNum, + Name: interfaces[j].NetworkName, + DefaultGateway: "false", + IPAddress: interfaces[j].IP, + }, + } + + url := h.ovnURL + "/network-controller-intent" + "/" + genericAppIntentName + + "/workload-intents/" + workloadIntent + "/interfaces" + jsonLoad, _ := json.Marshal(iface) + status, err := orch.apiPost(jsonLoad, url, interfaceName) + if err != nil { + log.Fatalln(err) + } + if status != 201 { + return status + } + fmt.Printf("interface %s status %s %s\n", interfaceName, status, url) + } + } + + return nil +} + +func (h *networkIntentHandler) getObject() (interface{}, interface{}) { + orch := h.orchInstance + retcode := 200 + dataRead := h.orchInstance.dataRead + for _, compositeAppValue := range dataRead.compositeAppMap { + compositeAppMetadata := compositeAppValue.Metadata.Metadata + compositeAppSpec := compositeAppValue.Metadata.Spec + Dig := compositeAppValue.DigMap + for digName, digValue := range Dig { + h.ovnURL = "http://" + orch.MiddleendConf.OvnService + "/v2/projects/" + + orch.projectName + "/composite-apps/" + compositeAppMetadata.Name + + "/" + compositeAppSpec.Version + + "/deployment-intent-groups/" + digName + for nwintName, nwintValue := range digValue.NwintMap { + var wrlintList []NetworkWlIntent + wlurl := h.ovnURL + "/network-controller-intent/" + nwintName + "/workload-intents" + retcode, retval, err := orch.apiGet(wlurl, orch.compositeAppName+"_getnwwlint") + fmt.Printf("Get Wrkld intents in Composite app %s dig %s nw intent %s status %d\n", + orch.compositeAppName, digName, nwintName, retcode) + if err != nil { + fmt.Printf("Failed to read nw workload int") + return nil, 500 + } + if retcode != 200 { + fmt.Printf("Failed to read nw workload int") + return nil, retcode + } + json.Unmarshal(retval, &wrlintList) + nwintValue.WrkintMap = make(map[string]*WrkintData, len(wrlintList)) + for _, wrlIntValue := range wrlintList { + var WrkintDataInstance WrkintData + WrkintDataInstance.Wrkint = wrlIntValue + + var ifaceList []NwInterface + ifaceurl := h.ovnURL + "/network-controller-intent/" + nwintName + + "/workload-intents/" + wrlIntValue.Metadata.Name + "/interfaces" + retcode, retval, err := orch.apiGet(ifaceurl, orch.compositeAppName+"_getnwiface") + fmt.Printf("Get interface in Composite app %s dig %s nw intent %s wrkld intent %s status %d\n", + orch.compositeAppName, digName, nwintName, wrlIntValue.Metadata.Name, retcode) + if err != nil { + fmt.Printf("Failed to read nw interface") + return nil, 500 + } + if retcode != 200 { + fmt.Printf("Failed to read nw interface") + return nil, retcode + } + json.Unmarshal(retval, &ifaceList) + WrkintDataInstance.Interfaces = ifaceList + nwintValue.WrkintMap[wrlIntValue.Metadata.Name] = &WrkintDataInstance + } + } + } + } + return nil, retcode +} + +func (h *networkIntentHandler) getAnchor() (interface{}, interface{}) { + orch := h.orchInstance + retcode := 200 + dataRead := h.orchInstance.dataRead + for _, compositeAppValue := range dataRead.compositeAppMap { + compositeAppMetadata := compositeAppValue.Metadata.Metadata + compositeAppSpec := compositeAppValue.Metadata.Spec + Dig := compositeAppValue.DigMap + for digName, digValue := range Dig { + h.ovnURL = "http://" + orch.MiddleendConf.OvnService + "/v2/projects/" + + orch.projectName + "/composite-apps/" + compositeAppMetadata.Name + + "/" + compositeAppSpec.Version + + "/deployment-intent-groups/" + digName + var nwintList []NetworkCtlIntent + + url := h.ovnURL + "/network-controller-intent" + retcode, retval, err := orch.apiGet(url, orch.compositeAppName+"_getnwint") + fmt.Printf("Get Network Ctl intent in Composite app %s dig %s status %d\n", + orch.compositeAppName, digName, retcode) + if err != nil { + fmt.Printf("Failed to read nw int %s\n", err) + return nil, 500 + } + if retcode != 200 { + fmt.Printf("Failed to read nw int") + return nil, retcode + } + json.Unmarshal(retval, &nwintList) + digValue.NwintMap = make(map[string]*NwintData, len(nwintList)) + for _, nwIntValue := range nwintList { + var NwintDataInstance NwintData + NwintDataInstance.Nwint = nwIntValue + digValue.NwintMap[nwIntValue.Metadata.Name] = &NwintDataInstance + } + } + } + return nil, retcode +} + +func (h *networkIntentHandler) deleteObject() interface{} { + orch := h.orchInstance + retcode := 200 + dataRead := h.orchInstance.dataRead + for _, compositeAppValue := range dataRead.compositeAppMap { + compositeAppMetadata := compositeAppValue.Metadata.Metadata + compositeAppSpec := compositeAppValue.Metadata.Spec + Dig := compositeAppValue.DigMap + for digName, digValue := range Dig { + h.ovnURL = "http://" + orch.MiddleendConf.OvnService + "/v2/projects/" + + orch.projectName + "/composite-apps/" + compositeAppMetadata.Name + + "/" + compositeAppSpec.Version + + "/deployment-intent-groups/" + digName + + for nwintName, nwintValue := range digValue.NwintMap { + for wrkintName, wrkintValue := range nwintValue.WrkintMap { + // Delete the interfaces per workload intent. + for _, value := range wrkintValue.Interfaces { + url := h.ovnURL + "network-controller-intent/" + nwintName + "/workload-intents/" + + wrkintName + "/interfaces/" + value.Spec.Name + fmt.Printf("Delete app nw interface %s\n", url) + retcode, err := orch.apiDel(url, orch.compositeAppName+"_delnwinterface") + if err != nil { + return err + } + if retcode != 204 { + return retcode + } + fmt.Printf("Delete nw interface resp %s\n", retcode) + } + // Delete the workload intents. + url := h.ovnURL + "network-controller-intent/" + nwintName + "/workload-intents/" + wrkintName + fmt.Printf("Delete app nw wl intent %s\n", url) + retcode, err := orch.apiDel(url, orch.compositeAppName+"_delnwwrkintent") + if err != nil { + return err + } + if retcode != 204 { + return retcode + } + fmt.Printf("Delete nw wl intent resp %s\n", retcode) + } // For workload intents in network controller intent. + } // For network controller intents in Dig. + } // For Dig. + } // For composite app. + return retcode +} + +func (h networkIntentHandler) deleteAnchor() interface{} { + orch := h.orchInstance + retcode := 200 + dataRead := h.orchInstance.dataRead + for _, compositeAppValue := range dataRead.compositeAppMap { + compositeAppMetadata := compositeAppValue.Metadata.Metadata + compositeAppSpec := compositeAppValue.Metadata.Spec + Dig := compositeAppValue.DigMap + for digName, digValue := range Dig { + h.ovnURL = "http://" + orch.MiddleendConf.OvnService + "/v2/projects/" + + orch.projectName + "/composite-apps/" + compositeAppMetadata.Name + + "/" + compositeAppSpec.Version + + "/deployment-intent-groups/" + digName + for nwintName, _ := range digValue.NwintMap { + // loop through all app intens in the gpint + url := h.ovnURL + "/network-controller-intent/" + nwintName + fmt.Printf("Delete app nw controller intent %s\n", url) + retcode, err := orch.apiDel(url, compositeAppMetadata.Name+"_delnwctlintent") + if err != nil { + return err + } + if retcode != 204 { + return retcode + } + fmt.Printf("Delete nw controller intent %s\n", retcode) + } + } + } + return retcode +} + +func addNetworkIntent(I orchWorkflow) interface{} { + //1. Add network controller Intent + err := I.createAnchor() + if err != nil { + return err + } + + //2. Add network workload intent + err = I.createObject() + if err != nil { + return err + } + + return nil +} + +func delNwintData(I orchWorkflow) interface{} { + // 1. Create the Anchor point + err := I.deleteObject() + if err != nil { + return err + } + // 2. Create the Objects + err = I.deleteAnchor() + if err != nil { + return err + } + return nil +} diff --git a/src/tools/emcoui/middle_end/app/profile.go b/src/tools/emcoui/middle_end/app/profile.go new file mode 100644 index 00000000..fff43cdc --- /dev/null +++ b/src/tools/emcoui/middle_end/app/profile.go @@ -0,0 +1,261 @@ +/* +======================================================================= +Copyright (c) 2017-2020 Aarna Networks, Inc. +All rights reserved. +====================================================================== +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 app + +import ( + "encoding/json" + "fmt" + "log" +) + +// ProfileData captures per app profile +type ProfileData struct { + Name string `json:"profileName"` + AppProfiles map[string]string `json:"appProfile"` +} + +// ProfileMeta is metadta for the profile APIs +type ProfileMeta struct { + Metadata apiMetaData `json:"metadata"` + Spec ProfileSpec `json:"spec"` +} + +// ProfileSpec is the spec for the profile APIs +type ProfileSpec struct { + AppName string `json:"app-name"` +} + +// ProfileHandler This implements the orchworkflow interface +type ProfileHandler struct { + orchURL string + orchInstance *OrchestrationHandler + response struct { + payload map[string][]byte + status map[string]string + } +} + +func (h *ProfileHandler) getObject() (interface{}, interface{}) { + orch := h.orchInstance + dataRead := h.orchInstance.dataRead + retcode := 200 + for _, compositeAppValue := range dataRead.compositeAppMap { + var profileList []ProfileMeta + compositeAppMetadata := compositeAppValue.Metadata.Metadata + compositeAppSpec := compositeAppValue.Metadata.Spec + h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" + + orch.projectName + "/composite-apps/" + compositeAppMetadata.Name + + "/" + compositeAppSpec.Version + "/composite-profiles" + for profileName, profileValue := range compositeAppValue.ProfileDataArray { + url := h.orchURL + "/" + profileName + "/profiles" + retcode, respval, err := orch.apiGet(url, compositeAppMetadata.Name+"_getprofiles") + fmt.Printf("Get app profiles status %d\n", retcode) + if err != nil { + fmt.Printf("Failed to read profile %s\n", profileName) + return nil, 500 + } + if retcode != 200 { + fmt.Printf("Failed to read profile %s\n", profileName) + return nil, retcode + } + json.Unmarshal(respval, &profileList) + profileValue.AppProfiles = make([]ProfileMeta, len(profileList)) + for appProfileIndex, appProfile := range profileList { + profileValue.AppProfiles[appProfileIndex] = appProfile + } + } + } + return nil, retcode +} + +func (h *ProfileHandler) getAnchor() (interface{}, interface{}) { + orch := h.orchInstance + respcode := 200 + dataRead := h.orchInstance.dataRead + for _, compositeAppValue := range dataRead.compositeAppMap { + var profilemetaList []ProfileMeta + compositeAppMetadata := compositeAppValue.Metadata.Metadata + compositeAppSpec := compositeAppValue.Metadata.Spec + h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" + + orch.projectName + "/composite-apps/" + compositeAppMetadata.Name + + "/" + compositeAppSpec.Version + "/composite-profiles" + + respcode, respdata, err := orch.apiGet(h.orchURL, compositeAppMetadata.Name+"_getcprofile") + if err != nil { + fmt.Printf("Failed to get composite profiles\n") + return nil, 500 + } + if respcode != 200 { + fmt.Printf("composite profile GET status %d\n", respcode) + return nil, respcode + } + json.Unmarshal(respdata, &profilemetaList) + compositeAppValue.ProfileDataArray = make(map[string]*ProfilesData, len(profilemetaList)) + for _, value := range profilemetaList { + ProfilesDataInstance := ProfilesData{} + ProfilesDataInstance.Profile = value + compositeAppValue.ProfileDataArray[value.Metadata.Name] = &ProfilesDataInstance + } + } + return nil, respcode +} + +func (h *ProfileHandler) deleteObject() interface{} { + orch := h.orchInstance + dataRead := h.orchInstance.dataRead + for _, compositeAppValue := range dataRead.compositeAppMap { + compositeAppMetadata := compositeAppValue.Metadata.Metadata + compositeAppSpec := compositeAppValue.Metadata.Spec + h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" + + orch.projectName + "/composite-apps/" + compositeAppMetadata.Name + + "/" + compositeAppSpec.Version + "/composite-profiles/" + for profileName, profileValue := range compositeAppValue.ProfileDataArray { + for _, appProfileValue := range profileValue.AppProfiles { + url := h.orchURL + profileName + "/profiles/" + appProfileValue.Metadata.Name + + fmt.Printf("Delete app profiles %s\n", url) + resp, err := orch.apiDel(url, compositeAppMetadata.Name+"_delappProfiles") + if err != nil { + return err + } + if resp != 204 { + return resp + } + fmt.Printf("Delete profiles status %s\n", resp) + } + } + } + return nil +} + +func (h *ProfileHandler) deleteAnchor() interface{} { + orch := h.orchInstance + dataRead := h.orchInstance.dataRead + for _, compositeAppValue := range dataRead.compositeAppMap { + compositeAppMetadata := compositeAppValue.Metadata.Metadata + compositeAppSpec := compositeAppValue.Metadata.Spec + h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" + + orch.projectName + "/composite-apps/" + compositeAppMetadata.Name + + "/" + compositeAppSpec.Version + "/composite-profiles/" + + for profileName, _ := range compositeAppValue.ProfileDataArray { + url := h.orchURL + profileName + fmt.Printf("Delete profile %s\n", url) + resp, err := orch.apiDel(url, compositeAppMetadata.Name+"_delProfile") + if err != nil { + return err + } + if resp != 204 { + return resp + } + fmt.Printf("Delete profile status %s\n", resp) + } + } + return nil +} + +func (h *ProfileHandler) createAnchor() interface{} { + orch := h.orchInstance + + profileCreate := ProfileMeta{ + Metadata: apiMetaData{ + Name: orch.compositeAppName + "_profile", + Description: "Profile created from middleend", + UserData1: "data 1", + UserData2: "data2"}, + } + jsonLoad, _ := json.Marshal(profileCreate) + h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" + + orch.projectName + "/composite-apps" + url := h.orchURL + "/" + orch.compositeAppName + "/" + "v1" + "/composite-profiles" + resp, err := orch.apiPost(jsonLoad, url, orch.compositeAppName+"_profile") + if err != nil { + return err + } + if resp != 201 { + return resp + } + fmt.Printf("ProfileHandler resp %s\n", resp) + + return nil +} + +func (h *ProfileHandler) createObject() interface{} { + orch := h.orchInstance + + for i := range orch.meta { + fileName := orch.meta[i].ProfileMetadata.FileName + appName := orch.meta[i].Metadata.Name + profileName := orch.meta[i].Metadata.Name + "_profile" + + // Upload the application helm chart + fh := orch.file[fileName] + profileAdd := ProfileMeta{ + Metadata: apiMetaData{ + Name: profileName, + Description: "NA", + UserData1: "data 1", + UserData2: "data2"}, + Spec: ProfileSpec{ + AppName: appName}, + } + compositeProfilename := orch.compositeAppName + "_profile" + + url := h.orchURL + "/" + orch.compositeAppName + "/" + "v1" + "/" + + "composite-profiles" + "/" + compositeProfilename + "/profiles" + jsonLoad, _ := json.Marshal(profileAdd) + status, err := orch.apiPostMultipart(jsonLoad, fh, url, profileName, fileName) + if err != nil { + log.Fatalln(err) + } + if status != 201 { + return status + } + fmt.Printf("CompositeProfile Profile %s status %s %s\n", profileName, status, url) + } + + return nil +} + +func createProfile(I orchWorkflow) interface{} { + // 1. Create the Anchor point + err := I.createAnchor() + if err != nil { + return err + } + // 2. Create the Objects + err = I.createObject() + if err != nil { + return err + } + return nil +} + +func delProfileData(I orchWorkflow) interface{} { + // 1. Delete the object + err := I.deleteObject() + if err != nil { + return err + } + // 2. Delete the Anchor + err = I.deleteAnchor() + if err != nil { + return err + } + return nil +} diff --git a/src/tools/emcoui/middle_end/app/projects.go b/src/tools/emcoui/middle_end/app/projects.go new file mode 100644 index 00000000..39fe7573 --- /dev/null +++ b/src/tools/emcoui/middle_end/app/projects.go @@ -0,0 +1,187 @@ +/* +======================================================================= +Copyright (c) 2017-2020 Aarna Networks, Inc. +All rights reserved. +====================================================================== +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 app + +import ( + "encoding/json" + "fmt" +) + +// CompositeApp application structure +type ProjectMetadata struct { + Metadata apiMetaData `json:"metadata"` +} + +// CompAppHandler , This implements the orchworkflow interface +type projectHandler struct { + orchURL string + orchInstance *OrchestrationHandler +} + +func (h *projectHandler) getObject() (interface{}, interface{}) { + orch := h.orchInstance + dataRead := h.orchInstance.dataRead + var cappList []CompositeApp + if orch.treeFilter != nil { + temp:=CompositeApp{} + h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" + + orch.projectName + "/composite-apps/" + orch.treeFilter.compositeAppName+"/"+ + orch.treeFilter.compositeAppVersion + respcode, respdata, err := orch.apiGet(h.orchURL, orch.projectName+"_getcapps") + fmt.Printf("Get capp status %s\n", respcode) + if err != nil { + return nil, 500 + } + if respcode != 200 { + return nil, respcode + } + fmt.Printf("Get capp status %s\n", respcode) + json.Unmarshal(respdata, &temp) + cappList = append(cappList, temp) + } else { + h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" + + orch.projectName + "/composite-apps" + respcode, respdata, err := orch.apiGet(h.orchURL, orch.projectName+"_getcapps") + fmt.Printf("Get capp status %s\n", respcode) + if err != nil { + return nil, 500 + } + if respcode != 200 { + return nil, respcode + } + fmt.Printf("Get capp status %s\n", respcode) + json.Unmarshal(respdata, &cappList) + } + + dataRead.compositeAppMap = make(map[string]*CompositeAppTree, len(cappList)) + for k, value := range cappList { + fmt.Printf("%+v", cappList[k]) + var cappsDataInstance CompositeAppTree + cappName := value.Metadata.Name + cappsDataInstance.Metadata = value + dataRead.compositeAppMap[cappName] = &cappsDataInstance + } + return nil, 200 +} + +func (h *projectHandler) getAnchor() (interface{}, interface{}) { + orch := h.orchInstance + dataRead := h.orchInstance.dataRead + h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" + + orch.projectName + + respcode, respdata, err := orch.apiGet(h.orchURL, orch.projectName+"_getProject") + if err != nil { + return nil, 500 + } + if respcode != 200 { + return nil, respcode + } + fmt.Printf("Get project %s\n", respcode) + json.Unmarshal(respdata, &dataRead.Metadata) + return nil, respcode +} + +func (h *projectHandler) deleteObject() interface{} { + orch := h.orchInstance + dataRead := h.orchInstance.dataRead + cappList := dataRead.compositeAppMap + h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" + + orch.projectName + "/composite-apps" + for compositeAppName, compositeAppValue := range cappList { + url := h.orchURL + "/" + compositeAppName + "/" + compositeAppValue.Metadata.Spec.Version + fmt.Printf("Delete composite app %s\n", url) + resp, err := orch.apiDel(url, compositeAppName+"_delcapp") + if err != nil { + return err + } + if resp != 204 { + return resp + } + fmt.Printf("Delete composite app status %s\n", resp) + } + return nil +} + +func (h *projectHandler) deleteAnchor() interface{} { + orch := h.orchInstance + h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" + orch.projectName + fmt.Printf("Delete Project %s \n", h.orchURL) + resp, err := orch.apiDel(h.orchURL, orch.projectName+"_delProject") + if err != nil { + return err + } + if resp != 204 { + return resp + } + fmt.Printf("Delete Project status %s\n", resp) + return nil +} + +func (h *projectHandler) createAnchor() interface{} { + orch := h.orchInstance + + projectCreate := ProjectMetadata{ + Metadata: apiMetaData{ + Name: orch.projectName, + Description: orch.projectDesc, + UserData1: "data 1", + UserData2: "data 2"}, + } + + jsonLoad, _ := json.Marshal(projectCreate) + h.orchURL = "http://" + orch.MiddleendConf.OrchService + "/v2/projects/" + orch.projectName + resp, err := orch.apiPost(jsonLoad, h.orchURL, orch.projectName) + if err != nil { + return err + } + if resp != 201 { + return resp + } + orch.version = "v1" + fmt.Printf("projectHandler resp %s\n", resp) + + return nil +} + +func (h *projectHandler) createObject() interface{} { + return nil +} + +func createProject(I orchWorkflow) interface{} { + // 1. Create the Anchor point + err := I.createAnchor() + if err != nil { + return err + } + return nil +} + +func delProject(I orchWorkflow) interface{} { + // 1. Delete the object + err := I.deleteObject() + if err != nil { + return err + } + // 2. Delete the Anchor + err = I.deleteAnchor() + if err != nil { + return err + } + return nil +} diff --git a/src/tools/emcoui/middle_end/authproxy/README.md b/src/tools/emcoui/middle_end/authproxy/README.md new file mode 100644 index 00000000..1d68a431 --- /dev/null +++ b/src/tools/emcoui/middle_end/authproxy/README.md @@ -0,0 +1,16 @@ + +Authproxy is part of middleend and it exposes following 3 apis +1. **/v1/login** + - Redirects user to keycloak login page. + - Sets a cookie with original URL +2. **/v1/callback** + - After successful login gets auth code and exchange it for token. + - Set id_token and access_token in cookie and redirects to original URL +3. **/v1/auth** + - Retrieve idtoken from cookie and verifies the JWT. + - If id_token is valid then access to resources else redirects to login page. + +Required inputs of authproxy comes from authproxy section of helm config +- Issuer +- Redirect URI +- Client id diff --git a/src/tools/emcoui/middle_end/authproxy/authproxy.go b/src/tools/emcoui/middle_end/authproxy/authproxy.go new file mode 100644 index 00000000..78819ef6 --- /dev/null +++ b/src/tools/emcoui/middle_end/authproxy/authproxy.go @@ -0,0 +1,281 @@ +/* +======================================================================= +Copyright (c) 2017-2020 Aarna Networks, Inc. +All rights reserved. +====================================================================== +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 authproxy + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "net/url" + "strings" + + "github.com/dgrijalva/jwt-go" +) + +type AuthProxy struct { + AuthProxyConf AuthProxyConfig +} + +// AuthProxyConfig holds inputs of authproxy +type AuthProxyConfig struct { + Issuer string `json:"issuer"` + RedirectURI string `json:"redirect_uri"` + ClientID string `json:"client_id"` +} + +// NewAppHandler interface implementing REST callhandler +func NewAppHandler() *AuthProxy { + return &AuthProxy{} +} + +// OpenIDConfiguration struct to map response from OIDC +type OpenIDConfiguration struct { + Issuer string `json:"issuer"` + AuthzEndpoint string `json:"authorization_endpoint"` + TokenEndPoint string `json:"token_endpoint"` + IntrospectionEndpoint string `json:"introspection_endpoint"` + JWKSURI string `json:"jwks_uri"` +} + +// TokenConfig struct holds tokens +type TokenConfig struct { + ACCESSTOKEN string `json:"access_token"` + IDTOKEN string `json:"id_token"` +} + +// RealmConfig struct holds public_key of issuer +type RealmConfig struct { + PublicKey string `json:"public_key"` +} + +var openIDConfig *OpenIDConfiguration +var realmConfig *RealmConfig + +// Loads the openIDconfig from the OIDC only once +func getOpenIDConfig(issuer string) OpenIDConfiguration { + if openIDConfig != nil { + log.Println("openidconfig is not null and returning the cached value") + return *openIDConfig + } + log.Println("openidconfig is null and loading the values") + url := issuer + ".well-known/openid-configuration" + response, err := http.Get(url) + if err != nil { + log.Printf("The openidconfig HTTP request failed with error %s", err) + return *openIDConfig + } + + defer response.Body.Close() + bodyBytes, _ := ioutil.ReadAll(response.Body) + json.Unmarshal(bodyBytes, &openIDConfig) + return *openIDConfig +} + +// LoginHandler redirects to client login page and sets cookie with the original path +func (h AuthProxy) LoginHandler(w http.ResponseWriter, r *http.Request) { + log.Println("LoginHandler start") + + rd := r.FormValue("rd") + scope := r.FormValue("scope") + + log.Printf("[LoginHandler] url Param 'rd' is: %s, 'scope' is: %s\n", string(rd), string(scope)) + redirect := r.Header.Get("X-Auth-Request-Redirect") + log.Println("redirect url from HEADER is: " + redirect) + if len(redirect) == 0 { + redirect = rd + } + + cookie := http.Cookie{ + Name: "org", + Value: redirect, + Path: "/", + Domain: "", + Secure: false, + HttpOnly: false, + } + // Set cookie with original URL + http.SetCookie(w, &cookie) + state := "1234" // Optional parameter included in all login redirects + if len(scope) == 0 { + // generate token with offline_access scope so that it can be stored in cookie and reused + scope = "openid offline_access" + } + + // get authorization endpoint from function openidconfig + authzEndpoint := getOpenIDConfig(h.AuthProxyConf.Issuer).AuthzEndpoint + + // Construct redirect URL with params + u, _ := url.Parse(authzEndpoint) + q := u.Query() + q.Add("client_id", h.AuthProxyConf.ClientID) + // h.AuthProxyConf.RedirectURI is the callback endpoint of middleend. + // after successful authentication, url will be redirected to this one + q.Add("redirect_uri", h.AuthProxyConf.RedirectURI) + q.Add("response_type", "code") + q.Add("scope", scope) + q.Add("state", state) + u.RawQuery = q.Encode() + + log.Println("[LoginHandler] Redireced URL -> " + u.String()) + http.Redirect(w, r, u.String(), http.StatusFound) +} + +/* + * CallbackHandler reads the OIDC config + * Gets token with API and sets id and access tokens in cookies + * Redirects to original URL + */ +func (h AuthProxy) CallbackHandler(w http.ResponseWriter, r *http.Request) { + state := r.FormValue("state") + code := r.FormValue("code") + tokenEndpoint := getOpenIDConfig(h.AuthProxyConf.Issuer).TokenEndPoint + log.Printf("[CallbackHandler] state: %s , code: %s , tokenEndpoint: %s \n", state, code, tokenEndpoint) + + client := http.Client{} + form := url.Values{} + form.Add("client_id", h.AuthProxyConf.ClientID) + form.Add("client_secret", "") + form.Add("grant_type", "authorization_code") + form.Add("code", code) + form.Add("redirect_uri", h.AuthProxyConf.RedirectURI) + request, err := http.NewRequest("POST", tokenEndpoint, strings.NewReader(form.Encode())) + request.Header.Add("Content-Type", "application/x-www-form-urlencoded") + + resp, err := client.Do(request) + if err != nil { + log.Printf("[CallbackHandler] HTTP request to %s failed\n", tokenEndpoint) + log.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Println("[CallbackHandler] Error while reading response from tokenEndpoint") + log.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + var tokenConfig TokenConfig + json.Unmarshal(body, &tokenConfig) + log.Printf("[CallbackHandler] access_token: %s \n id_token: %s\n", tokenConfig.ACCESSTOKEN, tokenConfig.IDTOKEN) + + // Construct the original URL with cookie org + var orginalURL string + cookie, err := r.Cookie("org") + if err == nil { + orginalURL = cookie.Value + } + fmt.Println("[CallbackHandler] orginalURL from cookie: " + orginalURL) + + // Create cookies with id_token, access_token + idTokenCookie := http.Cookie{ + Name: "idtoken", + Value: tokenConfig.IDTOKEN, + Path: "/", + Domain: "", + Secure: false, + HttpOnly: false, + } + accessTokencookie := http.Cookie{ + Name: "accesstoken", + Value: tokenConfig.ACCESSTOKEN, + Path: "/", + Domain: "", + Secure: false, + HttpOnly: false, + } + + http.SetCookie(w, &idTokenCookie) + http.SetCookie(w, &accessTokencookie) + + // Finally return the original URL with the cookies + http.Redirect(w, r, orginalURL, http.StatusFound) +} + +// AuthHandler verifies the token and returns response +func (h AuthProxy) AuthHandler(w http.ResponseWriter, r *http.Request) { + log.Println("[AuthHandler] Authenticating the token") + + var idToken string + cookie, err := r.Cookie("idtoken") + if err == nil { + cookieVal := cookie.Value + idToken = cookieVal + } + + if idToken == "" { + log.Println("[AuthHandler] id token is nil ") + w.WriteHeader(http.StatusUnauthorized) + return + } + error := validateToken(h.AuthProxyConf.Issuer, idToken) + if error != nil { + log.Println("[AuthHandler] Issue with token and returning failed response") + w.WriteHeader(http.StatusUnauthorized) + } +} + +/* +* Validates JWT token +* verifies signature, token expiry and invalid check... etc + */ +func validateToken(issuer string, reqToken string) error { + log.Printf("[AuthHandler] Validating JWT token: \n%s\n", reqToken) + + //load realm public key only once + if realmConfig == nil { + log.Println("[AuthHandler] realmconfig is null and loading the value") + response, err := http.Get(issuer) + if err != nil { + log.Printf("[AuthHandler] Error while retreiving issuer details : %s\n", err) + return err + } + defer response.Body.Close() + bodyBytes, _ := ioutil.ReadAll(response.Body) + json.Unmarshal(bodyBytes, &realmConfig) + } + SecretKey := "-----BEGIN CERTIFICATE-----\n" + realmConfig.PublicKey + "\n-----END CERTIFICATE-----" + key, er := jwt.ParseRSAPublicKeyFromPEM([]byte(SecretKey)) + if er != nil { + log.Println("[AuthHandler] Error occured while parsing public key") + log.Println(er) + return er + } + + token, err := jwt.Parse(reqToken, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { + return nil, fmt.Errorf("[AuthHandler] Unexpected signing method: %v", token.Header["alg"]) + } + return key, nil + }) + + if err != nil { + log.Println("[AuthHandler] Error while parsing token") + log.Println(err) + return err + } + if _, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { + log.Println("[AuthHandler] Token is valid") + } + return nil +} diff --git a/src/tools/emcoui/middle_end/db/dbconnection.go b/src/tools/emcoui/middle_end/db/dbconnection.go new file mode 100644 index 00000000..5496c39c --- /dev/null +++ b/src/tools/emcoui/middle_end/db/dbconnection.go @@ -0,0 +1,145 @@ +/* +======================================================================= +Copyright (c) 2017-2020 Aarna Networks, Inc. +All rights reserved. +====================================================================== +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 ( + "encoding/json" + "fmt" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "golang.org/x/net/context" +) + +// MongoStore is the interface which implements the db.Store interface +type MongoStore struct { + db *mongo.Database +} + +// Key interface +type Key interface { +} + +// DBconn variable of type Store +var DBconn Store + +// Store Interface which implements the data store functions +type Store interface { + HealthCheck() error + Find(coll string, key []byte, tag string) ([][]byte, error) + Unmarshal(inp []byte, out interface{}) error +} + +// NewMongoStore Return mongo client +func NewMongoStore(name string, store *mongo.Database, svcEp string) (Store, error) { + if store == nil { + ip := "mongodb://" + svcEp + clientOptions := options.Client() + clientOptions.ApplyURI(ip) + mongoClient, err := mongo.NewClient(clientOptions) + if err != nil { + return nil, err + } + + err = mongoClient.Connect(context.Background()) + if err != nil { + return nil, err + } + store = mongoClient.Database(name) + } + return &MongoStore{ + db: store, + }, nil +} + +// CreateDBClient creates the DB client. currently only mongo +func CreateDBClient(dbType string, dbName string, svcEp string) error { + var err error + switch dbType { + case "mongo": + DBconn, err = NewMongoStore(dbName, nil, svcEp) + default: + fmt.Println(dbType + "DB not supported") + } + return err +} + +// HealthCheck verifies the database connection +func (m *MongoStore) HealthCheck() error { + _, err := (*mongo.SingleResult).DecodeBytes(m.db.RunCommand(context.Background(), bson.D{{"serverStatus", 1}})) + if err != nil { + fmt.Println("Error getting DB server status: err %s", err) + } + return nil +} + +func (m *MongoStore) Unmarshal(inp []byte, out interface{}) error { + err := bson.Unmarshal(inp, out) + if err != nil { + fmt.Printf("Failed to unmarshall bson") + return err + } + return nil +} + +// Find a document +func (m *MongoStore) Find(coll string, key []byte, tag string) ([][]byte, error) { + var bsonMap bson.M + err := json.Unmarshal([]byte(key), &bsonMap) + if err != nil { + fmt.Println("Failed to unmarshall %s\n", key) + return nil, err + } + + filter := bson.M{ + "$and": []bson.M{bsonMap}, + } + + fmt.Printf("%+v %s\n", filter, tag) + projection := bson.D{ + {tag, 1}, + {"_id", 0}, + } + + c := m.db.Collection(coll) + + cursor, err := c.Find(context.Background(), filter, options.Find().SetProjection(projection)) + if err != nil { + fmt.Println("Failed to find the document %s\n", err) + return nil, err + } + + defer cursor.Close(context.Background()) + var data []byte + var result [][]byte + for cursor.Next(context.Background()) { + d := cursor.Current + switch d.Lookup(tag).Type { + case bson.TypeString: + data = []byte(d.Lookup(tag).StringValue()) + default: + r, err := d.LookupErr(tag) + if err != nil { + fmt.Println("Unable to read data %s %s\n", string(r.Value), err) + } + data = r.Value + } + result = append(result, data) + } + return result, nil +} diff --git a/src/tools/emcoui/middle_end/go.mod b/src/tools/emcoui/middle_end/go.mod new file mode 100644 index 00000000..3d195979 --- /dev/null +++ b/src/tools/emcoui/middle_end/go.mod @@ -0,0 +1,14 @@ +module example.com/middleend + +go 1.14 + +require ( + github.com/dgrijalva/jwt-go v3.2.0+incompatible + github.com/gorilla/handlers v1.5.0 + github.com/gorilla/mux v1.8.0 + github.com/lestrrat-go/jwx v1.0.5 + go.mongodb.org/mongo-driver v1.4.1 + golang.org/x/net v0.0.0-20200707034311-ab3426394381 + k8s.io/apimachinery v0.19.3 + k8s.io/client-go v0.19.3 +) diff --git a/src/tools/emcoui/middle_end/go.sum b/src/tools/emcoui/middle_end/go.sum new file mode 100644 index 00000000..d8e5a43f --- /dev/null +++ b/src/tools/emcoui/middle_end/go.sum @@ -0,0 +1,428 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.51.0/go.mod h1:hWtGJ6gnXH+KgDv+V0zFGDvpi07n3z8ZNj3T1RW0Gcw= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= +github.com/Azure/go-autorest/autorest v0.9.6/go.mod h1:/FALq9T/kS7b5J5qsQ+RSTUdAmGFqi0vUdVNNx8q630= +github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= +github.com/Azure/go-autorest/autorest/adal v0.8.2/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q= +github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= +github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g= +github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM= +github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= +github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/aws/aws-sdk-go v1.29.15 h1:0ms/213murpsujhsnxnNKNeVouW60aJqSd992Ks3mxs= +github.com/aws/aws-sdk-go v1.29.15/go.mod h1:1KvfttTE3SPKMpo8g2c6jL3ZKfXtFvKscTgahTma5Xg= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v1.0.2 h1:KPldsxuKGsS2FPWsNeg9ZO18aCrGKujPoWXn2yo+KQM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= +github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-logr/logr v0.2.0 h1:QvGt2nLcHH0WK9orKa+ppBPAxREcH364nPUedEpK0TY= +github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= +github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= +github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= +github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= +github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= +github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg= +github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs= +github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= +github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= +github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk= +github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28= +github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo= +github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk= +github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw= +github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360= +github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg= +github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE= +github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8= +github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= +github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= +github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= +github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= +github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ= +github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0= +github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= +github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gnostic v0.4.1 h1:DLJCy1n/vrD4HPjOvYcT8aYQXpPIzoRZONaYwyycI+I= +github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= +github.com/gorilla/handlers v1.5.0 h1:4wjo3sf9azi99c8hTmyaxp9y5S+pFszsy3pP0rAw/lw= +github.com/gorilla/handlers v1.5.0/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q= +github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= +github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.9.5 h1:U+CaK85mrNNb4k8BNOfgJtJ/gr6kswUCFj6miSzVC6M= +github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lestrrat-go/iter v0.0.0-20200422075355-fc1769541911 h1:FvnrqecqX4zT0wOIbYK1gNgTm0677INEWiFY8UEYggY= +github.com/lestrrat-go/iter v0.0.0-20200422075355-fc1769541911/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc= +github.com/lestrrat-go/jwx v1.0.5 h1:8bVUGXXkR3+YQNwuFof3lLxSJMLtrscHJfGI6ZIBRD0= +github.com/lestrrat-go/jwx v1.0.5/go.mod h1:TPF17WiSFegZo+c20fdpw49QD+/7n4/IsGvEmCSWwT0= +github.com/lestrrat-go/pdebug v0.0.0-20200204225717-4d6bd78da58d/go.mod h1:B06CSso/AWxiPejj+fheUINGeBKeeEZNt8w+EoU7+L8= +github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= +github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c h1:u40Z8hqBAAQyv+vATcGgV0YCnDjqSL7/q/JyPhhJSPk= +github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= +github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc h1:n+nNi93yXLkJvKwXNP9d55HC7lGK4H/SRcwB5IaUZLo= +github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.mongodb.org/mongo-driver v1.4.1 h1:38NSAyDPagwnFpUA/D5SFgbugUYR3NzYRNa4Qk9UxKs= +go.mongodb.org/mongo-driver v1.4.1/go.mod h1:llVBH2pkj9HywK0Dtdt6lDikOjFLbceHVu/Rc0iMKLs= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5 h1:8dUaAV7K4uHsF56JQWkprecIQKdPHtR9jCHF5nB8uzc= +golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6 h1:pE8b58s1HRDMi8RDc79m0HISf9D4TzseP40cEA6IGfs= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4 h1:5/PjkGUjvEU5Gl6BxmvKRPpqo2uNMv4rcHBMwzk/st8= +golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200417140056-c07e33ef3290/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +k8s.io/api v0.19.3 h1:GN6ntFnv44Vptj/b+OnMW7FmzkpDoIDLZRvKX3XH9aU= +k8s.io/api v0.19.3/go.mod h1:VF+5FT1B74Pw3KxMdKyinLo+zynBaMBiAfGMuldcNDs= +k8s.io/apimachinery v0.19.3 h1:bpIQXlKjB4cB/oNpnNnV+BybGPR7iP5oYpsOTEJ4hgc= +k8s.io/apimachinery v0.19.3/go.mod h1:DnPGDnARWFvYa3pMHgSxtbZb7gpzzAZ1pTfaUNDVlmA= +k8s.io/client-go v0.19.3 h1:ctqR1nQ52NUs6LpI0w+a5U+xjYwflFwA13OJKcicMxg= +k8s.io/client-go v0.19.3/go.mod h1:+eEMktZM+MG0KO+PTkci8xnbCZHvj9TqR6Q1XDUIJOM= +k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= +k8s.io/klog/v2 v2.2.0 h1:XRvcwJozkgZ1UQJmfMGpvRthQHOvihEhYtDfAaxMz/A= +k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= +k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o= +k8s.io/utils v0.0.0-20200729134348-d5654de09c73 h1:uJmqzgNWG7XyClnU/mLPBWwfKKF1K8Hf8whTseBgJcg= +k8s.io/utils v0.0.0-20200729134348-d5654de09c73/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +sigs.k8s.io/structured-merge-diff/v4 v4.0.1 h1:YXTMot5Qz/X1iBRJhAt+vI+HVttY0WkSqqhKxQ0xVbA= +sigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/src/tools/emcoui/middle_end/main/main.go b/src/tools/emcoui/middle_end/main/main.go new file mode 100644 index 00000000..97a31017 --- /dev/null +++ b/src/tools/emcoui/middle_end/main/main.go @@ -0,0 +1,112 @@ +/* +======================================================================= +Copyright (c) 2017-2020 Aarna Networks, Inc. +All rights reserved. +====================================================================== +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" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "os/signal" + "time" + + "example.com/middleend/app" + "example.com/middleend/authproxy" + "example.com/middleend/db" + "github.com/gorilla/handlers" + "github.com/gorilla/mux" +) + +/* This is the main package of the middleend. This package + * implements the http server which exposes service ar 9891. + * It also intialises an API router which handles the APIs with + * subpath /v1. + */ +func main() { + depHandler := app.NewAppHandler() + authProxyHandler := authproxy.NewAppHandler() + configFile, err := os.Open("/opt/emco/config/middleend.conf") + if err != nil { + fmt.Printf("Failed to read middleend configuration") + return + } + defer configFile.Close() + + // Read the configuration json + byteValue, _ := ioutil.ReadAll(configFile) + json.Unmarshal(byteValue, &depHandler.MiddleendConf) + json.Unmarshal(byteValue, &authProxyHandler.AuthProxyConf) + + // Connect to the DB + err = db.CreateDBClient("mongo", "mco", depHandler.MiddleendConf.Mongo) + if err != nil { + fmt.Println("Failed to connect to DB") + return + } + // Get an instance of the OrchestrationHandler, this type implements + // the APIs i.e CreateApp, ShowApp, DeleteApp. + httpRouter := mux.NewRouter().PathPrefix("/middleend").Subrouter() + loggedRouter := handlers.LoggingHandler(os.Stdout, httpRouter) + log.Println("Starting middle end service") + + httpServer := &http.Server{ + Handler: loggedRouter, + Addr: ":" + depHandler.MiddleendConf.OwnPort, + WriteTimeout: 15 * time.Second, + ReadTimeout: 15 * time.Second, + } + httpRouter.HandleFunc("/healthcheck", depHandler.GetHealth).Methods("GET") + + // POST, GET, DELETE composite apps + httpRouter.HandleFunc("/projects/{project-name}/composite-apps", depHandler.CreateApp).Methods("POST") + //httpRouter.HandleFunc("/projects/{project-name}/composite-apps", depHandler.GetAllCaps).Methods("GET") + httpRouter.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{version}", + depHandler.GetSvc).Methods("GET") + httpRouter.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{version}", + depHandler.DelSvc).Methods("DELETE") + // POST, GET, DELETE deployment intent groups + httpRouter.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{version}/deployment-intent-groups", + depHandler.CreateDig).Methods("POST") + httpRouter.HandleFunc("/projects/{project-name}/deployment-intent-groups", depHandler.GetAllDigs).Methods("GET") + httpRouter.HandleFunc("/projects/{project-name}/composite-apps/{composite-app-name}/{version}/deployment-intent-groups/{deployment-intent-group-name}", + depHandler.DelDig).Methods("DELETE") + + // Authproxy relates APIs + httpRouter.HandleFunc("/login", authProxyHandler.LoginHandler).Methods("GET") + httpRouter.HandleFunc("/callback", authProxyHandler.CallbackHandler).Methods("GET") + httpRouter.HandleFunc("/auth", authProxyHandler.AuthHandler).Methods("GET") + // Cluster createion API + httpRouter.HandleFunc("/clusterproviders/{cluster-provider-name}/clusters", depHandler.CheckConnection).Methods("POST") + + // Start server in a go routine. + go func() { + log.Fatal(httpServer.ListenAndServe()) + }() + + // Gracefull shutdown of the server, + // create a channel and wait for SIGINT + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + log.Println("wait for signal") + <-c + log.Println("Bye Bye") + httpServer.Shutdown(context.Background()) +} diff --git a/src/tools/emcoui/public/robots.txt b/src/tools/emcoui/public/robots.txt index e9e57dc4..f2ce78e7 100644 --- a/src/tools/emcoui/public/robots.txt +++ b/src/tools/emcoui/public/robots.txt @@ -1,3 +1,17 @@ +#======================================================================= +# Copyright (c) 2017-2020 Aarna Networks, Inc. +# All rights reserved. +# ====================================================================== +# 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. +# ======================================================================== # https://www.robotstxt.org/robotstxt.html User-agent: * Disallow: diff --git a/src/tools/emcoui/src/App.js b/src/tools/emcoui/src/App.js index 2613ecfd..3a2c5ffc 100644 --- a/src/tools/emcoui/src/App.js +++ b/src/tools/emcoui/src/App.js @@ -11,7 +11,7 @@ // 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. -// ======================================================================== +// ======================================================================== import React from "react"; import { BrowserRouter as Router, @@ -53,7 +53,7 @@ function App() { <Redirect exact from={`${match.path}`} - to={`${match.path}/composite-apps`} + to={`${match.path}/services`} /> <Route path={`${match.path}`} diff --git a/src/tools/emcoui/src/admin/AdminNavigator.js b/src/tools/emcoui/src/admin/AdminNavigator.js index be07cba0..9cee73b1 100644 --- a/src/tools/emcoui/src/admin/AdminNavigator.js +++ b/src/tools/emcoui/src/admin/AdminNavigator.js @@ -11,7 +11,7 @@ // 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. -// ======================================================================== +// ======================================================================== import React, { useState } from "react"; import PropTypes from "prop-types"; import clsx from "clsx"; @@ -37,15 +37,15 @@ const categories = [ url: "/projects", }, { - id: "Clusters", - icon: <DnsRoundedIcon />, - url: "/clusters", - }, - { id: "Controllers", icon: <SettingsIcon />, url: "/controllers", }, + { + id: "Clusters", + icon: <DnsRoundedIcon />, + url: "/clusters", + }, ], }, ]; diff --git a/src/tools/emcoui/src/admin/clusterProvider/ClusterProviderForm.jsx b/src/tools/emcoui/src/admin/clusterProvider/ClusterProviderForm.jsx index 150a1912..57ee7557 100644 --- a/src/tools/emcoui/src/admin/clusterProvider/ClusterProviderForm.jsx +++ b/src/tools/emcoui/src/admin/clusterProvider/ClusterProviderForm.jsx @@ -11,147 +11,157 @@ // 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. -// ======================================================================== -import React from 'react'; -import PropTypes from 'prop-types'; -import { withStyles } from '@material-ui/core/styles'; -import Button from '@material-ui/core/Button'; +// ======================================================================== +import React from "react"; +import PropTypes from "prop-types"; +import { withStyles } from "@material-ui/core/styles"; +import Button from "@material-ui/core/Button"; -import Dialog from '@material-ui/core/Dialog'; -import MuiDialogTitle from '@material-ui/core/DialogTitle'; -import MuiDialogContent from '@material-ui/core/DialogContent'; -import MuiDialogActions from '@material-ui/core/DialogActions'; -import IconButton from '@material-ui/core/IconButton'; -import CloseIcon from '@material-ui/icons/Close'; -import Typography from '@material-ui/core/Typography'; -import { TextField } from '@material-ui/core'; +import Dialog from "@material-ui/core/Dialog"; +import MuiDialogTitle from "@material-ui/core/DialogTitle"; +import MuiDialogContent from "@material-ui/core/DialogContent"; +import MuiDialogActions from "@material-ui/core/DialogActions"; +import IconButton from "@material-ui/core/IconButton"; +import CloseIcon from "@material-ui/icons/Close"; +import Typography from "@material-ui/core/Typography"; +import { TextField } from "@material-ui/core"; import * as Yup from "yup"; -import { Formik } from 'formik'; +import { Formik } from "formik"; const styles = (theme) => ({ - root: { - margin: 0, - padding: theme.spacing(2), - }, - closeButton: { - position: 'absolute', - right: theme.spacing(1), - top: theme.spacing(1), - color: theme.palette.grey[500], - }, + root: { + margin: 0, + padding: theme.spacing(2), + }, + closeButton: { + position: "absolute", + right: theme.spacing(1), + top: theme.spacing(1), + color: theme.palette.grey[500], + }, }); const DialogTitle = withStyles(styles)((props) => { - const { children, classes, onClose, ...other } = props; - return ( - <MuiDialogTitle disableTypography className={classes.root} {...other}> - <Typography variant="h6">{children}</Typography> - {onClose ? ( - <IconButton className={classes.closeButton} onClick={onClose}> - <CloseIcon /> - </IconButton> - ) : null} - </MuiDialogTitle> - ); + const { children, classes, onClose, ...other } = props; + return ( + <MuiDialogTitle disableTypography className={classes.root} {...other}> + <Typography variant="h6">{children}</Typography> + {onClose ? ( + <IconButton className={classes.closeButton} onClick={onClose}> + <CloseIcon /> + </IconButton> + ) : null} + </MuiDialogTitle> + ); }); const DialogActions = withStyles((theme) => ({ - root: { - margin: 0, - padding: theme.spacing(1), - }, + root: { + margin: 0, + padding: theme.spacing(1), + }, }))(MuiDialogActions); const DialogContent = withStyles((theme) => ({ - root: { - padding: theme.spacing(2), - } + root: { + padding: theme.spacing(2), + }, }))(MuiDialogContent); -const schema = Yup.object( - { - name: Yup.string().required(), - description: Yup.string(), - }) +const schema = Yup.object({ + name: Yup.string().required(), + description: Yup.string(), +}); const ClusterProviderForm = (props) => { - const { onClose, item, open, onSubmit } = props; - const buttonLabel = item ? "OK" : "Create" - const title = item ? "Edit Cluster Provider" : "Register Cluster Provider" - const handleClose = () => { - onClose(); - }; - let initialValues = item ? { name: item.metadata.name, description: item.metadata.description } : { name: "", description: "" } + const { onClose, item, open, onSubmit } = props; + const buttonLabel = item ? "OK" : "Create"; + const title = item ? "Edit Cluster Provider" : "Register Cluster Provider"; + const handleClose = () => { + onClose(); + }; + let initialValues = item + ? { name: item.metadata.name, description: item.metadata.description } + : { name: "", description: "" }; - return ( - <Dialog maxWidth={"xs"} onClose={handleClose} aria-labelledby="customized-dialog-title" open={open} disableBackdropClick> - <DialogTitle id="simple-dialog-title">{title}</DialogTitle> - <Formik - initialValues={initialValues} - onSubmit={async values => { - onSubmit(values); - }} - validationSchema={schema} - > - {props => { - const { - values, - touched, - errors, - isSubmitting, - handleChange, - handleBlur, - handleSubmit - } = props; - return ( - <form noValidate onSubmit={handleSubmit}> - <DialogContent dividers> - <TextField - style={{ width: "100%", marginBottom: "10px" }} - id="name" - label="Provider name" - type="text" - value={values.name} - onChange={handleChange} - onBlur={handleBlur} - helperText={(errors.name && touched.name && ( - "Name is required" - ))} - required - error={errors.name && touched.name} - /> - <TextField - style={{ width: "100%", marginBottom: "25px" }} - name="description" - value={values.description} - onChange={handleChange} - onBlur={handleBlur} - id="description" - label="Description" - multiline - rowsMax={4} - /> - </DialogContent> - <DialogActions> - <Button autoFocus onClick={handleClose} color="secondary"> - Cancel - </Button> - <Button autoFocus type="submit" color="primary" disabled={isSubmitting}> - {buttonLabel} - </Button> - </DialogActions> - </form> - ); - }} - </Formik> - </Dialog> - ); + return ( + <Dialog + maxWidth={"xs"} + onClose={handleClose} + aria-labelledby="customized-dialog-title" + open={open} + disableBackdropClick + > + <DialogTitle id="simple-dialog-title">{title}</DialogTitle> + <Formik + initialValues={initialValues} + onSubmit={async (values) => { + onSubmit(values); + }} + validationSchema={schema} + > + {(props) => { + const { + values, + touched, + errors, + isSubmitting, + handleChange, + handleBlur, + handleSubmit, + } = props; + return ( + <form noValidate onSubmit={handleSubmit}> + <DialogContent dividers> + <TextField + style={{ width: "100%", marginBottom: "10px" }} + id="name" + label="Provider name" + type="text" + value={values.name} + onChange={handleChange} + onBlur={handleBlur} + helperText={errors.name && touched.name && "Name is required"} + required + error={errors.name && touched.name} + /> + <TextField + style={{ width: "100%", marginBottom: "25px" }} + name="description" + value={values.description} + onChange={handleChange} + onBlur={handleBlur} + id="description" + label="Description" + multiline + rowsMax={4} + /> + </DialogContent> + <DialogActions> + <Button autoFocus onClick={handleClose} color="secondary"> + Cancel + </Button> + <Button + autoFocus + type="submit" + color="primary" + disabled={isSubmitting} + > + {buttonLabel} + </Button> + </DialogActions> + </form> + ); + }} + </Formik> + </Dialog> + ); }; ClusterProviderForm.propTypes = { - onClose: PropTypes.func.isRequired, - open: PropTypes.bool.isRequired, - item: PropTypes.object + onClose: PropTypes.func.isRequired, + open: PropTypes.bool.isRequired, + item: PropTypes.object, }; export default ClusterProviderForm; diff --git a/src/tools/emcoui/src/admin/clusterProvider/ClusterProvidersAccordian.jsx b/src/tools/emcoui/src/admin/clusterProvider/ClusterProvidersAccordian.jsx index 20317695..192992bc 100644 --- a/src/tools/emcoui/src/admin/clusterProvider/ClusterProvidersAccordian.jsx +++ b/src/tools/emcoui/src/admin/clusterProvider/ClusterProvidersAccordian.jsx @@ -11,7 +11,7 @@ // 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. -// ======================================================================== +// ======================================================================== import React, { useState } from "react"; import { makeStyles } from "@material-ui/core/styles"; import Accordion from "@material-ui/core/Accordion"; @@ -22,11 +22,13 @@ import ExpandMoreIcon from "@material-ui/icons/ExpandMore"; import apiService from "../../services/apiService"; import { Button } from "@material-ui/core"; import DeleteIcon from "@material-ui/icons/Delete"; -import EditIcon from "@material-ui/icons/Edit"; +// import EditIcon from "@material-ui/icons/Edit"; import ClusterForm from "./clusters/ClusterForm"; import ClustersTable from "./clusters/ClusterTable"; import DeleteDialog from "../../common/Dialogue"; -import ClusterProviderForm from "../clusterProvider/ClusterProviderForm"; +import Notification from "../../common/Notification"; + +//import ClusterProviderForm from "../clusterProvider/ClusterProviderForm"; const useStyles = makeStyles((theme) => ({ root: { @@ -47,8 +49,9 @@ export default function ControlledAccordions({ data, setData, ...props }) { const [expanded, setExpanded] = useState(false); const [open, setOpen] = React.useState(false); const [formOpen, setFormOpen] = useState(false); - const [openProviderForm, setOpenProviderForm] = useState(false); + // const [openProviderForm, setOpenProviderForm] = useState(false); const [selectedRowIndex, setSelectedRowIndex] = useState(0); + const [notificationDetails, setNotificationDetails] = useState({}); const handleAccordianOpen = (providerRow) => (event, isExpanded) => { if (!isExpanded) { setExpanded(isExpanded ? providerRow : false); @@ -141,7 +144,7 @@ export default function ControlledAccordions({ data, setData, ...props }) { setSelectedRowIndex(index); setOpen(true); }; - const handleSubmit = (values) => { + const handleSubmit = (values, setSubmitting) => { let metadata = {}; if (values.userData) { metadata = JSON.parse(values.userData); @@ -150,7 +153,6 @@ export default function ControlledAccordions({ data, setData, ...props }) { metadata.description = values.description; const formData = new FormData(); formData.append("file", values.file); - // `{"metadata":{ "name": "${values.name}", "description": "${values.description}" }}` formData.append("metadata", `{"metadata":${JSON.stringify(metadata)}}`); formData.append("providerName", data[selectedRowIndex].metadata.name); apiService @@ -161,12 +163,24 @@ export default function ControlledAccordions({ data, setData, ...props }) { ? (data[selectedRowIndex].clusters = [res]) : data[selectedRowIndex].clusters.push(res); setData([...data]); + setFormOpen(false); + setNotificationDetails({ + show: true, + message: `${values.name} cluster added`, + severity: "success", + }); }) .catch((err) => { - console.log("error adding cluster : ", err); - }) - .finally(() => { - setFormOpen(false); + debugger; + if (err.response.status === 403) { + setNotificationDetails({ + show: true, + message: `${err.response.data}`, + severity: "error", + }); + setSubmitting(false); + } + console.log("error adding cluster : " + err); }); }; const handleFormClose = () => { @@ -198,35 +212,36 @@ export default function ControlledAccordions({ data, setData, ...props }) { setOpen(false); setSelectedRowIndex(0); }; - const handleEdit = (index) => { - setSelectedRowIndex(index); - setOpenProviderForm(true); - }; - const handleCloseProviderForm = () => { - setOpenProviderForm(false); - }; - const handleSubmitProviderForm = (values) => { - let request = { - payload: { metatada: values }, - providerName: data[selectedRowIndex].metadata.name, - }; - apiService - .updateClusterProvider(request) - .then((res) => { - setData((data) => { - data[selectedRowIndex].metadata = res.metadata; - return data; - }); - }) - .catch((err) => { - console.log("error updating cluster provider. " + err); - }) - .finally(() => { - setOpenProviderForm(false); - }); - }; + // const handleEdit = (index) => { + // setSelectedRowIndex(index); + // setOpenProviderForm(true); + // }; + // const handleCloseProviderForm = () => { + // setOpenProviderForm(false); + // }; + // const handleSubmitProviderForm = (values) => { + // let request = { + // payload: { metatada: values }, + // providerName: data[selectedRowIndex].metadata.name, + // }; + // apiService + // .updateClusterProvider(request) + // .then((res) => { + // setData((data) => { + // data[selectedRowIndex].metadata = res.metadata; + // return data; + // }); + // }) + // .catch((err) => { + // console.log("error updating cluster provider. " + err); + // }) + // .finally(() => { + // setOpenProviderForm(false); + // }); + // }; return ( <> + <Notification notificationDetails={notificationDetails} /> {data && data.length > 0 && ( <div className={classes.root}> <ClusterForm @@ -234,12 +249,12 @@ export default function ControlledAccordions({ data, setData, ...props }) { onClose={handleFormClose} onSubmit={handleSubmit} /> - <ClusterProviderForm + {/* <ClusterProviderForm open={openProviderForm} onClose={handleCloseProviderForm} onSubmit={handleSubmitProviderForm} item={data[selectedRowIndex]} - /> + /> */} <DeleteDialog open={open} onClose={handleClose} @@ -288,6 +303,8 @@ export default function ControlledAccordions({ data, setData, ...props }) { > Delete Provider </Button> + {/* + //edit cluster provider is not supported by the api yet <Button variant="outlined" size="small" @@ -299,7 +316,7 @@ export default function ControlledAccordions({ data, setData, ...props }) { }} > Edit Provider - </Button> + </Button> */} </div> <AccordionDetails> {item.clusters && ( diff --git a/src/tools/emcoui/src/admin/clusterProvider/clusters/ClusterForm.jsx b/src/tools/emcoui/src/admin/clusterProvider/clusters/ClusterForm.jsx index 6d9fc83b..6c49cb85 100644 --- a/src/tools/emcoui/src/admin/clusterProvider/clusters/ClusterForm.jsx +++ b/src/tools/emcoui/src/admin/clusterProvider/clusters/ClusterForm.jsx @@ -11,7 +11,7 @@ // 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. -// ======================================================================== +// ======================================================================== import React from "react"; import PropTypes from "prop-types"; import { withStyles } from "@material-ui/core/styles"; @@ -113,8 +113,8 @@ const ClusterForm = (props) => { <DialogTitle id="simple-dialog-title">{title}</DialogTitle> <Formik initialValues={initialValues} - onSubmit={async (values) => { - onSubmit(values); + onSubmit={(values, actions) => { + onSubmit(values, actions.setSubmitting); }} validationSchema={schema} > diff --git a/src/tools/emcoui/src/admin/clusterProvider/clusters/ClusterTable.jsx b/src/tools/emcoui/src/admin/clusterProvider/clusters/ClusterTable.jsx index 1066d472..26bc1ca9 100644 --- a/src/tools/emcoui/src/admin/clusterProvider/clusters/ClusterTable.jsx +++ b/src/tools/emcoui/src/admin/clusterProvider/clusters/ClusterTable.jsx @@ -11,309 +11,528 @@ // 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. -// ======================================================================== -import React, { useState } from 'react'; -import PropTypes from 'prop-types'; -import AddIconOutline from '@material-ui/icons/AddCircleOutline'; -import AddIcon from '@material-ui/icons/Add'; -import Table from '@material-ui/core/Table'; -import TableBody from '@material-ui/core/TableBody'; -import TableCell from '@material-ui/core/TableCell'; -import TableContainer from '@material-ui/core/TableContainer'; -import TableHead from '@material-ui/core/TableHead'; -import TableRow from '@material-ui/core/TableRow'; -import IconButton from '@material-ui/core/IconButton'; -import EditIcon from '@material-ui/icons/Edit'; -import Chip from '@material-ui/core/Chip'; -import SettingsEthernetIcon from '@material-ui/icons/SettingsEthernet'; -import DeleteIcon from '@material-ui/icons/Delete'; -import { makeStyles, TextField, Button } from '@material-ui/core'; +// ======================================================================== +import React, { useState } from "react"; +import PropTypes from "prop-types"; +import AddIconOutline from "@material-ui/icons/AddCircleOutline"; +import AddIcon from "@material-ui/icons/Add"; +import Table from "@material-ui/core/Table"; +import TableBody from "@material-ui/core/TableBody"; +import TableCell from "@material-ui/core/TableCell"; +import TableContainer from "@material-ui/core/TableContainer"; +import TableHead from "@material-ui/core/TableHead"; +import TableRow from "@material-ui/core/TableRow"; +import IconButton from "@material-ui/core/IconButton"; +// import EditIcon from "@material-ui/icons/Edit"; +import Chip from "@material-ui/core/Chip"; +import SettingsEthernetIcon from "@material-ui/icons/SettingsEthernet"; +import DeleteIcon from "@material-ui/icons/Delete"; +import { makeStyles, TextField, Button } from "@material-ui/core"; import NetworkForm from "../networks/NetworkForm"; import apiService from "../../../services/apiService"; import DeleteDialog from "../../../common/Dialogue"; -import CancelOutlinedIcon from '@material-ui/icons/CancelOutlined'; -import CheckIcon from '@material-ui/icons/CheckCircleOutlineOutlined'; -import InfoOutlinedIcon from '@material-ui/icons/InfoOutlined'; +import CancelOutlinedIcon from "@material-ui/icons/CancelOutlined"; +import CheckIcon from "@material-ui/icons/CheckCircleOutlineOutlined"; +import InfoOutlinedIcon from "@material-ui/icons/InfoOutlined"; import NetworkDetailsDialog from "../../../common/DetailsDialog"; -import DoneOutlineIcon from '@material-ui/icons/DoneOutline'; +import DoneOutlineIcon from "@material-ui/icons/DoneOutline"; import ClusterForm from "../clusters/ClusterForm"; +import Notification from "../../../common/Notification"; const useStyles = makeStyles((theme) => ({ - root: { - width: '100%', - }, - heading: { - fontSize: theme.typography.pxToRem(15), - flexBasis: '33.33%', - flexShrink: 0, - }, - secondaryHeading: { - fontSize: theme.typography.pxToRem(15), - color: theme.palette.text.secondary, - }, + root: { + width: "100%", + }, + heading: { + fontSize: theme.typography.pxToRem(15), + flexBasis: "33.33%", + flexShrink: 0, + }, + secondaryHeading: { + fontSize: theme.typography.pxToRem(15), + color: theme.palette.text.secondary, + }, })); const ClusterTable = ({ clustersData, ...props }) => { - const classes = useStyles(); - const [formOpen, setformOpen] = useState(false); - const [networkDetailsOpen, setNetworkDetailsOpen] = useState(false); - const [network, setNetwork] = useState({}); - const [activeRowIndex, setActiveRowIndex] = useState(0); - const [activeNetwork, setActiveNetwork] = useState({}); - const [open, setOpen] = useState(false); - const [openDeleteNetwork, setOpenDeleteNetwork] = useState(false); - const [showAddLabel, setShowAddLabel] = useState(false); - const [labelInput, setLabelInput] = React.useState(""); - const [clusterFormOpen, setClusterFormOpen] = useState(false); - const handleFormClose = () => { + const classes = useStyles(); + const [formOpen, setformOpen] = useState(false); + const [networkDetailsOpen, setNetworkDetailsOpen] = useState(false); + const [network, setNetwork] = useState({}); + const [activeRowIndex, setActiveRowIndex] = useState(0); + const [activeNetwork, setActiveNetwork] = useState({}); + const [open, setOpen] = useState(false); + const [openDeleteNetwork, setOpenDeleteNetwork] = useState(false); + const [showAddLabel, setShowAddLabel] = useState(false); + const [labelInput, setLabelInput] = useState(""); + // const [clusterFormOpen, setClusterFormOpen] = useState(false); + const [notificationDetails, setNotificationDetails] = useState({}); + const handleFormClose = () => { + setformOpen(false); + }; + const handleSubmit = (data) => { + let networkSpec = JSON.parse(data.spec); + let payload = { + metadata: { name: data.name, description: data.description }, + spec: networkSpec, + }; + let request = { + providerName: props.providerName, + clusterName: clustersData[activeRowIndex].metadata.name, + networkType: data.type, + payload: payload, + }; + apiService + .addNetwork(request) + .then((res) => { + let networkType = + data.type === "networks" ? "networks" : "providerNetworks"; + !clustersData[activeRowIndex][networkType] || + clustersData[activeRowIndex][networkType] === null + ? (clustersData[activeRowIndex][networkType] = [res]) + : clustersData[activeRowIndex][networkType].push(res); + }) + .catch((err) => { + console.log("error adding cluster network : ", err); + }) + .finally(() => { + setActiveRowIndex(0); setformOpen(false); - } - const handleSubmit = (data) => { - let networkSpec = JSON.parse(data.spec); - let payload = { metadata: { name: data.name, description: data.description }, spec: networkSpec }; - let request = { providerName: props.providerName, clusterName: clustersData[activeRowIndex].metadata.name, networkType: data.type, payload: payload }; - apiService.addNetwork(request).then(res => { - let networkType = (data.type === "networks" ? "networks" : "providerNetworks"); - (!clustersData[activeRowIndex][networkType] || clustersData[activeRowIndex][networkType] === null) ? (clustersData[activeRowIndex][networkType] = [res]) : clustersData[activeRowIndex][networkType].push(res); - }).catch(err => { - console.log("error adding cluster network : ", err) - }).finally(() => { - setActiveRowIndex(0); - setformOpen(false); + }); + }; + const handleAddNetwork = (index) => { + setActiveRowIndex(index); + setformOpen(true); + }; + const handleDeleteLabel = (index, label, labelIndex) => { + let request = { + providerName: props.providerName, + clusterName: clustersData[index].metadata.name, + labelName: label, + }; + apiService + .deleteClusterLabel(request) + .then((res) => { + console.log("label deleted"); + clustersData[index].labels.splice(labelIndex, 1); + props.onUpdateCluster(props.parentIndex, clustersData); + }) + .catch((err) => { + console.log("error deleting label : ", err); + }); + }; + const handleClose = (el) => { + if (el.target.innerText === "Delete") { + let request = { + providerName: props.providerName, + clusterName: clustersData[activeRowIndex].metadata.name, + }; + apiService + .deleteCluster(request) + .then(() => { + console.log("cluster deleted"); + props.onDeleteCluster(props.parentIndex, activeRowIndex); + }) + .catch((err) => { + console.log("Error deleting cluster : ", +err); + setNotificationDetails({ + show: true, + message: "Unable to remove cluster", + severity: "error", + }); }); } - const handleAddNetwork = (index) => { - setActiveRowIndex(index); - setformOpen(true); - } - const handleDeleteLabel = (index, label, labelIndex) => { - let request = { providerName: props.providerName, clusterName: clustersData[index].metadata.name, labelName: label } - apiService.deleteClusterLabel(request).then(res => { - console.log("label deleted"); - clustersData[index].labels.splice(labelIndex, 1); - props.onUpdateCluster(props.parentIndex, clustersData); - }).catch(err => { console.log("error deleting label : ", err) }) - } - const handleClose = el => { - if (el.target.innerText === "Delete") { - let request = { providerName: props.providerName, clusterName: clustersData[activeRowIndex].metadata.name }; - apiService.deleteCluster(request).then(() => { - console.log("cluster deleted"); - props.onDeleteCluster(props.parentIndex, activeRowIndex); - }).catch(err => { - console.log("Error deleting cluster : ", err) - }) - } - setOpen(false); - setActiveRowIndex(0); - }; + setOpen(false); + setActiveRowIndex(0); + }; - const handleCloseDeleteNetwork = (el) => { - if (el.target.innerText === "Delete") { - let networkName = clustersData[activeRowIndex][activeNetwork.networkType][activeNetwork.networkIndex].metadata.name; - let networkType = (activeNetwork.networkType === "providerNetworks" ? "provider-networks" : "networks"); - let request = { providerName: props.providerName, clusterName: clustersData[activeRowIndex].metadata.name, networkType: networkType, networkName: networkName }; - apiService.deleteClusterNetwork(request).then(() => { - console.log("cluster network deleted"); - clustersData[activeRowIndex][activeNetwork.networkType].splice(activeNetwork.networkIndex, 1); - }).catch(err => { - console.log("Error deleting cluster network : ", err) - }).finally(() => { setActiveRowIndex(0); setActiveNetwork({}); }) - } - setOpenDeleteNetwork(false); - } - const handleDeleteCluster = (index) => { - setActiveRowIndex(index); - setOpen(true); - } - const handleAddLabel = (index) => { - if (labelInput !== "") { - let request = { providerName: props.providerName, clusterName: clustersData[activeRowIndex].metadata.name, payload: { "label-name": labelInput } }; - apiService.addClusterLabel(request) - .then(res => { - (!clustersData[index].labels || clustersData[index].labels === null) ? (clustersData[index].labels = [res]) : clustersData[index].labels.push(res); - }) - .catch(err => { console.log("error adding label", err) }) - .finally(() => { - setShowAddLabel(!showAddLabel); - }) - } + const handleCloseDeleteNetwork = (el) => { + if (el.target.innerText === "Delete") { + let networkName = + clustersData[activeRowIndex][activeNetwork.networkType][ + activeNetwork.networkIndex + ].metadata.name; + let networkType = + activeNetwork.networkType === "providerNetworks" + ? "provider-networks" + : "networks"; + let request = { + providerName: props.providerName, + clusterName: clustersData[activeRowIndex].metadata.name, + networkType: networkType, + networkName: networkName, + }; + apiService + .deleteClusterNetwork(request) + .then(() => { + console.log("cluster network deleted"); + clustersData[activeRowIndex][activeNetwork.networkType].splice( + activeNetwork.networkIndex, + 1 + ); + }) + .catch((err) => { + console.log("Error deleting cluster network : ", err); + }) + .finally(() => { + setActiveRowIndex(0); + setActiveNetwork({}); + }); } - - const handleToggleAddLabel = (index) => { - setShowAddLabel(showAddLabel === index ? false : index); - setActiveRowIndex(index); - setLabelInput(''); + setOpenDeleteNetwork(false); + }; + const handleDeleteCluster = (index) => { + setActiveRowIndex(index); + setOpen(true); + }; + const handleAddLabel = (index) => { + if (labelInput !== "") { + let request = { + providerName: props.providerName, + clusterName: clustersData[activeRowIndex].metadata.name, + payload: { "label-name": labelInput }, + }; + apiService + .addClusterLabel(request) + .then((res) => { + !clustersData[index].labels || clustersData[index].labels === null + ? (clustersData[index].labels = [res]) + : clustersData[index].labels.push(res); + }) + .catch((err) => { + console.log("error adding label", err); + }) + .finally(() => { + setShowAddLabel(!showAddLabel); + }); } - const handleLabelInputChange = (event) => { - setLabelInput(event.target.value); - }; + }; - const handleNetworkDetailOpen = (network) => { - setNetwork(network); - setNetworkDetailsOpen(true); - } - const handleDeleteNetwork = (index, networkIndex, networkType, networkName) => { - setActiveNetwork({ networkIndex: networkIndex, networkType: networkType, name: networkName }); - setActiveRowIndex(index); - setOpenDeleteNetwork(true); - } - const applyNetworkConfig = (clusterName) => { - let request = { providerName: props.providerName, clusterName: clusterName } - apiService.applyNetworkConfig(request) - .then(res => { - console.log("Network config applied"); - }) - .catch(err => { - console.log("Error applying network config : ", err); - if (err.response) - console.log("Network config applied" + err.response.data); - else - console.log("Network config applied" + err); - }); - } - const handleClusterFormClose = () => { - setClusterFormOpen(false); - } - const handleClusterSubmit = (values) => { - const formData = new FormData(); - if (values.file) - formData.append('file', values.file); - formData.append("metadata", `{"metadata":{ "name": "${values.name}", "description": "${values.description}" }}`); - formData.append("providerName", props.providerName); - apiService.updateCluster(formData) - .then(res => { - clustersData[activeRowIndex].metadata = res.metadata; - props.onUpdateCluster(props.parentIndex, clustersData); - }) - .catch(err => { console.log("error updating cluster : ", err) }) - .finally(() => { handleClusterFormClose() }); + const handleToggleAddLabel = (index) => { + setShowAddLabel(showAddLabel === index ? false : index); + setActiveRowIndex(index); + setLabelInput(""); + }; + const handleLabelInputChange = (event) => { + setLabelInput(event.target.value); + }; - } - const handleEditCluster = (index) => { - setActiveRowIndex(index); - setClusterFormOpen(true); - } - return ( + const handleNetworkDetailOpen = (network) => { + setNetwork(network); + setNetworkDetailsOpen(true); + }; + const handleDeleteNetwork = ( + index, + networkIndex, + networkType, + networkName + ) => { + setActiveNetwork({ + networkIndex: networkIndex, + networkType: networkType, + name: networkName, + }); + setActiveRowIndex(index); + setOpenDeleteNetwork(true); + }; + const applyNetworkConfig = (clusterName) => { + let request = { + providerName: props.providerName, + clusterName: clusterName, + }; + apiService + .applyNetworkConfig(request) + .then((res) => { + setNotificationDetails({ + show: true, + message: "Network configuration applied", + severity: "success", + }); + console.log("Network config applied"); + }) + .catch((err) => { + setNotificationDetails({ + show: true, + message: "Error applying network configuration", + severity: "error", + }); + console.log("Error applying network config : ", err); + if (err.response) + console.log("Network config applied" + err.response.data); + else console.log("Network config applied" + err); + }); + }; + // const handleClusterFormClose = () => { + // setClusterFormOpen(false); + // }; + // const handleClusterSubmit = (values) => { + // const formData = new FormData(); + // if (values.file) formData.append("file", values.file); + // formData.append( + // "metadata", + // `{"metadata":{ "name": "${values.name}", "description": "${values.description}" }}` + // ); + // formData.append("providerName", props.providerName); + // apiService + // .updateCluster(formData) + // .then((res) => { + // clustersData[activeRowIndex].metadata = res.metadata; + // props.onUpdateCluster(props.parentIndex, clustersData); + // }) + // .catch((err) => { + // console.log("error updating cluster : ", err); + // }) + // .finally(() => { + // handleClusterFormClose(); + // }); + // }; + //disabling as edit is not supported yet by the api yet + // const handleEditCluster = (index) => { + // setActiveRowIndex(index); + // setClusterFormOpen(true); + // }; + return ( + <> + <Notification notificationDetails={notificationDetails} /> + {clustersData && clustersData.length > 0 && ( <> - {clustersData && (clustersData.length > 0) && - (<> - <ClusterForm item={clustersData[activeRowIndex]} open={clusterFormOpen} onClose={handleClusterFormClose} onSubmit={handleClusterSubmit} /> - <NetworkDetailsDialog onClose={setNetworkDetailsOpen} open={networkDetailsOpen} item={network} type="Network" /> - <NetworkForm onClose={handleFormClose} onSubmit={handleSubmit} open={formOpen} /> - <DeleteDialog open={open} onClose={handleClose} title={"Delete Cluster"} - content={`Are you sure you want to delete "${clustersData[activeRowIndex] ? clustersData[activeRowIndex].metadata.name : ""}" ?`} /> - <DeleteDialog open={openDeleteNetwork} onClose={handleCloseDeleteNetwork} title={"Delete Network"} content={`Are you sure you want to delete "${activeNetwork.name}" ?`} /> - <TableContainer > - <Table className={classes.table}> - <TableHead> - <TableRow> - <TableCell style={{ width: "10%" }}>Name</TableCell> - <TableCell style={{ width: "15%" }}>Description</TableCell> - <TableCell style={{ width: "20%" }}>Networks </TableCell> - <TableCell style={{ width: "35%" }}>Labels </TableCell> - <TableCell style={{ width: "20%" }}>Actions</TableCell> - </TableRow> - </TableHead> - <TableBody> - {clustersData.map((row, index) => ( - <TableRow key={row.metadata.name + "" + index}> - <TableCell >{row.metadata.name}</TableCell> - <TableCell >{row.metadata.description}</TableCell> - <TableCell> - <div> - {row.providerNetworks && (row.providerNetworks.length > 0) && row.providerNetworks.map((providerNetwork, providerNetworkIndex) => - (<Chip - key={providerNetwork.metadata.name + "" + providerNetworkIndex} - size="small" - icon={<InfoOutlinedIcon onClick={() => { handleNetworkDetailOpen(providerNetwork) }} style={{ cursor: "pointer" }} />} - onDelete={(e) => { handleDeleteNetwork(index, providerNetworkIndex, "providerNetworks", providerNetwork.metadata.name) }} - label={providerNetwork.metadata.name} - style={{ marginRight: "10px", marginBottom: "5px" }} - />) - )} + {/* <ClusterForm + item={clustersData[activeRowIndex]} + open={clusterFormOpen} + onClose={handleClusterFormClose} + onSubmit={handleClusterSubmit} + /> */} + <NetworkDetailsDialog + onClose={setNetworkDetailsOpen} + open={networkDetailsOpen} + item={network} + type="Network" + /> + <NetworkForm + onClose={handleFormClose} + onSubmit={handleSubmit} + open={formOpen} + /> + <DeleteDialog + open={open} + onClose={handleClose} + title={"Delete Cluster"} + content={`Are you sure you want to delete "${ + clustersData[activeRowIndex] + ? clustersData[activeRowIndex].metadata.name + : "" + }" ?`} + /> + <DeleteDialog + open={openDeleteNetwork} + onClose={handleCloseDeleteNetwork} + title={"Delete Network"} + content={`Are you sure you want to delete "${activeNetwork.name}" ?`} + /> + <TableContainer> + <Table className={classes.table}> + <TableHead> + <TableRow> + <TableCell style={{ width: "10%" }}>Name</TableCell> + <TableCell style={{ width: "15%" }}>Description</TableCell> + <TableCell style={{ width: "20%" }}>Networks </TableCell> + <TableCell style={{ width: "35%" }}>Labels </TableCell> + <TableCell style={{ width: "20%" }}>Actions</TableCell> + </TableRow> + </TableHead> + <TableBody> + {clustersData.map((row, index) => ( + <TableRow key={row.metadata.name + "" + index}> + <TableCell>{row.metadata.name}</TableCell> + <TableCell>{row.metadata.description}</TableCell> + <TableCell> + <div> + {row.providerNetworks && + row.providerNetworks.length > 0 && + row.providerNetworks.map( + (providerNetwork, providerNetworkIndex) => ( + <Chip + key={ + providerNetwork.metadata.name + + "" + + providerNetworkIndex + } + size="small" + icon={ + <InfoOutlinedIcon + onClick={() => { + handleNetworkDetailOpen(providerNetwork); + }} + style={{ cursor: "pointer" }} + /> + } + onDelete={(e) => { + handleDeleteNetwork( + index, + providerNetworkIndex, + "providerNetworks", + providerNetwork.metadata.name + ); + }} + label={providerNetwork.metadata.name} + style={{ + marginRight: "10px", + marginBottom: "5px", + }} + /> + ) + )} - {row.networks && (row.networks.length > 0) && row.networks.map((network, networkIndex) => - (<Chip - key={network.metadata.name + "" + networkIndex} - size="small" - icon={<InfoOutlinedIcon onClick={() => { handleNetworkDetailOpen(network) }} style={{ cursor: "pointer" }} />} - onDelete={(e) => { handleDeleteNetwork(index, networkIndex, "networks", network.metadata.name) }} - label={network.metadata.name} - style={{ marginRight: "10px", marginBottom: "5px" }} - color="secondary" - />) - )} - </div> - </TableCell> - <TableCell> - {row.labels && (row.labels.length > 0) && row.labels.map((label, labelIndex) => - (<Chip - key={label["label-name"] + "" + labelIndex} - size="small" - icon={<SettingsEthernetIcon />} - label={label["label-name"]} - onDelete={(e) => { handleDeleteLabel(index, label["label-name"], labelIndex) }} - color="primary" - style={{ marginRight: "10px" }} - />) - )} - {(showAddLabel === index) && - <TextField - style={{ height: "24px" }} - size="small" - value={labelInput} - onChange={handleLabelInputChange} - id="outlined-basic" label="Add label" variant="outlined" /> - } - {(showAddLabel === index) && - <IconButton color="primary" onClick={() => { handleAddLabel(index) }}> - <CheckIcon /> - </IconButton> - } - <IconButton color="primary" onClick={() => { handleToggleAddLabel(index) }}> - {!(showAddLabel === index) && <AddIconOutline />} - {(showAddLabel === index) && <CancelOutlinedIcon color="secondary" />} - </IconButton> - </TableCell> - <TableCell> - <Button - variant="outlined" - startIcon={<AddIcon />} - size="small" - color="primary" - title="Add Network" - onClick={() => { handleAddNetwork(index) }}> - Network - </Button> - <IconButton - style={{ color: "green" }} - onClick={() => { applyNetworkConfig(row.metadata.name) }} - title="Apply Network Configuration"> - <DoneOutlineIcon /> - </IconButton> - <IconButton - title="Edit" - onClick={() => { handleEditCluster(index) }} - color="primary"> - <EditIcon /> - </IconButton> - <IconButton - title="Delete" - color="secondary" - onClick={() => { handleDeleteCluster(index) }}> - <DeleteIcon /> - </IconButton> - </TableCell> - </TableRow>))} - </TableBody> - </Table> - </TableContainer> - </>)} - {(!clustersData || (clustersData.length === 0)) && (<span>No Clusters</span>)} - </>) -} + {row.networks && + row.networks.length > 0 && + row.networks.map((network, networkIndex) => ( + <Chip + key={network.metadata.name + "" + networkIndex} + size="small" + icon={ + <InfoOutlinedIcon + onClick={() => { + handleNetworkDetailOpen(network); + }} + style={{ cursor: "pointer" }} + /> + } + onDelete={(e) => { + handleDeleteNetwork( + index, + networkIndex, + "networks", + network.metadata.name + ); + }} + label={network.metadata.name} + style={{ + marginRight: "10px", + marginBottom: "5px", + }} + color="secondary" + /> + ))} + </div> + </TableCell> + <TableCell> + {row.labels && + row.labels.length > 0 && + row.labels.map((label, labelIndex) => ( + <Chip + key={label["label-name"] + "" + labelIndex} + size="small" + icon={<SettingsEthernetIcon />} + label={label["label-name"]} + onDelete={(e) => { + handleDeleteLabel( + index, + label["label-name"], + labelIndex + ); + }} + color="primary" + style={{ marginRight: "10px" }} + /> + ))} + {showAddLabel === index && ( + <TextField + style={{ height: "24px" }} + size="small" + value={labelInput} + onChange={handleLabelInputChange} + id="outlined-basic" + label="Add label" + variant="outlined" + /> + )} + {showAddLabel === index && ( + <IconButton + color="primary" + onClick={() => { + handleAddLabel(index); + }} + > + <CheckIcon /> + </IconButton> + )} + <IconButton + color="primary" + onClick={() => { + handleToggleAddLabel(index); + }} + > + {!(showAddLabel === index) && <AddIconOutline />} + {showAddLabel === index && ( + <CancelOutlinedIcon color="secondary" /> + )} + </IconButton> + </TableCell> + <TableCell> + <Button + variant="outlined" + startIcon={<AddIcon />} + size="small" + color="primary" + title="Add Network" + onClick={() => { + handleAddNetwork(index); + }} + > + Network + </Button> + <IconButton + color="primary" + disabled={ + !( + (row.networks && row.networks.length > 0) || + (row.providerNetworks && + row.providerNetworks.length > 0) + ) + } + onClick={() => { + applyNetworkConfig(row.metadata.name); + }} + title="Apply Network Configuration" + > + <DoneOutlineIcon /> + </IconButton> + {/* + //disabling as edit is not supported yet by the api yet + <IconButton + title="Edit" + onClick={() => { handleEditCluster(index) }} + color="primary"> + <EditIcon /> + </IconButton> */} + <IconButton + title="Delete" + color="secondary" + disabled={ + (row.networks && row.networks.length > 0) || + (row.providerNetworks && + row.providerNetworks.length > 0) || + (row.labels && row.labels.length > 0) + } + onClick={() => { + handleDeleteCluster(index); + }} + > + <DeleteIcon /> + </IconButton> + </TableCell> + </TableRow> + ))} + </TableBody> + </Table> + </TableContainer> + </> + )} + {(!clustersData || clustersData.length === 0) && <span>No Clusters</span>} + </> + ); +}; ClusterTable.propTypes = { - clusters: PropTypes.arrayOf(PropTypes.object) + clusters: PropTypes.arrayOf(PropTypes.object), }; export default ClusterTable; diff --git a/src/tools/emcoui/src/admin/controllers/Controllers.jsx b/src/tools/emcoui/src/admin/controllers/Controllers.jsx index 4a8a502c..4316f6ea 100644 --- a/src/tools/emcoui/src/admin/controllers/Controllers.jsx +++ b/src/tools/emcoui/src/admin/controllers/Controllers.jsx @@ -29,7 +29,8 @@ function Controllers() { apiService .getControllers() .then((res) => { - setControllersData(res); + if (res && res.length > 0) setControllersData(res); + else setControllersData([]); }) .catch((err) => { console.log("error getting controllers : " + err); @@ -53,9 +54,7 @@ function Controllers() { .addController(request) .then((res) => { setControllersData((controllersData) => { - if (controllersData && controllersData.length > 0) - return [...controllersData, res]; - else return [res]; + return [...controllersData, res]; }); }) .catch((err) => { diff --git a/src/tools/emcoui/src/admin/projects/ProjectForm.jsx b/src/tools/emcoui/src/admin/projects/ProjectForm.jsx index 751de5d0..4ea87b2c 100644 --- a/src/tools/emcoui/src/admin/projects/ProjectForm.jsx +++ b/src/tools/emcoui/src/admin/projects/ProjectForm.jsx @@ -11,146 +11,157 @@ // 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. -// ======================================================================== -import React from 'react'; -import PropTypes from 'prop-types'; -import { withStyles } from '@material-ui/core/styles'; -import Button from '@material-ui/core/Button'; +// ======================================================================== +import React from "react"; +import PropTypes from "prop-types"; +import { withStyles } from "@material-ui/core/styles"; +import Button from "@material-ui/core/Button"; -import Dialog from '@material-ui/core/Dialog'; -import MuiDialogTitle from '@material-ui/core/DialogTitle'; -import MuiDialogContent from '@material-ui/core/DialogContent'; -import MuiDialogActions from '@material-ui/core/DialogActions'; -import IconButton from '@material-ui/core/IconButton'; -import CloseIcon from '@material-ui/icons/Close'; -import Typography from '@material-ui/core/Typography'; -import { TextField } from '@material-ui/core'; +import Dialog from "@material-ui/core/Dialog"; +import MuiDialogTitle from "@material-ui/core/DialogTitle"; +import MuiDialogContent from "@material-ui/core/DialogContent"; +import MuiDialogActions from "@material-ui/core/DialogActions"; +import IconButton from "@material-ui/core/IconButton"; +import CloseIcon from "@material-ui/icons/Close"; +import Typography from "@material-ui/core/Typography"; +import { TextField } from "@material-ui/core"; import * as Yup from "yup"; -import { Formik } from 'formik'; +import { Formik } from "formik"; const styles = (theme) => ({ - root: { - margin: 0, - padding: theme.spacing(2), - }, - closeButton: { - position: 'absolute', - right: theme.spacing(1), - top: theme.spacing(1), - color: theme.palette.grey[500], - }, + root: { + margin: 0, + padding: theme.spacing(2), + }, + closeButton: { + position: "absolute", + right: theme.spacing(1), + top: theme.spacing(1), + color: theme.palette.grey[500], + }, }); const DialogTitle = withStyles(styles)((props) => { - const { children, classes, onClose, ...other } = props; - return ( - <MuiDialogTitle disableTypography className={classes.root} {...other}> - <Typography variant="h6">{children}</Typography> - {onClose ? ( - <IconButton className={classes.closeButton} onClick={onClose}> - <CloseIcon /> - </IconButton> - ) : null} - </MuiDialogTitle> - ); + const { children, classes, onClose, ...other } = props; + return ( + <MuiDialogTitle disableTypography className={classes.root} {...other}> + <Typography variant="h6">{children}</Typography> + {onClose ? ( + <IconButton className={classes.closeButton} onClick={onClose}> + <CloseIcon /> + </IconButton> + ) : null} + </MuiDialogTitle> + ); }); const DialogActions = withStyles((theme) => ({ - root: { - margin: 0, - padding: theme.spacing(1), - }, + root: { + margin: 0, + padding: theme.spacing(1), + }, }))(MuiDialogActions); const DialogContent = withStyles((theme) => ({ - root: { - padding: theme.spacing(2), - } + root: { + padding: theme.spacing(2), + }, }))(MuiDialogContent); -const schema = Yup.object( - { - name: Yup.string().required(), - description: Yup.string(), - }) +const schema = Yup.object({ + name: Yup.string().required(), + description: Yup.string(), +}); const ProjectFormFunc = (props) => { - const { onClose, item, open, onSubmit } = props; - const buttonLabel = item ? "OK" : "Create" - const title = item ? "Edit Project" : "Create Project" - const handleClose = () => { - onClose(); - }; - let initialValues = item ? { name: item.metadata.name, description: item.metadata.description } : { name: "", description: "" } + const { onClose, item, open, onSubmit } = props; + const buttonLabel = item ? "OK" : "Create"; + const title = item ? "Edit Project" : "Create Project"; + const handleClose = () => { + onClose(); + }; + let initialValues = item + ? { name: item.metadata.name, description: item.metadata.description } + : { name: "", description: "" }; - return ( - <Dialog maxWidth={"xs"} onClose={handleClose} aria-labelledby="customized-dialog-title" open={open} disableBackdropClick> - <DialogTitle id="simple-dialog-title">{title}</DialogTitle> - <Formik - initialValues={initialValues} - onSubmit={async values => { - onSubmit(values); - }} - validationSchema={schema} - > - {props => { - const { - values, - touched, - errors, - isSubmitting, - handleChange, - handleBlur, - handleSubmit - } = props; - return ( - <form noValidate onSubmit={handleSubmit}> - <DialogContent dividers> - <TextField - style={{ width: "100%", marginBottom: "10px" }} - id="name" - label="Project name" - type="text" - value={values.name} - onChange={handleChange} - onBlur={handleBlur} - helperText={(errors.name && touched.name && ( - "Name is required" - ))} - required - error={errors.name && touched.name} - /> - <TextField - style={{ width: "100%", marginBottom: "25px" }} - name="description" - value={values.description} - onChange={handleChange} - onBlur={handleBlur} - id="description" - label="Description" - multiline - rowsMax={4} - /> - </DialogContent> - <DialogActions> - <Button autoFocus onClick={handleClose} color="secondary"> - Cancel - </Button> - <Button autoFocus type="submit" color="primary" disabled={isSubmitting}> - {buttonLabel} - </Button> - </DialogActions> - </form> - ); - }} - </Formik> - </Dialog> - ); + return ( + <Dialog + maxWidth={"xs"} + onClose={handleClose} + aria-labelledby="customized-dialog-title" + open={open} + disableBackdropClick + > + <DialogTitle id="simple-dialog-title">{title}</DialogTitle> + <Formik + initialValues={initialValues} + onSubmit={async (values) => { + onSubmit(values); + }} + validationSchema={schema} + > + {(props) => { + const { + values, + touched, + errors, + isSubmitting, + handleChange, + handleBlur, + handleSubmit, + } = props; + return ( + <form noValidate onSubmit={handleSubmit}> + <DialogContent dividers> + <TextField + style={{ width: "100%", marginBottom: "10px" }} + id="name" + label="Project name" + type="text" + value={values.name} + onChange={handleChange} + onBlur={handleBlur} + helperText={errors.name && touched.name && "Name is required"} + required + disabled={item} + error={errors.name && touched.name} + /> + <TextField + style={{ width: "100%", marginBottom: "25px" }} + name="description" + value={values.description} + onChange={handleChange} + onBlur={handleBlur} + id="description" + label="Description" + multiline + rowsMax={4} + /> + </DialogContent> + <DialogActions> + <Button autoFocus onClick={handleClose} color="secondary"> + Cancel + </Button> + <Button + autoFocus + type="submit" + color="primary" + disabled={isSubmitting} + > + {buttonLabel} + </Button> + </DialogActions> + </form> + ); + }} + </Formik> + </Dialog> + ); }; ProjectFormFunc.propTypes = { - onClose: PropTypes.func.isRequired, - open: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired, + open: PropTypes.bool.isRequired, }; export default ProjectFormFunc; diff --git a/src/tools/emcoui/src/admin/projects/ProjectsTable.jsx b/src/tools/emcoui/src/admin/projects/ProjectsTable.jsx index d96d44fa..fb03155d 100644 --- a/src/tools/emcoui/src/admin/projects/ProjectsTable.jsx +++ b/src/tools/emcoui/src/admin/projects/ProjectsTable.jsx @@ -11,7 +11,7 @@ // 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. -// ======================================================================== +// ======================================================================== import React from "react"; import { withStyles, makeStyles } from "@material-ui/core/styles"; import Table from "@material-ui/core/Table"; @@ -24,118 +24,142 @@ import Paper from "@material-ui/core/Paper"; import { Link } from "react-router-dom"; import IconButton from "@material-ui/core/IconButton"; import EditIcon from "@material-ui/icons/Edit"; -import DeleteDialog from "../../common/Dialogue" +import DeleteDialog from "../../common/Dialogue"; import DeleteIcon from "@material-ui/icons/Delete"; import ProjectForm from "./ProjectForm"; import apiService from "../../services/apiService"; const StyledTableCell = withStyles((theme) => ({ - body: { - fontSize: 14, - }, + body: { + fontSize: 14, + }, }))(TableCell); const StyledTableRow = withStyles((theme) => ({ - root: { - "&:nth-of-type(odd)": { - backgroundColor: theme.palette.action.hover, - }, + root: { + "&:nth-of-type(odd)": { + backgroundColor: theme.palette.action.hover, }, + }, }))(TableRow); const useStyles = makeStyles({ - table: { - minWidth: 350, - }, - cell: { - color: "grey", - }, + table: { + minWidth: 350, + }, + cell: { + color: "grey", + }, }); export default function ProjectsTable(props) { - const classes = useStyles(); - const [open, setOpen] = React.useState(false); - const [openForm, setOpenForm] = React.useState(false); - const [index, setIndex] = React.useState(0); + const classes = useStyles(); + const [open, setOpen] = React.useState(false); + const [openForm, setOpenForm] = React.useState(false); + const [index, setIndex] = React.useState(0); - let handleEdit = index => { - setIndex(index); - setOpenForm(true); - } - const handleClose = el => { - if (el.target.innerText === "Delete") { - apiService.deleteProject(props.data[index].metadata.name).then(() => { - console.log("project deleted"); - props.data.splice(index, 1); - props.setProjectsData([...props.data]); - }).catch(err => { - console.log("Error deleting project : ", err) - }) - } - setOpen(false); - setIndex(0); - }; - const handleFormClose = () => { - setIndex(0); - setOpenForm(false); - }; - const handleDelete = (index) => { - setIndex(index); - setOpen(true); - } - const handleSubmit = (data) => { - let payload = { "metadata": data } - apiService.updateProject(payload).then(res => { - props.data[index] = res; - props.setProjectsData([...props.data]); - }).catch(err => { - console.log("Error updating project : ", err); + let handleEdit = (index) => { + setIndex(index); + setOpenForm(true); + }; + const handleClose = (el) => { + if (el.target.innerText === "Delete") { + apiService + .deleteProject(props.data[index].metadata.name) + .then(() => { + console.log("project deleted"); + props.data.splice(index, 1); + props.setProjectsData([...props.data]); }) - setOpenForm(false); - }; - - return ( - <React.Fragment> - {(props.data && props.data.length > 0) && - <> - <ProjectForm open={openForm} onClose={handleFormClose} item={props.data[index]} onSubmit={handleSubmit} /> - <DeleteDialog open={open} onClose={handleClose} title={"Delete Project"} - content={`Are you sure you want to delete "${props.data[index] ? props.data[index].metadata.name : ""}" ?`} /> - <TableContainer component={Paper}> - <Table className={classes.table} size="small"> - <TableHead> - <TableRow> - <StyledTableCell>Name</StyledTableCell> - <StyledTableCell>Description</StyledTableCell> - <StyledTableCell>Actions</StyledTableCell> - </TableRow> - </TableHead> - <TableBody> - {props.data.map((row, index) => ( - <StyledTableRow key={row.metadata.name + "" + index}> - <StyledTableCell> - {" "} - <Link to={`/app/projects/${row.metadata.name}`}>{row.metadata.name}</Link> - </StyledTableCell> - <StyledTableCell className={classes.cell}> - {row.metadata.description} - </StyledTableCell> - <StyledTableCell className={classes.cell}> - <IconButton onClick={(e) => handleEdit(index)} title="Edit" > - <EditIcon color="primary" /> - </IconButton> - <IconButton onClick={(e) => handleDelete(index)} title="Delete" > - <DeleteIcon color="secondary" /> - </IconButton> - </StyledTableCell> - </StyledTableRow> - ))} - </TableBody> - </Table> - </TableContainer> - </> - } + .catch((err) => { + console.log("Error deleting project : ", err); + }); + } + setOpen(false); + setIndex(0); + }; + const handleFormClose = () => { + setIndex(0); + setOpenForm(false); + }; + const handleDelete = (index) => { + setIndex(index); + setOpen(true); + }; + const handleSubmit = (data) => { + let payload = { metadata: data }; + apiService + .updateProject(payload) + .then((res) => { + props.data[index] = res; + props.setProjectsData([...props.data]); + }) + .catch((err) => { + console.log("Error updating project : ", err); + }); + setOpenForm(false); + }; - </React.Fragment> - ); + return ( + <React.Fragment> + {props.data && props.data.length > 0 && ( + <> + <ProjectForm + open={openForm} + onClose={handleFormClose} + item={props.data[index]} + onSubmit={handleSubmit} + /> + <DeleteDialog + open={open} + onClose={handleClose} + title={"Delete Project"} + content={`Are you sure you want to delete "${ + props.data[index] ? props.data[index].metadata.name : "" + }" ?`} + /> + <TableContainer component={Paper}> + <Table className={classes.table} size="small"> + <TableHead> + <TableRow> + <StyledTableCell>Name</StyledTableCell> + <StyledTableCell>Description</StyledTableCell> + <StyledTableCell>Actions</StyledTableCell> + </TableRow> + </TableHead> + <TableBody> + {props.data.map((row, index) => ( + <StyledTableRow key={row.metadata.name + "" + index}> + <StyledTableCell> + {" "} + <Link to={`/app/projects/${row.metadata.name}`}> + {row.metadata.name} + </Link> + </StyledTableCell> + <StyledTableCell className={classes.cell}> + {row.metadata.description} + </StyledTableCell> + <StyledTableCell className={classes.cell}> + <IconButton + onClick={(e) => handleEdit(index)} + title="Edit" + > + <EditIcon color="primary" /> + </IconButton> + <IconButton + onClick={(e) => handleDelete(index)} + title="Delete" + > + <DeleteIcon color="secondary" /> + </IconButton> + </StyledTableCell> + </StyledTableRow> + ))} + </TableBody> + </Table> + </TableContainer> + </> + )} + </React.Fragment> + ); } diff --git a/src/tools/emcoui/src/appbase/AppBase.js b/src/tools/emcoui/src/appbase/AppBase.js index 5dd3b53b..76dc4d8e 100644 --- a/src/tools/emcoui/src/appbase/AppBase.js +++ b/src/tools/emcoui/src/appbase/AppBase.js @@ -11,7 +11,7 @@ // 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. -// ======================================================================== +// ======================================================================== import React from "react"; import PropTypes from "prop-types"; import { ThemeProvider, withStyles } from "@material-ui/core/styles"; @@ -25,6 +25,7 @@ import theme from "../theme/Theme"; import apiService from "../services/apiService"; import DeploymentIntentGroups from "../deploymentIntentGroups/DeploymentIntentGroups"; import { Switch, Route, Link } from "react-router-dom"; +// import Dashboard from "../dashboard/DashboardView"; const drawerWidth = 256; const styles = { @@ -45,11 +46,10 @@ const styles = { }, main: { flex: 1, - padding: theme.spacing(6, 4), + padding: theme.spacing(3, 4, 6, 4), background: "#eaeff1", }, footer: { - // padding: theme.spacing(2), background: "#eaeff1", }, }; @@ -63,17 +63,6 @@ class AppBase extends React.Component { }; } - componentDidMount() { - apiService - .getCompositeApps({ projectName: this.state.projectName }) - .then((response) => { - this.setState({ data: response }); - }) - .catch((err) => { - console.log("Unable to get composite apps"); - }) - .finally(); - } setMobileOpen = (mobileOpen) => { this.setState({ mobileOpen }); }; @@ -114,15 +103,15 @@ class AppBase extends React.Component { path={`${this.props.match.url}/404`} component={() => <div>Page Not found</div>} /> - <Route - exact - path={`${this.props.match.url}/composite-apps`} - > + {/* <Route exact path={`${this.props.match.url}/dashboard`}> + <Dashboard projectName={this.state.projectName} /> + </Route> */} + <Route exact path={`${this.props.match.url}/services`}> <CompositeApps projectName={this.state.projectName} /> </Route> <Route exact - path={`${this.props.match.url}/composite-apps/:appname/:version`} + path={`${this.props.match.url}/services/:appname/:version`} > <CompositeApp projectName={this.state.projectName} /> </Route> diff --git a/src/tools/emcoui/src/appbase/Content.js b/src/tools/emcoui/src/appbase/Content.js index 2c907acb..eff9a561 100644 --- a/src/tools/emcoui/src/appbase/Content.js +++ b/src/tools/emcoui/src/appbase/Content.js @@ -95,7 +95,7 @@ function Content(props) { </AppBar> <div className={classes.contentWrapper}> <Typography color="textSecondary" align="center"> - No composite apps for this project yet + No services for this project yet </Typography> </div> </Paper> diff --git a/src/tools/emcoui/src/appbase/Header.js b/src/tools/emcoui/src/appbase/Header.js index 0222151f..19f148a4 100644 --- a/src/tools/emcoui/src/appbase/Header.js +++ b/src/tools/emcoui/src/appbase/Header.js @@ -11,7 +11,7 @@ // 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. -// ======================================================================== +// ======================================================================== import React from "react"; import PropTypes from "prop-types"; import AppBar from "@material-ui/core/AppBar"; @@ -57,18 +57,18 @@ function Header(props) { let headerName = ""; let getHeaderName = () => { - if (location.pathname === `${props.match.url}/composite-apps`) { - headerName = "Composite Apps"; + if (location.pathname === `${props.match.url}/dashboard`) { + headerName = "Dashboard"; + } else if (location.pathname === `${props.match.url}/services`) { + headerName = "Services"; } else if ( location.pathname === `${props.match.url}/deployment-intent-group` ) { headerName = "Deployment Intent Groups"; - } else if (location.pathname.includes("composite-apps")) { + } else if (location.pathname.includes("services")) { headerName = - "Composite Apps / " + - location.pathname - .slice(location.pathname.indexOf("composite-apps")) - .slice(15); + "services / " + + location.pathname.slice(location.pathname.indexOf("services")).slice(9); } else if (location.pathname === `${props.match.url}/projects`) { headerName = "Projects"; } else if (location.pathname === `${props.match.url}/clusters`) { diff --git a/src/tools/emcoui/src/appbase/Navigator.js b/src/tools/emcoui/src/appbase/Navigator.js index 2df2c009..e8f16367 100644 --- a/src/tools/emcoui/src/appbase/Navigator.js +++ b/src/tools/emcoui/src/appbase/Navigator.js @@ -11,7 +11,7 @@ // 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. -// ======================================================================== +// ======================================================================== import React, { useState } from "react"; import PropTypes from "prop-types"; import clsx from "clsx"; @@ -23,7 +23,7 @@ import ListItem from "@material-ui/core/ListItem"; import ListItemIcon from "@material-ui/core/ListItemIcon"; import ListItemText from "@material-ui/core/ListItemText"; import HomeIcon from "@material-ui/icons/Home"; -import AppsIcon from "@material-ui/icons/Apps"; +import DeviceHubIcon from "@material-ui/icons/DeviceHub"; import DnsRoundedIcon from "@material-ui/icons/DnsRounded"; import { withRouter, Link } from "react-router-dom"; @@ -32,9 +32,9 @@ const categories = [ id: "1", children: [ { - id: "Composite Apps", - icon: <AppsIcon />, - url: "/composite-apps", + id: "Services", + icon: <DeviceHubIcon />, + url: "/services", }, { id: "Deployment Intent Groups", @@ -85,8 +85,8 @@ const styles = (theme) => ({ marginTop: theme.spacing(2), }, version: { - fontSize: "15px" - } + fontSize: "15px", + }, }); function Navigator(props) { @@ -99,11 +99,20 @@ function Navigator(props) { setActiveTab(location.pathname); } return ( - <Drawer PaperProps={props.PaperProps} variant={props.variant} open={props.open} onClose={props.onClose}> + <Drawer + PaperProps={props.PaperProps} + variant={props.variant} + open={props.open} + onClose={props.onClose} + > <List disablePadding> - <Link style={{ textDecoration: "none" }} to='/'> + <Link style={{ textDecoration: "none" }} to="/"> <ListItem - className={clsx(classes.firebase, classes.item, classes.itemCategory)} + className={clsx( + classes.firebase, + classes.item, + classes.itemCategory + )} > <ListItemText classes={{ @@ -111,14 +120,29 @@ function Navigator(props) { }} > ONAP4K8s - </ListItemText> - <span - className={clsx(classes.version)} - >{process.env.REACT_APP_VERSION}</span> + </ListItemText> + <span className={clsx(classes.version)}> + {process.env.REACT_APP_VERSION} + </span> </ListItem> </Link> - <ListItem className={clsx(classes.item, classes.itemCategory)}> + {/* <Link + style={{ textDecoration: "none" }} + to={{ + pathname: `${props.match.url}/dashboard`, + activeItem: "childId", + }} + key={"childId"} + > */} + <ListItem + button + className={clsx( + classes.item, + classes.itemCategory, + activeItem.includes("dashboard") && classes.itemActiveItem + )} + > <ListItemIcon className={classes.itemIcon}> <HomeIcon /> </ListItemIcon> @@ -130,10 +154,18 @@ function Navigator(props) { Dashboard </ListItemText> </ListItem> + {/* </Link> */} {categories.map(({ id, children }) => ( <React.Fragment key={id}> {children.map(({ id: childId, icon, url }) => ( - <Link style={{ textDecoration: "none" }} to={{ pathname: `${props.match.url}${url}`, activeItem: childId }} key={childId}> + <Link + style={{ textDecoration: "none" }} + to={{ + pathname: `${props.match.url}${url}`, + activeItem: childId, + }} + key={childId} + > <ListItem button className={clsx( diff --git a/src/tools/emcoui/src/assets/icons/empty.svg b/src/tools/emcoui/src/assets/icons/empty.svg new file mode 100644 index 00000000..f4e020ed --- /dev/null +++ b/src/tools/emcoui/src/assets/icons/empty.svg @@ -0,0 +1 @@ +<svg enable-background="new 0 0 24 24" height="512" viewBox="0 0 24 24" width="512" xmlns="http://www.w3.org/2000/svg"><path d="m21.5 22h-19c-1.378 0-2.5-1.121-2.5-2.5v-7c0-.07.015-.141.044-.205l3.969-8.82c.404-.896 1.299-1.475 2.28-1.475h11.414c.981 0 1.876.579 2.28 1.475l3.969 8.82c.029.064.044.135.044.205v7c0 1.379-1.122 2.5-2.5 2.5zm-20.5-9.393v6.893c0 .827.673 1.5 1.5 1.5h19c.827 0 1.5-.673 1.5-1.5v-6.893l-3.925-8.723c-.242-.536-.779-.884-1.368-.884h-11.414c-.589 0-1.126.348-1.368.885z"/><path d="m16.807 17h-9.614c-.622 0-1.186-.391-1.404-.973l-1.014-2.703c-.072-.194-.26-.324-.468-.324h-3.557c-.276 0-.5-.224-.5-.5s.224-.5.5-.5h3.557c.622 0 1.186.391 1.405.973l1.013 2.703c.073.194.261.324.468.324h9.613c.208 0 .396-.13.468-.324l1.013-2.703c.22-.582.784-.973 1.406-.973h3.807c.276 0 .5.224.5.5s-.224.5-.5.5h-3.807c-.208 0-.396.13-.468.324l-1.013 2.703c-.219.582-.784.973-1.405.973z"/></svg>
\ No newline at end of file diff --git a/src/tools/emcoui/src/common/ExpandableCard.jsx b/src/tools/emcoui/src/common/ExpandableCard.jsx new file mode 100644 index 00000000..1d2ea9e6 --- /dev/null +++ b/src/tools/emcoui/src/common/ExpandableCard.jsx @@ -0,0 +1,94 @@ +//======================================================================= +// Copyright (c) 2017-2020 Aarna Networks, Inc. +// All rights reserved. +// ====================================================================== +// 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. +// ======================================================================== + +import React, { useState } from "react"; +import { makeStyles } from "@material-ui/core/styles"; +import clsx from "clsx"; +import Card from "@material-ui/core/Card"; +import CardHeader from "@material-ui/core/CardHeader"; +import CardContent from "@material-ui/core/CardContent"; +import Collapse from "@material-ui/core/Collapse"; +import IconButton from "@material-ui/core/IconButton"; +import ExpandMoreIcon from "@material-ui/icons/ExpandMore"; +import StorageIcon from "@material-ui/icons/Storage"; +import ErrorIcon from "@material-ui/icons/Error"; + +const useStyles = makeStyles((theme) => ({ + root: { + width: "100%", + }, + expand: { + transform: "rotate(0deg)", + marginLeft: "auto", + transition: theme.transitions.create("transform", { + duration: theme.transitions.duration.shortest, + }), + }, + expandOpen: { + transform: "rotate(180deg)", + }, +})); +const ExpandableCard = (props) => { + const classes = useStyles(); + const [expanded, setExpanded] = useState(false); + + const handleExpandClick = () => { + if (!expanded) { + setExpanded(!expanded); + } else { + setExpanded(!expanded); + } + }; + + return ( + <> + <Card className={classes.root}> + <CardHeader + onClick={handleExpandClick} + avatar={ + <> + <StorageIcon fontSize="large" /> + </> + } + action={ + <> + {props.error && ( + <ErrorIcon color="error" style={{ verticalAlign: "middle" }} /> + )} + <IconButton + className={clsx(classes.expand, { + [classes.expandOpen]: expanded, + })} + onClick={handleExpandClick} + aria-expanded={expanded} + > + <ExpandMoreIcon /> + </IconButton> + </> + } + title={props.title} + subheader={props.description} + /> + <Collapse in={expanded} timeout="auto" unmountOnExit> + <CardContent>{props.content}</CardContent> + </Collapse> + </Card> + </> + ); +}; + +ExpandableCard.propTypes = {}; + +export default ExpandableCard; diff --git a/src/tools/emcoui/src/common/FileUpload.jsx b/src/tools/emcoui/src/common/FileUpload.jsx index 847951e9..97d34bc2 100644 --- a/src/tools/emcoui/src/common/FileUpload.jsx +++ b/src/tools/emcoui/src/common/FileUpload.jsx @@ -11,58 +11,64 @@ // 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. -// ======================================================================== -import React from 'react'; -import PropTypes from 'prop-types'; +// ======================================================================== +import React from "react"; +import PropTypes from "prop-types"; import FileCopyIcon from "@material-ui/icons/FileCopy"; import CloudUploadIcon from "@material-ui/icons/CloudUpload"; -import './fileUpload.css' +import "./fileUpload.css"; const FileUpload = (props) => { - return ( - <> - <div className="file-upload"> - <div - className="file-upload-wrap" - style={{ - border: props.file && props.file.name && "2px dashed rgba(0, 131, 143, 1)" - }} - > - <input - required - className="file-upload-input" - type="file" - accept={props.accept ? props.accept : "*"} - name="file" - onBlur={props.handleBlur ? props.handleBlur : null} - onChange={(event) => { - props.setFieldValue("file", event.currentTarget.files[0]); - }} - /> + return ( + <> + <div className="file-upload"> + <div + className="file-upload-wrap" + style={{ + border: + props.file && + props.file.name && + "2px dashed rgba(0, 131, 143, 1)", + }} + > + <input + required + className="file-upload-input" + type="file" + accept={props.accept ? props.accept : "*"} + name="file" + onBlur={props.handleBlur ? props.handleBlur : null} + onChange={(event) => { + props.setFieldValue(props.name, event.currentTarget.files[0]); + }} + /> - <div className="file-upload-text"> - {(props.file && props.file.name) ? (<> - <span> - <FileCopyIcon color="primary" /> - </span> - <span style={{ fontWeight: 600 }}>{props.file.name}</span> - </>) : (<> - <span> - <CloudUploadIcon /> - </span> - <span> - Drag And Drop or Click To Upload - </span> - </>)} - </div> - </div> - </div> - </>); + <div className="file-upload-text"> + {props.file && props.file.name ? ( + <> + <span> + <FileCopyIcon color="primary" /> + </span> + <span style={{ fontWeight: 600 }}>{props.file.name}</span> + </> + ) : ( + <> + <span> + <CloudUploadIcon /> + </span> + <span>Drag And Drop or Click To Upload</span> + </> + )} + </div> + </div> + </div> + </> + ); }; FileUpload.propTypes = { - handleBlur: PropTypes.func, - setFieldValue: PropTypes.func.isRequired, + handleBlur: PropTypes.func, + setFieldValue: PropTypes.func.isRequired, }; export default FileUpload; diff --git a/src/tools/emcoui/src/common/Form.jsx b/src/tools/emcoui/src/common/Form.jsx index e9fe3a2d..6e8eee2e 100644 --- a/src/tools/emcoui/src/common/Form.jsx +++ b/src/tools/emcoui/src/common/Form.jsx @@ -11,144 +11,155 @@ // 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. -// ======================================================================== -import React from 'react'; -import PropTypes from 'prop-types'; -import { withStyles } from '@material-ui/core/styles'; -import Button from '@material-ui/core/Button'; +// ======================================================================== +import React from "react"; +import PropTypes from "prop-types"; +import { withStyles } from "@material-ui/core/styles"; +import Button from "@material-ui/core/Button"; -import Dialog from '@material-ui/core/Dialog'; -import MuiDialogTitle from '@material-ui/core/DialogTitle'; -import MuiDialogContent from '@material-ui/core/DialogContent'; -import MuiDialogActions from '@material-ui/core/DialogActions'; -import IconButton from '@material-ui/core/IconButton'; -import CloseIcon from '@material-ui/icons/Close'; -import Typography from '@material-ui/core/Typography'; -import { TextField } from '@material-ui/core'; +import Dialog from "@material-ui/core/Dialog"; +import MuiDialogTitle from "@material-ui/core/DialogTitle"; +import MuiDialogContent from "@material-ui/core/DialogContent"; +import MuiDialogActions from "@material-ui/core/DialogActions"; +import IconButton from "@material-ui/core/IconButton"; +import CloseIcon from "@material-ui/icons/Close"; +import Typography from "@material-ui/core/Typography"; +import { TextField } from "@material-ui/core"; import * as Yup from "yup"; -import { Formik } from 'formik'; +import { Formik } from "formik"; const styles = (theme) => ({ - root: { - margin: 0, - padding: theme.spacing(2), - }, - closeButton: { - position: 'absolute', - right: theme.spacing(1), - top: theme.spacing(1), - color: theme.palette.grey[500], - }, + root: { + margin: 0, + padding: theme.spacing(2), + }, + closeButton: { + position: "absolute", + right: theme.spacing(1), + top: theme.spacing(1), + color: theme.palette.grey[500], + }, }); const DialogTitle = withStyles(styles)((props) => { - const { children, classes, onClose, ...other } = props; - return ( - <MuiDialogTitle disableTypography className={classes.root} {...other}> - <Typography variant="h6">{children}</Typography> - {onClose ? ( - <IconButton className={classes.closeButton} onClick={onClose}> - <CloseIcon /> - </IconButton> - ) : null} - </MuiDialogTitle> - ); + const { children, classes, onClose, ...other } = props; + return ( + <MuiDialogTitle disableTypography className={classes.root} {...other}> + <Typography variant="h6">{children}</Typography> + {onClose ? ( + <IconButton className={classes.closeButton} onClick={onClose}> + <CloseIcon /> + </IconButton> + ) : null} + </MuiDialogTitle> + ); }); const DialogActions = withStyles((theme) => ({ - root: { - margin: 0, - padding: theme.spacing(1), - }, + root: { + margin: 0, + padding: theme.spacing(1), + }, }))(MuiDialogActions); const DialogContent = withStyles((theme) => ({ - root: { - padding: theme.spacing(2), - } + root: { + padding: theme.spacing(2), + }, }))(MuiDialogContent); -const schema = Yup.object( - { - name: Yup.string().required(), - description: Yup.string(), - }) +const schema = Yup.object({ + name: Yup.string().required(), + description: Yup.string(), +}); const CreateForm = (props) => { - const { onClose, item, open, onSubmit } = props; - const buttonLabel = item ? "OK" : "Create" - const title = item ? "Edit" : "Create" - const handleClose = () => { - onClose(); - }; - let initialValues = item ? { name: item.metadata.name, description: item.metadata.description } : { name: "", description: "" } + const { onClose, item, open, onSubmit } = props; + const buttonLabel = item ? "OK" : "Create"; + const title = item ? "Edit" : "Create"; + const handleClose = () => { + onClose(); + }; + let initialValues = item + ? { name: item.metadata.name, description: item.metadata.description } + : { name: "", description: "" }; - return ( - <Dialog maxWidth={"xs"} onClose={handleClose} aria-labelledby="customized-dialog-title" open={open} disableBackdropClick> - <DialogTitle id="simple-dialog-title">{title}</DialogTitle> - <Formik - initialValues={initialValues} - onSubmit={async values => { - onSubmit(values); - }} - validationSchema={schema} - > - {props => { - const { - touched, - errors, - isSubmitting, - handleChange, - handleBlur, - handleSubmit - } = props; - return ( - <form noValidate onSubmit={handleSubmit}> - <DialogContent dividers> - <TextField - style={{ width: "100%", marginBottom: "10px" }} - id="name" - label="Name" - name="name" - type="text" - onChange={handleChange} - onBlur={handleBlur} - helperText={(errors.name && touched.name && ( - "Name is required" - ))} - required - error={errors.name && touched.name} - /> - <TextField - style={{ width: "100%", marginBottom: "25px" }} - name="description" - onChange={handleChange} - onBlur={handleBlur} - id="description" - label="Description" - multiline - rowsMax={4} - /> - </DialogContent> - <DialogActions> - <Button autoFocus onClick={handleClose} color="secondary"> - Cancel - </Button> - <Button autoFocus type="submit" color="primary" disabled={isSubmitting}> - {buttonLabel} - </Button> - </DialogActions> - </form> - ); - }} - </Formik> - </Dialog> - ); + return ( + <Dialog + maxWidth={"xs"} + onClose={handleClose} + aria-labelledby="customized-dialog-title" + open={open} + disableBackdropClick + > + <DialogTitle id="simple-dialog-title">{title}</DialogTitle> + <Formik + initialValues={initialValues} + onSubmit={async (values) => { + onSubmit(values); + }} + validationSchema={schema} + > + {(props) => { + const { + touched, + errors, + isSubmitting, + handleChange, + handleBlur, + handleSubmit, + submitCount, + } = props; + return ( + <form noValidate onSubmit={handleSubmit}> + <DialogContent dividers> + <TextField + style={{ width: "100%", marginBottom: "10px" }} + id="name" + label="Name" + name="name" + type="text" + onChange={handleChange} + onBlur={handleBlur} + helperText={errors.name && touched.name && "Name is required"} + required + error={errors.name && touched.name} + /> + <TextField + style={{ width: "100%", marginBottom: "25px" }} + name="description" + onChange={handleChange} + onBlur={handleBlur} + id="description" + label="Description" + multiline + rowsMax={4} + /> + </DialogContent> + <DialogActions> + <Button autoFocus onClick={handleClose} color="secondary"> + Cancel + </Button> + <Button + autoFocus + type="submit" + color="primary" + disabled={isSubmitting || submitCount > 0} + > + {buttonLabel} + </Button> + </DialogActions> + </form> + ); + }} + </Formik> + </Dialog> + ); }; CreateForm.propTypes = { - onClose: PropTypes.func.isRequired, - open: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired, + open: PropTypes.bool.isRequired, }; export default CreateForm; diff --git a/src/tools/emcoui/src/compositeApps/CompositeApp.jsx b/src/tools/emcoui/src/compositeApps/CompositeApp.jsx index 34d07fbc..8b2c2b10 100644 --- a/src/tools/emcoui/src/compositeApps/CompositeApp.jsx +++ b/src/tools/emcoui/src/compositeApps/CompositeApp.jsx @@ -11,7 +11,7 @@ // 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. -// ======================================================================== +// ======================================================================== import React from "react"; import Tab from "@material-ui/core/Tab"; import Tabs from "@material-ui/core/Tabs"; @@ -19,15 +19,15 @@ import Paper from "@material-ui/core/Paper"; import { withStyles } from "@material-ui/core/styles"; import Box from "@material-ui/core/Box"; import PropTypes from "prop-types"; -import Apps from "../compositeApps/apps/Apps"; -import CompositeProfiles from "../compositeApps/compositeProfiles/CompositeProfiles"; -import Intents from "../compositeApps/intents/GenericPlacementIntents"; import BackIcon from "@material-ui/icons/ArrowBack"; import { withRouter } from "react-router-dom"; import { IconButton } from "@material-ui/core"; import apiService from "../services/apiService"; import Spinner from "../common/Spinner"; -import NetworkIntent from "../networkIntents/NetworkIntents"; +import Apps from "../compositeApps/apps/Apps"; +import CompositeProfiles from "../compositeApps/compositeProfiles/CompositeProfiles"; +// import Intents from "../compositeApps/intents/GenericPlacementIntents"; +// import NetworkIntent from "../networkIntents/NetworkIntents"; const lightColor = "rgba(255, 255, 255, 0.7)"; @@ -134,8 +134,6 @@ class CompositeApp extends React.Component { > <Tab label="Apps" /> <Tab label="Composite Profiles" /> - <Tab label="Generic Placement Intents" /> - <Tab label="Network Controller Intents" /> </Tabs> {this.state.isLoading && <Spinner />} @@ -158,22 +156,6 @@ class CompositeApp extends React.Component { appsData={this.state.appsData} /> </TabPanel> - <TabPanel value={this.state.activeTab} index={2}> - <Intents - projectName={this.props.projectName} - compositeAppName={this.state.compositeAppName} - compositeAppVersion={this.state.compositeAppVersion} - appsData={this.state.appsData} - /> - </TabPanel> - <TabPanel value={this.state.activeTab} index={3}> - <NetworkIntent - projectName={this.props.projectName} - compositeAppName={this.state.compositeAppName} - compositeAppVersion={this.state.compositeAppVersion} - appsData={this.state.appsData} - /> - </TabPanel> </> )} </Paper> @@ -181,7 +163,5 @@ class CompositeApp extends React.Component { ); } } - CompositeApp.propTypes = {}; - export default withStyles(styles)(withRouter(CompositeApp)); diff --git a/src/tools/emcoui/src/compositeApps/CompositeAppTable.jsx b/src/tools/emcoui/src/compositeApps/CompositeAppTable.jsx index 220d7df8..926ab545 100644 --- a/src/tools/emcoui/src/compositeApps/CompositeAppTable.jsx +++ b/src/tools/emcoui/src/compositeApps/CompositeAppTable.jsx @@ -11,7 +11,7 @@ // 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. -// ======================================================================== +// ======================================================================== import React, { useState } from "react"; import { withStyles, makeStyles } from "@material-ui/core/styles"; import Table from "@material-ui/core/Table"; @@ -21,13 +21,15 @@ import TableContainer from "@material-ui/core/TableContainer"; import TableHead from "@material-ui/core/TableHead"; import TableRow from "@material-ui/core/TableRow"; import Paper from "@material-ui/core/Paper"; -import { Link } from "react-router-dom"; +// import { Link } from "react-router-dom"; import IconButton from "@material-ui/core/IconButton"; import EditIcon from "@material-ui/icons/Edit"; -import DeleteIcon from '@material-ui/icons/Delete'; +import DeleteIcon from "@material-ui/icons/Delete"; import CreateCompositeAppForm from "./dialogs/CompositeAppForm"; import apiService from "../services/apiService"; import DeleteDialog from "../common/Dialogue"; +import { Link, withRouter } from "react-router-dom"; +import Notification from "../common/Notification"; const StyledTableCell = withStyles((theme) => ({ body: { @@ -52,50 +54,80 @@ const useStyles = makeStyles({ }, }); -export default function CustomizedTables({ data, ...props }) { +function CustomizedTables({ data, ...props }) { const classes = useStyles(); const [openForm, setOpenForm] = useState(false); const [activeRowIndex, setActiveRowIndex] = useState(0); const [row, setRow] = useState({}); const [open, setOpen] = useState(false); + const [notificationDetails, setNotificationDetails] = useState({}); let onEditCompositeApp = (row, index) => { setActiveRowIndex(index); setRow(row); setOpenForm(true); - } + // props.history.push(`services/${row.metadata.name}/${row.spec.version}`); + }; const handleCloseForm = (fields) => { if (fields) { - let request = { payload: { name: fields.name, description: fields.description, spec: { version: fields.version } }, projectName: props.projectName, compositeAppVersion: row.spec.version }; - apiService.updateCompositeApp(request).then(res => { - let updatedData = data.slice(); - updatedData.splice(activeRowIndex, 1); - updatedData.push(res); - props.handleUpdateState(updatedData); - }).catch(err => { - console.log("error creating composite app : ", err) - }).finally(() => { - setOpenForm(false); - }); - } - else { + let request = { + payload: { + name: fields.name, + description: fields.description, + spec: { version: fields.version }, + }, + projectName: props.projectName, + compositeAppVersion: row.spec.version, + }; + apiService + .updateCompositeApp(request) + .then((res) => { + let updatedData = data.slice(); + updatedData.splice(activeRowIndex, 1); + updatedData.push(res); + props.handleUpdateState(updatedData); + }) + .catch((err) => { + console.log("error creating composite app : ", err); + }) + .finally(() => { + setOpenForm(false); + }); + } else { setOpenForm(false); } }; const handleDeleteCompositeApp = (index) => { setActiveRowIndex(index); setOpen(true); - } - const handleClose = el => { + }; + const handleClose = (el) => { if (el.target.innerText === "Delete") { - let request = { projectName: props.projectName, compositeAppName: data[activeRowIndex].metadata.name, compositeAppVersion: data[activeRowIndex].spec.version }; - apiService.deleteCompositeApp(request).then(() => { - console.log("cluster deleted"); - data.splice(activeRowIndex, 1); - let updatedData = data.slice(); - props.handleUpdateState(updatedData); - }).catch(err => { - console.log("Error deleting cluster : ", err) - }) + let request = { + projectName: props.projectName, + compositeAppName: data[activeRowIndex].metadata.name, + compositeAppVersion: data[activeRowIndex].spec.version, + }; + apiService + .deleteCompositeApp(request) + .then(() => { + console.log("cluster deleted"); + data.splice(activeRowIndex, 1); + let updatedData = data.slice(); + props.handleUpdateState(updatedData); + }) + .catch((err) => { + console.log("Error deleting cluster : ", err); + let message = "Error deleting service"; + if (err.response.data.includes("Non emtpy DIG in service")) { + message = + "Error deleting service : please delete deployment intent group first"; + } + setNotificationDetails({ + show: true, + message: message, + severity: "error", + }); + }); } setOpen(false); setActiveRowIndex(0); @@ -103,11 +135,22 @@ export default function CustomizedTables({ data, ...props }) { return ( <> - {data && (data.length > 0) && - (<> - <CreateCompositeAppForm open={openForm} handleClose={handleCloseForm} item={row} /> - <DeleteDialog open={open} onClose={handleClose} title={"Delete Cluster"} - content={`Are you sure you want to delete "${data[activeRowIndex] ? data[activeRowIndex].metadata.name : ""}" ?`} /> + <Notification notificationDetails={notificationDetails} /> + {data && data.length > 0 && ( + <> + <CreateCompositeAppForm + open={openForm} + handleClose={handleCloseForm} + item={row} + /> + <DeleteDialog + open={open} + onClose={handleClose} + title={"Delete Service"} + content={`Are you sure you want to delete "${ + data[activeRowIndex] ? data[activeRowIndex].metadata.name : "" + }" ?`} + /> <TableContainer component={Paper}> <Table className={classes.table} size="small"> <TableHead> @@ -122,8 +165,12 @@ export default function CustomizedTables({ data, ...props }) { {data.map((row, index) => ( <StyledTableRow key={row.metadata.name}> <StyledTableCell> - {" "} - <Link to={`composite-apps/${row.metadata.name}/${row.spec.version}`}>{row.metadata.name}</Link> + <Link + to={`services/${row.metadata.name}/${row.spec.version}`} + > + {row.metadata.name} + </Link> + {/* {row.metadata.name} */} </StyledTableCell> <StyledTableCell className={classes.cell}> {row.metadata.description} @@ -132,10 +179,18 @@ export default function CustomizedTables({ data, ...props }) { {row.spec.version} </StyledTableCell> <StyledTableCell className={classes.cell}> - <IconButton onClick={(e) => onEditCompositeApp(row, index)} title="Edit"> + {/* <IconButton + onClick={(e) => onEditCompositeApp(row, index)} + title="Edit" + > <EditIcon color="primary" /> - </IconButton> - <IconButton color="secondary" onClick={() => { handleDeleteCompositeApp(index) }}> + </IconButton> */} + <IconButton + color="secondary" + onClick={() => { + handleDeleteCompositeApp(index); + }} + > <DeleteIcon /> </IconButton> </StyledTableCell> @@ -144,7 +199,11 @@ export default function CustomizedTables({ data, ...props }) { </TableBody> </Table> </TableContainer> - </>)} - {(!data || (data.length === 0)) && (<span>No Clusters</span>)} - </>) + </> + )} + {(!data || data.length === 0) && <span>No Composite Apps</span>} + </> + ); } + +export default withRouter(CustomizedTables); diff --git a/src/tools/emcoui/src/compositeApps/CompositeApps.jsx b/src/tools/emcoui/src/compositeApps/CompositeApps.jsx index e7901ff9..5c540039 100644 --- a/src/tools/emcoui/src/compositeApps/CompositeApps.jsx +++ b/src/tools/emcoui/src/compositeApps/CompositeApps.jsx @@ -11,14 +11,15 @@ // 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. -// ======================================================================== +// ======================================================================== import React from "react"; import CompositeAppTable from "./CompositeAppTable"; -import { withStyles, Button, Grid } from "@material-ui/core"; +import { withStyles, Button, Grid, Typography } from "@material-ui/core"; import CreateCompositeAppForm from "./dialogs/CompositeAppForm"; import AddIcon from "@material-ui/icons/Add"; import apiService from "../services/apiService"; import Spinner from "../common/Spinner"; +import { ReactComponent as EmptyIcon } from "../assets/icons/empty.svg"; const styles = { root: { display: "flex", @@ -57,22 +58,43 @@ class CompositeApps extends React.Component { handleClose = (fields) => { if (fields) { - let request = { - payload: { - metadata: { name: fields.name, description: fields.description }, - spec: { version: fields.version }, - }, - projectName: this.props.projectName, + const formData = new FormData(); + let appsData = []; + fields.apps.forEach((app) => { + //add files for each app + formData.append(`${app.appName}_file`, app.file); + formData.append(`${app.appName}_profile`, app.profilePackageFile); + appsData.push({ + metadata: { + name: app.appName, + description: app.description ? app.description : "na", + filename: app.file.name, + }, + profileMetadata: { + name: `${app.appName}_profile`, + filename: app.profilePackageFile.name, + }, + clusters: app.clusters, + }); + }); + + let servicePayload = { + name: fields.name, + description: fields.description, + spec: { projectName: this.props.projectName, appsData }, }; + formData.append("servicePayload", JSON.stringify(servicePayload)); + let request = { projectName: this.props.projectName, payload: formData }; apiService - .createCompositeApp(request) + .addService(request) .then((res) => { + console.log("create service response : " + res); if (this.state.data && this.state.data.length > 0) this.setState({ data: [...this.state.data, res] }); else this.setState({ data: [res] }); }) .catch((err) => { - console.log("error creating composite app : ", err); + console.log("error adding app : ", err); }); } this.setState({ open: false }); @@ -88,32 +110,48 @@ class CompositeApps extends React.Component { {this.state.isLoading && <Spinner />} {!this.state.isLoading && ( <> - <Button - variant="outlined" - color="primary" - startIcon={<AddIcon />} - onClick={this.handleCreateCompositeApp} - > - Create Composite App - </Button> <CreateCompositeAppForm open={this.state.open} handleClose={this.handleClose} /> <Grid container spacing={2} alignItems="center"> - <Grid item xs style={{ marginTop: "20px" }}> - {this.state.data && this.state.data.length > 0 && ( + <Grid item xs={12}> + <Button + variant="outlined" + color="primary" + startIcon={<AddIcon />} + onClick={this.handleCreateCompositeApp} + > + Add service + </Button> + </Grid> + {this.state.data && this.state.data.length > 0 && ( + <Grid item xs={12}> <CompositeAppTable data={this.state.data} projectName={this.props.projectName} handleUpdateState={this.handleUpdateState} /> - )} - {(!this.state.data || this.state.data.length === 0) && ( - <span>No Composite Apps</span> - )} - </Grid> + </Grid> + )} </Grid> + {(!this.state.data || this.state.data.length === 0) && ( + <Grid + container + spacing={2} + direction="column" + alignItems="center" + > + <Grid style={{ marginTop: "60px" }} item xs={6}> + <EmptyIcon style={{ height: "100px", width: "100px" }} /> + </Grid> + <Grid item xs={12}> + <Typography variant="h6"> + No service found, start by adding a service + </Typography> + </Grid> + </Grid> + )} </> )} </> diff --git a/src/tools/emcoui/src/compositeApps/apps/Apps.jsx b/src/tools/emcoui/src/compositeApps/apps/Apps.jsx index 14be60c2..0e3638b3 100644 --- a/src/tools/emcoui/src/compositeApps/apps/Apps.jsx +++ b/src/tools/emcoui/src/compositeApps/apps/Apps.jsx @@ -11,7 +11,7 @@ // 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. -// ======================================================================== +// ======================================================================== import React, { useState } from "react"; import { makeStyles } from "@material-ui/core/styles"; import Card from "@material-ui/core/Card"; @@ -19,7 +19,7 @@ import CardContent from "@material-ui/core/CardContent"; import IconButton from "@material-ui/core/IconButton"; import Typography from "@material-ui/core/Typography"; import DeleteIcon from "@material-ui/icons/Delete"; -import EditIcon from "@material-ui/icons/Edit"; +// import EditIcon from "@material-ui/icons/Edit"; import { Grid, Button, Tooltip } from "@material-ui/core"; import AddIcon from "@material-ui/icons/Add"; import apiService from "../../services/apiService"; @@ -144,7 +144,7 @@ const Apps = ({ data, onStateChange, ...props }) => { }; return ( <> - <Button + {/* <Button variant="outlined" color="primary" startIcon={<AddIcon />} @@ -152,7 +152,7 @@ const Apps = ({ data, onStateChange, ...props }) => { size="small" > Add App - </Button> + </Button> */} <AppForm open={formOpen} onClose={handleFormClose} @@ -200,6 +200,7 @@ const Apps = ({ data, onStateChange, ...props }) => { </Typography> </CardContent> <div className={classes.controls}> + {/* //edit app api is not implemented yet <IconButton onClick={handleEditApp.bind(this, value)} color="primary" @@ -212,7 +213,7 @@ const Apps = ({ data, onStateChange, ...props }) => { onClick={() => handleDeleteApp(index)} > <DeleteIcon /> - </IconButton> + </IconButton> */} </div> </div> </Card> diff --git a/src/tools/emcoui/src/compositeApps/compositeProfiles/CompositeProfileCard.jsx b/src/tools/emcoui/src/compositeApps/compositeProfiles/CompositeProfileCard.jsx index 58161e8e..d565eba7 100644 --- a/src/tools/emcoui/src/compositeApps/compositeProfiles/CompositeProfileCard.jsx +++ b/src/tools/emcoui/src/compositeApps/compositeProfiles/CompositeProfileCard.jsx @@ -11,7 +11,7 @@ // 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. -// ======================================================================== +// ======================================================================== import React, { useState } from "react"; import { makeStyles } from "@material-ui/core/styles"; import clsx from "clsx"; @@ -35,7 +35,7 @@ import TableCell from "@material-ui/core/TableCell"; import Paper from "@material-ui/core/Paper"; import TableBody from "@material-ui/core/TableBody"; import apiService from "../../services/apiService"; -import EditIcon from "@material-ui/icons/Edit"; +// import EditIcon from "@material-ui/icons/Edit"; import DeleteIcon from "@material-ui/icons/Delete"; import ProfileForm from "./ProfileForm"; import DeleteDialog from "../../common/Dialogue"; @@ -196,7 +196,7 @@ export default function RecipeReviewCard(props) { /> <Collapse in={expanded} timeout="auto" unmountOnExit> <CardContent> - <Button + {/* <Button disabled={!(props.appsData && props.appsData.length > 0)} variant="outlined" size="small" @@ -208,11 +208,12 @@ export default function RecipeReviewCard(props) { }} > Add Profile - </Button> - <Button + </Button> */} + {/* <Button variant="outlined" size="small" color="secondary" + disabled={data && data.length > 0} style={{ float: "right" }} startIcon={<DeleteIcon />} onClick={() => { @@ -220,7 +221,7 @@ export default function RecipeReviewCard(props) { }} > Delete Composite Profile - </Button> + </Button> */} {data && data.length > 0 && ( <> <DeleteDialog @@ -238,7 +239,7 @@ export default function RecipeReviewCard(props) { <StyledTableCell>Name</StyledTableCell> <StyledTableCell>Description</StyledTableCell> <StyledTableCell>App</StyledTableCell> - <StyledTableCell>Actions</StyledTableCell> + {/* <StyledTableCell>Actions</StyledTableCell> */} </TableRow> </TableHead> <TableBody> @@ -253,7 +254,9 @@ export default function RecipeReviewCard(props) { <StyledTableCell> {profile.spec["app-name"]} </StyledTableCell> - <StyledTableCell> + {/* <StyledTableCell> + + //edit profile api is not implemented yet <IconButton onClick={(e) => handleEdit(index)} title="Edit" @@ -266,7 +269,7 @@ export default function RecipeReviewCard(props) { > <DeleteIcon color="secondary" /> </IconButton> - </StyledTableCell> + </StyledTableCell> */} </StyledTableRow> ))} </TableBody> @@ -275,7 +278,7 @@ export default function RecipeReviewCard(props) { </> )} {!(props.appsData && props.appsData.length > 0) && ( - <div>No apps found for adding profile</div> + <div>No app found for adding profile</div> )} </CardContent> </Collapse> diff --git a/src/tools/emcoui/src/compositeApps/compositeProfiles/CompositeProfiles.jsx b/src/tools/emcoui/src/compositeApps/compositeProfiles/CompositeProfiles.jsx index 25ecaaee..26629e03 100644 --- a/src/tools/emcoui/src/compositeApps/compositeProfiles/CompositeProfiles.jsx +++ b/src/tools/emcoui/src/compositeApps/compositeProfiles/CompositeProfiles.jsx @@ -11,7 +11,7 @@ // 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. -// ======================================================================== +// ======================================================================== import React, { useState, useEffect } from "react"; import Card from "./CompositeProfileCard"; import { Button, Grid } from "@material-ui/core"; @@ -82,7 +82,6 @@ const CompositeProfiles = (props) => { compositeAppVersion: props.compositeAppVersion, compositeProfileName: data[index].metadata.name, }; - console.log(request); apiService .deleteCompositeProfile(request) .then(() => { @@ -115,7 +114,7 @@ const CompositeProfiles = (props) => { }"`} /> - <Button + {/* <Button disabled={isLoading} variant="outlined" color="primary" @@ -123,7 +122,7 @@ const CompositeProfiles = (props) => { onClick={handleAddCompositeProfile} > Add Composite Profile - </Button> + </Button> */} <Form onClose={handleCloseForm} open={openForm} onSubmit={handleSubmit} /> <Grid container diff --git a/src/tools/emcoui/src/compositeApps/dialogs/AppForm.jsx b/src/tools/emcoui/src/compositeApps/dialogs/AppForm.jsx new file mode 100644 index 00000000..12dd7dd8 --- /dev/null +++ b/src/tools/emcoui/src/compositeApps/dialogs/AppForm.jsx @@ -0,0 +1,149 @@ +//======================================================================= +// Copyright (c) 2017-2020 Aarna Networks, Inc. +// All rights reserved. +// ====================================================================== +// 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. +// ======================================================================== +import React from "react"; +import ExpandableCard from "../../common/ExpandableCard"; +import { Grid, Paper, TextField } from "@material-ui/core"; +import FileUpload from "../../common/FileUpload"; + +function AppDetailsForm({ formikProps, ...props }) { + return ( + <> + <Paper + style={{ + width: "100%", + padding: "20px", + maxHeight: "395px", + overflowY: "auto", + scrollbarWidth: "thin", + }} + > + <Grid container spacing={3}> + <Grid item xs={6}> + <TextField + fullWidth + value={formikProps.values.apps[props.index].appName} + name={`apps[${props.index}].appName`} + id="app-name" + label="App name" + size="small" + onChange={formikProps.handleChange} + onBlur={formikProps.handleBlur} + required + helperText={ + formikProps.errors.apps && + formikProps.errors.apps[props.index] && + formikProps.errors.apps[props.index].appName + } + error={ + formikProps.errors.apps && + formikProps.errors.apps[props.index] && + formikProps.errors.apps[props.index].appName && + true + } + /> + </Grid> + <Grid item xs={6}> + <TextField + fullWidth + value={formikProps.values.apps[props.index].description} + name={`apps[${props.index}].description`} + id="app-description" + label="Description" + multiline + onChange={formikProps.handleChange} + onBlur={formikProps.handleBlur} + rowsMax={4} + /> + </Grid> + <Grid item xs={6}> + <label + style={{ marginTop: "20px" }} + className="MuiFormLabel-root MuiInputLabel-root" + htmlFor="file" + id="file-label" + > + App tgz file + <span className="MuiFormLabel-asterisk MuiInputLabel-asterisk"> +  * + </span> + </label> + <FileUpload + setFieldValue={formikProps.setFieldValue} + file={formikProps.values.apps[props.index].file} + onBlur={formikProps.handleBlur} + name={`apps[${props.index}].file`} + accept={".tgz"} + /> + {formikProps.errors.apps && + formikProps.errors.apps[props.index] && + formikProps.errors.apps[props.index].file && ( + <p style={{ color: "#f44336" }}> + {formikProps.errors.apps[props.index].file} + </p> + )} + </Grid> + <Grid item xs={6}> + <label + style={{ marginTop: "20px" }} + className="MuiFormLabel-root MuiInputLabel-root" + htmlFor="file" + id="file-label" + > + Profile tar file + <span className="MuiFormLabel-asterisk MuiInputLabel-asterisk"> +  * + </span> + </label> + <FileUpload + setFieldValue={formikProps.setFieldValue} + file={formikProps.values.apps[props.index].profilePackageFile} + onBlur={formikProps.handleBlur} + name={`apps[${props.index}].profilePackageFile`} + accept={".tar.gz, .tar"} + /> + {formikProps.errors.apps && + formikProps.errors.apps[props.index] && + formikProps.errors.apps[props.index].profilePackageFile && ( + <p style={{ color: "#f44336" }}> + {formikProps.errors.apps[props.index].profilePackageFile} + </p> + )} + </Grid> + </Grid> + </Paper> + </> + ); +} + +const AppForm = (props) => { + return ( + <ExpandableCard + error={ + props.formikProps.errors.apps && + props.formikProps.errors.apps[props.index] + } + title={props.name} + description={props.description} + content={ + <AppDetailsForm + formikProps={props.formikProps} + name={props.name} + index={props.index} + /> + } + /> + ); +}; +export default AppForm; diff --git a/src/tools/emcoui/src/compositeApps/dialogs/AppFormGeneral.jsx b/src/tools/emcoui/src/compositeApps/dialogs/AppFormGeneral.jsx new file mode 100644 index 00000000..e2272ae8 --- /dev/null +++ b/src/tools/emcoui/src/compositeApps/dialogs/AppFormGeneral.jsx @@ -0,0 +1,129 @@ +//======================================================================= +// Copyright (c) 2017-2020 Aarna Networks, Inc. +// All rights reserved. +// ====================================================================== +// 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. +// ======================================================================== +import { Grid, Paper, TextField } from "@material-ui/core"; +import FileUpload from "../../common/FileUpload"; +import React from "react"; + +function AppFormGeneral({ formikProps, ...props }) { + return ( + <> + <Paper + style={{ + width: "100%", + padding: "20px", + maxHeight: "395px", + overflowY: "auto", + scrollbarWidth: "thin", + }} + > + <Grid container spacing={3}> + <Grid item xs={6}> + <TextField + fullWidth + value={formikProps.values.apps[props.index].appName} + name={`apps[${props.index}].appName`} + id="app-name" + label="App name" + size="small" + onChange={formikProps.handleChange} + onBlur={formikProps.handleBlur} + required + helperText={ + formikProps.errors.apps && + formikProps.errors.apps[props.index] && + formikProps.errors.apps[props.index].appName + } + error={ + formikProps.errors.apps && + formikProps.errors.apps[props.index] && + formikProps.errors.apps[props.index].appName && + true + } + /> + </Grid> + <Grid item xs={6}> + <TextField + fullWidth + value={formikProps.values.apps[props.index].description} + name={`apps[${props.index}].description`} + id="app-description" + label="Description" + multiline + onChange={formikProps.handleChange} + onBlur={formikProps.handleBlur} + rowsMax={4} + /> + </Grid> + <Grid item xs={6}> + <label + style={{ marginTop: "20px" }} + className="MuiFormLabel-root MuiInputLabel-root" + htmlFor="file" + id="file-label" + > + App tgz file + <span className="MuiFormLabel-asterisk MuiInputLabel-asterisk"> +  * + </span> + </label> + <FileUpload + setFieldValue={formikProps.setFieldValue} + file={formikProps.values.apps[props.index].file} + onBlur={formikProps.handleBlur} + name={`apps[${props.index}].file`} + accept={".tgz"} + /> + {formikProps.errors.apps && + formikProps.errors.apps[props.index] && + formikProps.errors.apps[props.index].file && ( + <p style={{ color: "#f44336" }}> + {formikProps.errors.apps[props.index].file} + </p> + )} + </Grid> + <Grid item xs={6}> + <label + style={{ marginTop: "20px" }} + className="MuiFormLabel-root MuiInputLabel-root" + htmlFor="file" + id="file-label" + > + Profile tar file + <span className="MuiFormLabel-asterisk MuiInputLabel-asterisk"> +  * + </span> + </label> + <FileUpload + setFieldValue={formikProps.setFieldValue} + file={formikProps.values.apps[props.index].profilePackageFile} + onBlur={formikProps.handleBlur} + name={`apps[${props.index}].profilePackageFile`} + accept={".tar.gz, .tar"} + /> + {formikProps.errors.apps && + formikProps.errors.apps[props.index] && + formikProps.errors.apps[props.index].profilePackageFile && ( + <p style={{ color: "#f44336" }}> + {formikProps.errors.apps[props.index].profilePackageFile} + </p> + )} + </Grid> + </Grid> + </Paper> + </> + ); +} + +export default AppFormGeneral; diff --git a/src/tools/emcoui/src/compositeApps/dialogs/AppFormPlacement.jsx b/src/tools/emcoui/src/compositeApps/dialogs/AppFormPlacement.jsx new file mode 100644 index 00000000..c52c2b42 --- /dev/null +++ b/src/tools/emcoui/src/compositeApps/dialogs/AppFormPlacement.jsx @@ -0,0 +1,83 @@ +//======================================================================= +// Copyright (c) 2017-2020 Aarna Networks, Inc. +// All rights reserved. +// ====================================================================== +// 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. +// ======================================================================== +import React from "react"; +import PropTypes from "prop-types"; +import { Grid, Paper, Typography } from "@material-ui/core"; +import EnhancedTable from "./SortableTable"; + +function AppFormPlacement({ + formikProps, + index, + clusterProviders, + handleRowSelect, + ...props +}) { + return ( + <> + <Typography variant="subtitle1" style={{ float: "left" }}> + Select Clusters + <span className="MuiFormLabel-asterisk MuiInputLabel-asterisk"> *</span> + </Typography> + {formikProps.errors.apps && + formikProps.errors.apps[index] && + formikProps.errors.apps[index].clusters && ( + <span + style={{ + color: "#f44336", + marginRight: "35px", + float: "right", + }} + > + {typeof formikProps.errors.apps[index].clusters === "string" && + formikProps.errors.apps[index].clusters} + </span> + )} + <Grid + container + spacing={3} + style={{ + height: "400px", + overflowY: "auto", + width: "100%", + scrollbarWidth: "thin", + }} + > + {clusterProviders && + clusterProviders.length > 0 && + clusterProviders.map((clusterProvider) => ( + <Grid key={clusterProvider.name} item xs={12}> + <Paper> + <EnhancedTable + key={clusterProvider.name} + tableName={clusterProvider.name} + clusters={clusterProvider.clusters} + formikValues={formikProps.values.apps[index].clusters} + onRowSelect={handleRowSelect} + /> + </Paper> + </Grid> + ))} + </Grid> + </> + ); +} + +AppFormPlacement.propTypes = { + formikProps: PropTypes.object, + index: PropTypes.number, + handleRowSelect: PropTypes.func, +}; + +export default AppFormPlacement; diff --git a/src/tools/emcoui/src/compositeApps/dialogs/AppNetworkForm.jsx b/src/tools/emcoui/src/compositeApps/dialogs/AppNetworkForm.jsx new file mode 100644 index 00000000..055dc3e6 --- /dev/null +++ b/src/tools/emcoui/src/compositeApps/dialogs/AppNetworkForm.jsx @@ -0,0 +1,524 @@ +//======================================================================= +// Copyright (c) 2017-2020 Aarna Networks, Inc. +// All rights reserved. +// ====================================================================== +// 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. +// ======================================================================== +import React, { useState } from "react"; +import { makeStyles } from "@material-ui/core/styles"; +import Button from "@material-ui/core/Button"; +import Typography from "@material-ui/core/Typography"; +import { Grid, IconButton } from "@material-ui/core"; +import { TextField, Select, MenuItem, InputLabel } from "@material-ui/core"; +import AddIcon from "@material-ui/icons/Add"; +import CardContent from "@material-ui/core/CardContent"; +import Card from "@material-ui/core/Card"; +import apiService from "../../services/apiService"; +import DeleteIcon from "@material-ui/icons/Delete"; +import { Formik } from "formik"; +import Notification from "../../common/Notification"; + +function NetworkForm({ formikProps, ...props }) { + const [clusters, setClusters] = useState(props.clusters); + const [notificationDetails, setNotificationDetails] = useState({}); + const useStyles = makeStyles({ + root: { + minWidth: 275, + }, + title: { + fontSize: 14, + }, + pos: { + marginBottom: 12, + }, + }); + + const handleAddNetworkInterface = (providerIndex, clusterIndex, values) => { + let updatedFields = []; + if ( + values.apps[props.index].clusters[providerIndex].selectedClusters[ + clusterIndex + ].interfaces + ) { + updatedFields = [ + ...values.apps[props.index].clusters[providerIndex].selectedClusters[ + clusterIndex + ].interfaces, + { + networkName: "", + ip: "", + subnet: "", + }, + ]; + } else { + updatedFields = [ + { + networkName: "", + ip: "", + subnet: "", + }, + ]; + } + + let request = { + providerName: values.apps[props.index].clusters[providerIndex].provider, + clusterName: + values.apps[props.index].clusters[providerIndex].selectedClusters[ + clusterIndex + ].name, + }; + apiService + .getClusterProviderNetworks(request) + .then((networks) => { + let networkData = []; + if (networks && networks.length > 0) { + networks.forEach((network) => { + networkData.push({ + name: network.metadata.name, + subnets: network.spec.ipv4Subnets, + }); + }); + } + + apiService + .getClusterNetworks(request) + .then((clusterNetworks) => { + if (clusterNetworks && clusterNetworks.length > 0) { + clusterNetworks.forEach((clusterNetwork) => { + networkData.push({ + name: clusterNetwork.metadata.name, + subnets: clusterNetwork.spec.ipv4Subnets, + }); + }); + } + //add interface entry onyl of there is atlease one available network + if (networkData.length > 0) { + setClusters((clusters) => { + clusters[providerIndex].selectedClusters[ + clusterIndex + ].interfaces = updatedFields; + clusters[providerIndex].selectedClusters[ + clusterIndex + ].networks = networkData; + clusters[providerIndex].selectedClusters[ + clusterIndex + ].availableNetworks = getAvailableNetworks( + clusters[providerIndex].selectedClusters[clusterIndex] + ); + return clusters; + }); + formikProps.setFieldValue( + `apps[${props.index}].clusters[${providerIndex}].selectedClusters[${clusterIndex}].interfaces`, + updatedFields + ); + } else { + setNotificationDetails({ + show: true, + message: `No network available for this cluster`, + severity: "warning", + }); + } + }) + .catch((err) => { + console.log("error getting cluster networks : ", err); + }); + }) + .catch((err) => { + console.log("error getting cluster provider networks : ", err); + }) + .finally(() => { + return updatedFields; + }); + }; + + const handleSelectNetowrk = ( + e, + providerIndex, + clusterIndex, + interfaceIndex + ) => { + setClusters((clusters) => { + clusters[providerIndex].selectedClusters[clusterIndex].interfaces[ + interfaceIndex + ] = { + networkName: e.target.value, + ip: "", + subnet: "", + }; + clusters[providerIndex].selectedClusters[ + clusterIndex + ].availableNetworks = getAvailableNetworks( + clusters[providerIndex].selectedClusters[clusterIndex], + "handleAddNetworkInterface" + ); + return clusters; + }); + formikProps.handleChange(e); + }; + const handleRemoveNetwork = (providerIndex, clusterIndex, interfaceIndex) => { + setClusters((clusters) => { + clusters[providerIndex].selectedClusters[clusterIndex].interfaces.splice( + interfaceIndex, + 1 + ); + clusters[providerIndex].selectedClusters[ + clusterIndex + ].availableNetworks = getAvailableNetworks( + clusters[providerIndex].selectedClusters[clusterIndex] + ); + return clusters; + }); + formikProps.setFieldValue( + `apps[${props.index}].clusters[${providerIndex}].selectedClusters[${clusterIndex}].interfaces`, + clusters[providerIndex].selectedClusters[clusterIndex].interfaces + ); + }; + const getAvailableNetworks = (cluster) => { + let availableNetworks = []; + cluster.networks.forEach((network) => { + let match = false; + cluster.interfaces.forEach((networkInterface) => { + if (network.name === networkInterface.networkName) { + match = true; + return; + } + }); + if (!match) availableNetworks.push(network); + }); + return availableNetworks; + }; + + const classes = useStyles(); + return ( + <> + <Notification notificationDetails={notificationDetails} /> + <Grid + key="networkForm" + container + spacing={3} + style={{ + height: "400px", + overflowY: "auto", + width: "100%", + scrollbarWidth: "thin", + }} + > + {(!clusters || clusters.length < 1) && ( + <Grid item xs={12}> + <Typography variant="h6">No clusters selected</Typography> + </Grid> + )} + {clusters && + clusters.map((cluster, providerIndex) => ( + <Grid key={cluster.provider + providerIndex} item xs={12}> + <Card className={classes.root}> + <CardContent> + <Grid container spacing={2}> + <Grid item xs={12}> + <Typography + className={classes.title} + color="textSecondary" + gutterBottom + > + {cluster.provider} + </Typography> + </Grid> + {cluster.selectedClusters.map( + (selectedCluster, clusterIndex) => ( + <React.Fragment key={selectedCluster.name}> + <Grid item xs={12}> + <Typography>{selectedCluster.name}</Typography> + </Grid> + <Formik> + {() => { + const { + values, + errors, + handleChange, + handleBlur, + } = formikProps; + return ( + <> + {selectedCluster.interfaces && + selectedCluster.interfaces.length > 0 + ? selectedCluster.interfaces.map( + (networkInterface, interfaceIndex) => ( + <Grid + spacing={1} + container + item + key={interfaceIndex} + xs={12} + > + <Grid item xs={4}> + <InputLabel id="network-select-label"> + Network + </InputLabel> + <Select + fullWidth + labelId="network-select-label" + id="network-select" + name={`apps[${props.index}].clusters[${providerIndex}].selectedClusters[${clusterIndex}].interfaces[${interfaceIndex}].networkName`} + value={ + values.apps[props.index] + .clusters[providerIndex] + .selectedClusters[ + clusterIndex + ].interfaces[interfaceIndex] + .networkName + } + onChange={(e) => { + handleSelectNetowrk( + e, + providerIndex, + clusterIndex, + interfaceIndex + ); + }} + > + {values.apps[props.index] + .clusters[providerIndex] + .selectedClusters[ + clusterIndex + ].interfaces[interfaceIndex] + .networkName && ( + <MenuItem + key={ + values.apps[props.index] + .clusters[providerIndex] + .selectedClusters[ + clusterIndex + ].interfaces[ + interfaceIndex + ].networkName + } + value={ + values.apps[props.index] + .clusters[providerIndex] + .selectedClusters[ + clusterIndex + ].interfaces[ + interfaceIndex + ].networkName + } + > + { + values.apps[props.index] + .clusters[providerIndex] + .selectedClusters[ + clusterIndex + ].interfaces[ + interfaceIndex + ].networkName + } + </MenuItem> + )} + {selectedCluster.availableNetworks && + selectedCluster.availableNetworks.map( + (network) => ( + <MenuItem + key={network.name} + value={network.name} + > + {network.name} + </MenuItem> + ) + )} + </Select> + </Grid> + + <Grid item xs={4}> + <InputLabel id="subnet-select-label"> + Subnet + </InputLabel> + <Select + fullWidth + labelId="subnet-select-label" + id="subnet-select-label" + name={`apps[${props.index}].clusters[${providerIndex}].selectedClusters[${clusterIndex}].interfaces[${interfaceIndex}].subnet`} + value={ + values.apps[props.index] + .clusters[providerIndex] + .selectedClusters[ + clusterIndex + ].interfaces[interfaceIndex] + .subnet + } + onChange={handleChange} + > + {values.apps[props.index] + .clusters[providerIndex] + .selectedClusters[ + clusterIndex + ].interfaces[interfaceIndex] + .networkName === "" + ? null + : selectedCluster.networks + .filter( + (network) => + network.name === + values.apps[ + props.index + ].clusters[ + providerIndex + ].selectedClusters[ + clusterIndex + ].interfaces[ + interfaceIndex + ].networkName + )[0] + .subnets.map((subnet) => ( + <MenuItem + key={subnet.name} + value={subnet.name} + > + {subnet.name}( + {subnet.subnet}) + </MenuItem> + ))} + </Select> + </Grid> + <Grid item xs={3}> + <TextField + width={"65%"} + name={`apps[${props.index}].clusters[${providerIndex}].selectedClusters[${clusterIndex}].interfaces[${interfaceIndex}].ip`} + onBlur={handleBlur} + id="ip" + label="IP Address" + value={ + values.apps[props.index] + .clusters[providerIndex] + .selectedClusters[ + clusterIndex + ].interfaces[interfaceIndex] + .ip + } + onChange={handleChange} + helperText={ + (errors.apps && + errors.apps[props.index] && + errors.apps[props.index] + .clusters && + errors.apps[props.index] + .clusters[clusterIndex] && + errors.apps[props.index] + .clusters[clusterIndex] + .selectedClusters[ + clusterIndex + ] && + errors.apps[props.index] + .clusters[clusterIndex] + .selectedClusters[ + clusterIndex + ].interfaces[ + interfaceIndex + ] && + errors.apps[props.index] + .clusters[clusterIndex] + .selectedClusters[ + clusterIndex + ].interfaces[interfaceIndex] + .ip) || + "blank for auto assign" + } + error={ + errors.apps && + errors.apps[props.index] && + errors.apps[props.index] + .clusters && + errors.apps[props.index] + .clusters[clusterIndex] && + errors.apps[props.index] + .clusters[clusterIndex] + .selectedClusters[ + clusterIndex + ] && + errors.apps[props.index] + .clusters[clusterIndex] + .selectedClusters[ + clusterIndex + ].interfaces[ + interfaceIndex + ] && + errors.apps[props.index] + .clusters[clusterIndex] + .selectedClusters[ + clusterIndex + ].interfaces[interfaceIndex] + .ip && + true + } + /> + </Grid> + <Grid item xs={1}> + <IconButton + color="secondary" + onClick={() => { + handleRemoveNetwork( + providerIndex, + clusterIndex, + interfaceIndex + ); + }} + > + <DeleteIcon fontSize="small" /> + </IconButton> + </Grid> + </Grid> + ) + ) + : null} + <Grid + key={selectedCluster.name + "addButton"} + item + xs={12} + > + <Button + variant="outlined" + size="small" + fullWidth + color="primary" + disabled={ + selectedCluster.interfaces && + selectedCluster.interfaces.length > 0 && + selectedCluster.networks.length === + selectedCluster.interfaces.length + } + onClick={() => { + handleAddNetworkInterface( + providerIndex, + clusterIndex, + values + ); + }} + startIcon={<AddIcon />} + > + Add Network Interface + </Button> + </Grid> + </> + ); + }} + </Formik> + </React.Fragment> + ) + )} + </Grid> + </CardContent> + </Card> + </Grid> + ))} + </Grid> + </> + ); +} + +export default NetworkForm; diff --git a/src/tools/emcoui/src/compositeApps/dialogs/CompositeAppForm.jsx b/src/tools/emcoui/src/compositeApps/dialogs/CompositeAppForm.jsx index 29e17cd7..751ea8eb 100644 --- a/src/tools/emcoui/src/compositeApps/dialogs/CompositeAppForm.jsx +++ b/src/tools/emcoui/src/compositeApps/dialogs/CompositeAppForm.jsx @@ -11,185 +11,298 @@ // 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. -// ======================================================================== -import React from "react"; -import { withStyles } from "@material-ui/core/styles"; +// ======================================================================== +import React, { useState } from "react"; import Button from "@material-ui/core/Button"; import Dialog from "@material-ui/core/Dialog"; -import MuiDialogTitle from "@material-ui/core/DialogTitle"; -import MuiDialogContent from "@material-ui/core/DialogContent"; -import MuiDialogActions from "@material-ui/core/DialogActions"; +import AppBar from "@material-ui/core/AppBar"; +import Toolbar from "@material-ui/core/Toolbar"; +import IconButton from "@material-ui/core/IconButton"; import Typography from "@material-ui/core/Typography"; -import { TextField } from '@material-ui/core'; -const styles = (theme) => ({ - root: { - margin: 0, - padding: theme.spacing(2), +import CloseIcon from "@material-ui/icons/Close"; +import Slide from "@material-ui/core/Slide"; +import { Grid } from "@material-ui/core"; +import { TextField } from "@material-ui/core"; +import { makeStyles } from "@material-ui/core/styles"; +import AddIcon from "@material-ui/icons/Add"; +import NewAppForm from "../../common/Form"; +import AppForm from "./AppForm"; +import { Formik, FieldArray } from "formik"; +import * as Yup from "yup"; + +const Transition = React.forwardRef(function Transition(props, ref) { + return <Slide direction="up" ref={ref} {...props} />; +}); + +const useStyles = makeStyles((theme) => ({ + tableRoot: { + width: "100%", }, - closeButton: { + paper: { + width: "100%", + marginBottom: theme.spacing(2), + }, + table: { + minWidth: 550, + }, + visuallyHidden: { + border: 0, + clip: "rect(0 0 0 0)", + height: 1, + margin: -1, + overflow: "hidden", + padding: 0, position: "absolute", - right: theme.spacing(1), - top: theme.spacing(1), - color: theme.palette.grey[500], + top: 20, + width: 1, }, -}); - - -const DialogContent = withStyles((theme) => ({ - root: { - padding: theme.spacing(2), + appBar: { + position: "relative", + }, + title: { + marginLeft: theme.spacing(2), + flex: 1, + }, + demo: { + backgroundColor: theme.palette.background.paper, }, -}))(MuiDialogContent); - -const DialogActions = withStyles((theme) => ({ root: { - margin: 0, - padding: theme.spacing(1), + flexGrow: 1, + backgroundColor: theme.palette.background.paper, + display: "flex", + height: 424, }, -}))(MuiDialogActions); - + tabs: { + borderRight: `1px solid ${theme.palette.divider}`, + }, +})); -class CreateCompositeAppForm extends React.Component { - constructor(props) { - super(props) - this.state = { - fields: { name: "", version: "", description: "" }, - errors: {} - } - this.handleChange = this.handleChange.bind(this); - this.submituserRegistrationForm = this.submituserRegistrationForm.bind(this); - } +const PROFILE_SUPPORTED_FORMATS = [ + ".tgz", + ".tar.gz", + ".tar", + "application/x-tar", + "application/x-tgz", + "application/x-compressed", + "application/x-gzip", + "application/x-compressed-tar", + "application/gzip", +]; +const APP_PACKAGE_SUPPORTED_FORMATS = [ + ".tgz", + ".tar.gz", + ".tar", + "application/x-tar", + "application/x-tgz", + "application/x-compressed", + "application/x-gzip", + "application/x-compressed-tar", +]; +const serviceBasicValidationSchema = Yup.object({ + name: Yup.string().required(), + description: Yup.string(), + apps: Yup.array() + .of( + Yup.object({ + appName: Yup.string().required("App name is required"), + file: Yup.mixed() + .required("An app package file is required") + .test( + "fileFormat", + "Unsupported file format", + (value) => + value && APP_PACKAGE_SUPPORTED_FORMATS.includes(value.type) + ), + profilePackageFile: Yup.mixed() + .required("A profile package file is required") + .test( + "fileFormat", + "Unsupported file format", + (value) => value && PROFILE_SUPPORTED_FORMATS.includes(value.type) + ), + }) + ) + .required("At least one app is required"), +}); - componentDidMount = () => { - if (this.props.item) { - this.title = "Edit Composite App"; - this.buttonLabel = "Update"; - this.isEdit = true; - } - else { - this.title = "New Composite App"; - this.buttonLabel = "Create"; - this.isEdit = false; - } +const CreateCompositeAppForm = ({ open, handleClose }) => { + const classes = useStyles(); + const [openForm, setOpenForm] = useState(false); + const handleCloseForm = () => { + setOpenForm(false); }; - - componentDidUpdate = (prevProps, prevState) => { - if (this.props.item && ((prevProps.item !== this.props.item))) { - this.setState({ fields: { ...this.props.item.metadata, version: this.props.item.spec.version } }); - } - } - - resetFields = () => { - if (!this.isEdit) { - this.setState({ - fields: { name: "", version: "", description: "" }, - errors: {} - }); - } - else { - this.setState({ fields: { ...this.props.item.metadata, version: this.props.item.spec.version } }); - } - } - - handleClose = () => { - this.resetFields(); - this.props.handleClose(); + const handleAddApp = () => { + setOpenForm(true); }; - - submituserRegistrationForm(e) { - e.preventDefault(); - if (this.validateForm()) { - this.resetFields(); - this.props.handleClose(this.state.fields); - } - } - - validateForm() { - let fields = this.state.fields; - let errors = {}; - let formIsValid = true; - - if (!fields["name"]) { - formIsValid = false; - errors["name"] = "*Please enter your username."; - } - - if (typeof fields["name"] !== "string") { - if (!fields["name"].match(/^[a-zA-Z ]*$/)) { - formIsValid = false; - errors["name"] = "*Please enter alphabet characters only."; - } - } - this.setState({ - errors: errors - }); - return formIsValid; - } - - handleChange = (e) => { - this.setState({ fields: { ...this.state.fields, [e.target.name]: e.target.value } }); - } - - render = () => { - const { classes } = this.props; - return ( - <> + let initialValues = { name: "", description: "", apps: [] }; + return ( + <> + {open && ( <Dialog - maxWidth={"xs"} - onClose={this.handleClose} - aria-labelledby="customized-dialog-title" - open={this.props.open} - disableBackdropClick + open={open} + onClose={() => { + handleClose(); + }} + fullScreen + TransitionComponent={Transition} > - <MuiDialogTitle disableTypography className={classes.root} > - <Typography variant="h6">{this.title}</Typography> - </MuiDialogTitle> + <Formik + initialValues={initialValues} + onSubmit={(values, { setSubmitting }) => { + setSubmitting(false); + handleClose(values); + }} + validationSchema={serviceBasicValidationSchema} + > + {(props) => { + const { + values, + touched, + errors, + isSubmitting, + handleChange, + handleBlur, + handleSubmit, + } = props; + return ( + <> + <form noValidate onSubmit={handleSubmit}> + <AppBar className={classes.appBar}> + <Toolbar> + <IconButton + edge="start" + color="inherit" + onClick={() => { + handleClose(); + }} + aria-label="close" + > + <CloseIcon /> + </IconButton> + <Typography variant="h6" className={classes.title}> + Add Service + </Typography> + <Button + type="submit" + autoFocus + variant="contained" + disabled={isSubmitting} + > + SUBMIT + </Button> + </Toolbar> + </AppBar> + <div style={{ padding: "12px" }}> + <Grid + container + direction="row" + justify="center" + alignItems="center" + style={{ marginTop: "40px" }} + spacing={3} + > + <Grid item xs={6}> + <Grid container spacing={3}> + {errors.apps && + touched.apps && + typeof errors.apps !== "object" && ( + <Grid item xs={12} sm={12}> + <Typography>{errors.apps}</Typography> + </Grid> + )} + + <Grid item xs={12} sm={6}> + <TextField + fullWidth + name="name" + id="input-name" + label="Name" + variant="outlined" + size="small" + value={values.name} + onChange={handleChange} + onBlur={handleBlur} + required + helperText={ + errors.name && + touched.name && + "Name is required" + } + error={errors.name && touched.name} + /> + </Grid> + <Grid item xs={12} sm={6}> + <TextField + fullWidth + name="description" + id="input-description" + label="Description" + variant="outlined" + size="small" + value={values.description} + onChange={handleChange} + onBlur={handleBlur} + /> + </Grid> - <form onSubmit={this.submituserRegistrationForm}> - <DialogContent dividers> - <TextField - style={{ width: "40%", marginBottom: "10px" }} - name="name" - value={this.state.fields.name} - id="input-name" - label="Name" - helperText="Name should be unique" - onChange={this.handleChange} - required - /> - <TextField - style={{ width: "40%", marginBottom: "20px", float: "right" }} - name="version" - value={this.state.fields.version} - onChange={this.handleChange} - id="input-version" - label="Version" - required - /> - <TextField - style={{ width: "100%", marginBottom: "25px" }} - name="description" - value={this.state.fields.description} - onChange={this.handleChange} - id="input-description" - label="Description" - multiline - rowsMax={4} - /> - </DialogContent> - <DialogActions> - <Button autoFocus onClick={this.handleClose} color="secondary"> - Cancel - </Button> - <Button autoFocus type="submit" color="primary"> - {this.buttonLabel} - </Button> - </DialogActions> - </form> + <FieldArray + name="apps" + render={(arrayHelpers) => ( + <> + <NewAppForm + open={openForm} + onClose={handleCloseForm} + onSubmit={(values) => { + arrayHelpers.push({ + appName: values.name, + description: values.description, + }); + setOpenForm(false); + }} + /> + {values.apps && + values.apps.length > 0 && + values.apps.map((app, index) => ( + <Grid key={index} item sm={12} xs={12}> + <AppForm + formikProps={props} + name={app.appName} + description={app.description} + index={index} + initialValues={values} + /> + </Grid> + ))} + </> + )} + /> + <Grid item xs={12}> + <Button + variant="outlined" + size="small" + fullWidth + color="primary" + onClick={() => { + handleAddApp(); + }} + startIcon={<AddIcon />} + > + Add App + </Button> + </Grid> + </Grid> + </Grid> + </Grid> + </div> + </form> + </> + ); + }} + </Formik> </Dialog> - </> - ); - } -} -export default withStyles(styles)(CreateCompositeAppForm) + )} + </> + ); +}; +export default CreateCompositeAppForm; diff --git a/src/tools/emcoui/src/compositeApps/dialogs/SortableTable.jsx b/src/tools/emcoui/src/compositeApps/dialogs/SortableTable.jsx new file mode 100644 index 00000000..f1a6ac2d --- /dev/null +++ b/src/tools/emcoui/src/compositeApps/dialogs/SortableTable.jsx @@ -0,0 +1,410 @@ +//======================================================================= +// Copyright (c) 2017-2020 Aarna Networks, Inc. +// All rights reserved. +// ====================================================================== +// 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. +// ======================================================================== +import Table from "@material-ui/core/Table"; +import TableBody from "@material-ui/core/TableBody"; +import TableCell from "@material-ui/core/TableCell"; +import TableContainer from "@material-ui/core/TableContainer"; +import TableHead from "@material-ui/core/TableHead"; +import TableRow from "@material-ui/core/TableRow"; +import TableSortLabel from "@material-ui/core/TableSortLabel"; +import { makeStyles } from "@material-ui/core/styles"; +import React, { useEffect, useState } from "react"; +import Checkbox from "@material-ui/core/Checkbox"; +import PropTypes from "prop-types"; +import Typography from "@material-ui/core/Typography"; +import Toolbar from "@material-ui/core/Toolbar"; + +import clsx from "clsx"; +import TablePagination from "@material-ui/core/TablePagination"; +import { lighten } from "@material-ui/core/styles"; + +function descendingComparator(a, b, orderBy) { + if (b[orderBy] < a[orderBy]) { + return -1; + } + if (b[orderBy] > a[orderBy]) { + return 1; + } + return 0; +} + +function getComparator(order, orderBy) { + return order === "desc" + ? (a, b) => descendingComparator(a, b, orderBy) + : (a, b) => -descendingComparator(a, b, orderBy); +} + +function stableSort(array, comparator) { + const stabilizedThis = array.map((el, index) => [el, index]); + stabilizedThis.sort((a, b) => { + const order = comparator(a[0], b[0]); + if (order !== 0) return order; + return a[1] - b[1]; + }); + return stabilizedThis.map((el) => el[0]); +} + +const headCells = [ + { + id: "name", + numeric: false, + sortable: true, + disablePadding: true, + label: "Cluster", + }, + { + id: "description", + numeric: true, + sortable: false, + disablePadding: false, + label: "Description", + }, +]; + +function EnhancedTableHead(props) { + const { + classes, + onSelectAllClick, + order, + orderBy, + numSelected, + rowCount, + onRequestSort, + } = props; + const createSortHandler = (property) => (event) => { + onRequestSort(event, property); + }; + + return ( + <TableHead> + <TableRow> + <TableCell padding="checkbox"> + <Checkbox + indeterminate={numSelected > 0 && numSelected < rowCount} + checked={rowCount > 0 && numSelected === rowCount} + onChange={onSelectAllClick} + /> + </TableCell> + {headCells.map((headCell) => + headCell.sortable ? ( + <TableCell + style={{ fontWeight: "520" }} + key={headCell.id} + align={headCell.numeric ? "right" : "left"} + padding={headCell.disablePadding ? "none" : "default"} + sortDirection={orderBy === headCell.id ? order : false} + > + <TableSortLabel + active={orderBy === headCell.id} + direction={orderBy === headCell.id ? order : "asc"} + onClick={createSortHandler(headCell.id)} + > + {headCell.label} + {orderBy === headCell.id ? ( + <span className={classes.visuallyHidden}> + {order === "desc" + ? "sorted descending" + : "sorted ascending"} + </span> + ) : null} + </TableSortLabel> + </TableCell> + ) : ( + <TableCell + key={headCell.id} + style={{ fontWeight: "520" }} + padding={headCell.disablePadding ? "none" : "default"} + align={headCell.numeric ? "right" : "left"} + > + {headCell.label} + </TableCell> + ) + )} + </TableRow> + </TableHead> + ); +} + +EnhancedTableHead.propTypes = { + classes: PropTypes.object.isRequired, + numSelected: PropTypes.number.isRequired, + onRequestSort: PropTypes.func.isRequired, + onSelectAllClick: PropTypes.func.isRequired, + order: PropTypes.oneOf(["asc", "desc"]).isRequired, + orderBy: PropTypes.string.isRequired, + rowCount: PropTypes.number.isRequired, +}; + +const useToolbarStyles = makeStyles((theme) => ({ + root: { + paddingLeft: theme.spacing(2), + paddingRight: theme.spacing(1), + }, + highlight: + theme.palette.type === "light" + ? { + color: theme.palette.primary.main, + backgroundColor: lighten(theme.palette.primary.light, 0.85), + } + : { + color: theme.palette.text.primary, + backgroundColor: theme.palette.primary.dark, + }, + title: { + flex: "1 1 100%", + }, +})); + +const EnhancedTableToolbar = (props) => { + const classes = useToolbarStyles(); + const { numSelected } = props; + + return ( + <Toolbar + className={clsx(classes.root, { + [classes.highlight]: numSelected > 0, + })} + > + <Typography + className={classes.title} + variant="h6" + id="tableTitle" + component="div" + > + {props.tableName} + </Typography> + + <Typography + className={classes.title} + style={{ textAlign: "right" }} + color="inherit" + variant="subtitle1" + component="div" + > + {numSelected} selected + </Typography> + </Toolbar> + ); +}; + +EnhancedTableToolbar.propTypes = { + numSelected: PropTypes.number.isRequired, +}; + +const useStyles = makeStyles((theme) => ({ + tableRoot: { + width: "100%", + }, + paper: { + width: "100%", + marginBottom: theme.spacing(2), + }, + table: { + minWidth: 550, + }, + visuallyHidden: { + border: 0, + clip: "rect(0 0 0 0)", + height: 1, + margin: -1, + overflow: "hidden", + padding: 0, + position: "absolute", + top: 20, + width: 1, + }, + appBar: { + position: "relative", + }, + title: { + marginLeft: theme.spacing(2), + flex: 1, + }, + demo: { + backgroundColor: theme.palette.background.paper, + }, + root: { + flexGrow: 1, + backgroundColor: theme.palette.background.paper, + display: "flex", + height: 424, + }, + tabs: { + borderRight: `1px solid ${theme.palette.divider}`, + }, +})); + +function EnhancedTable({ + clusters, + formikValues, + tableName, + onRowSelect, + ...props +}) { + const classes = useStyles(); + const [order, setOrder] = useState("asc"); + const [orderBy, setOrderBy] = useState("name"); + const [selected, setSelected] = useState([]); + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(5); + const [rows, setRows] = useState([]); + + const handleRequestSort = (event, property) => { + const isAsc = orderBy === property && order === "asc"; + setOrder(isAsc ? "desc" : "asc"); + setOrderBy(property); + }; + + useEffect(() => { + if (formikValues) { + let formikClusterData = formikValues.filter( + (cluster) => cluster.provider === tableName + ); + if (formikClusterData && formikClusterData.length > 0) { + let data = []; + formikClusterData[0].selectedClusters.forEach((selectedCluster) => { + data.push(selectedCluster.name); + }); + setSelected(data); + } + } + setRows(clusters); + }, []); + + useEffect(() => { + onRowSelect(tableName, selected); + }, [selected]); + + const handleSelectAllClick = (event) => { + if (event.target.checked) { + const newSelecteds = rows.map((n) => n.name); + setSelected(newSelecteds); + return; + } + setSelected([]); + }; + + const handleClick = (event, name) => { + const selectedIndex = selected.indexOf(name); + let newSelected = []; + + if (selectedIndex === -1) { + newSelected = newSelected.concat(selected, name); + } else if (selectedIndex === 0) { + newSelected = newSelected.concat(selected.slice(1)); + } else if (selectedIndex === selected.length - 1) { + newSelected = newSelected.concat(selected.slice(0, -1)); + } else if (selectedIndex > 0) { + newSelected = newSelected.concat( + selected.slice(0, selectedIndex), + selected.slice(selectedIndex + 1) + ); + } + setSelected(newSelected); + }; + + const handleChangePage = (event, newPage) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event) => { + setRowsPerPage(parseInt(event.target.value, 10)); + setPage(0); + }; + + const isSelected = (name) => selected.indexOf(name) !== -1; + + const emptyRows = + rowsPerPage - Math.min(rowsPerPage, rows.length - page * rowsPerPage); + + return ( + <div className={classes.tableRoot}> + <EnhancedTableToolbar + tableName={tableName} + numSelected={selected.length} + /> + <TableContainer> + <Table + className={classes.table} + aria-labelledby="tableTitle" + size={"small"} + aria-label="enhanced table" + > + <EnhancedTableHead + classes={classes} + numSelected={selected.length} + order={order} + orderBy={orderBy} + onSelectAllClick={handleSelectAllClick} + onRequestSort={handleRequestSort} + rowCount={rows.length} + /> + <TableBody> + {stableSort(rows, getComparator(order, orderBy)) + .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) + .map((row, index) => { + const isItemSelected = isSelected(row.name); + const labelId = `enhanced-table-checkbox-${index}`; + + return ( + <TableRow + hover + onClick={(event) => handleClick(event, row.name)} + role="checkbox" + aria-checked={isItemSelected} + tabIndex={-1} + key={row.name} + selected={isItemSelected} + > + <TableCell padding="checkbox"> + <Checkbox + checked={isItemSelected} + inputProps={{ "aria-labelledby": labelId }} + /> + </TableCell> + <TableCell + component="th" + id={labelId} + scope="row" + padding="none" + > + {row.name} + </TableCell> + <TableCell align="right">{row.description}</TableCell> + </TableRow> + ); + })} + {emptyRows > 0 && ( + <TableRow style={{ height: 33 * emptyRows }}> + <TableCell colSpan={6} /> + </TableRow> + )} + </TableBody> + </Table> + </TableContainer> + <TablePagination + rowsPerPageOptions={[5, 10, 25]} + component="div" + count={rows.length} + rowsPerPage={rowsPerPage} + page={page} + onChangePage={handleChangePage} + onChangeRowsPerPage={handleChangeRowsPerPage} + /> + </div> + ); +} + +export default EnhancedTable; diff --git a/src/tools/emcoui/src/compositeApps/intents/AppPlacementIntentTable.jsx b/src/tools/emcoui/src/compositeApps/intents/AppPlacementIntentTable.jsx index 6dfb8279..09f70c99 100644 --- a/src/tools/emcoui/src/compositeApps/intents/AppPlacementIntentTable.jsx +++ b/src/tools/emcoui/src/compositeApps/intents/AppPlacementIntentTable.jsx @@ -11,7 +11,7 @@ // 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. -// ======================================================================== +// ======================================================================== import React, { useState } from "react"; import { TableContainer, @@ -24,7 +24,7 @@ import { } from "@material-ui/core"; import Paper from "@material-ui/core/Paper"; import TableBody from "@material-ui/core/TableBody"; -import EditIcon from "@material-ui/icons/Edit"; +// import EditIcon from "@material-ui/icons/Edit"; import DeleteIcon from "@material-ui/icons/Delete"; import PropTypes from "prop-types"; import apiService from "../../services/apiService"; @@ -106,31 +106,42 @@ const AppPlacementIntentTable = ({ data, setData, ...props }) => { <StyledTableCell>{entry.name}</StyledTableCell> <StyledTableCell>{entry.description}</StyledTableCell> <StyledTableCell> - {entry.allOf.map((intent, index) => ( - <Paper - key={index} - style={{ width: "max-content" }} - variant="outlined" - > - <label>Cluster Provider : </label> - <label style={{ fontWeight: "bold" }}> - {intent["provider-name"]}, - </label> - <label>Labels : </label> - <Chip - style={{ marginRight: "10px" }} - size="small" - label={intent["cluster-label-name"]} - color="primary" + {entry.allOf && + entry.allOf.map((intent, index) => ( + <Paper + key={index} + style={{ width: "max-content" }} variant="outlined" - /> - </Paper> - ))} + > + <label>Cluster Provider : </label> + <label style={{ fontWeight: "bold" }}> + {intent["provider-name"]} + </label> + <label>, Cluster : </label> + <label style={{ fontWeight: "bold" }}> + {intent["cluster-name"]} + </label> + {intent["cluster-label-name"] && ( + <> + <label>, Labels : </label> + <Chip + style={{ marginRight: "10px" }} + size="small" + label={intent["cluster-label-name"]} + color="primary" + variant="outlined" + /> + </> + )} + </Paper> + ))} </StyledTableCell> <StyledTableCell> + {/* + //edit app placement api has not been implemented yet <IconButton onClick={(e) => handleEdit(index)} title="Edit"> <EditIcon color="primary" /> - </IconButton> + </IconButton> */} <IconButton onClick={(e) => handleDelete(index)} title="Delete" diff --git a/src/tools/emcoui/src/compositeApps/intents/GenericPlacementIntentCard.jsx b/src/tools/emcoui/src/compositeApps/intents/GenericPlacementIntentCard.jsx index bb43972c..9946c92d 100644 --- a/src/tools/emcoui/src/compositeApps/intents/GenericPlacementIntentCard.jsx +++ b/src/tools/emcoui/src/compositeApps/intents/GenericPlacementIntentCard.jsx @@ -11,7 +11,7 @@ // 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. -// ======================================================================== +// ======================================================================== import React, { useState } from "react"; import { makeStyles } from "@material-ui/core/styles"; import clsx from "clsx"; @@ -168,6 +168,10 @@ const GenericPlacementIntentCard = (props) => { variant="outlined" size="small" color="secondary" + disabled={ + appPlacementIntentData.applications && + appPlacementIntentData.applications.length > 0 + } style={{ float: "right" }} startIcon={<DeleteIcon />} onClick={() => { @@ -190,6 +194,9 @@ const GenericPlacementIntentCard = (props) => { } /> )} + {!(props.appsData && props.appsData.length > 0) && ( + <div>No app found for adding app placement intent</div> + )} </CardContent> </Collapse> </Card> diff --git a/src/tools/emcoui/src/deploymentIntentGroups/DIGform.jsx b/src/tools/emcoui/src/deploymentIntentGroups/DIGform.jsx index f0cf1e1d..ee1fec74 100644 --- a/src/tools/emcoui/src/deploymentIntentGroups/DIGform.jsx +++ b/src/tools/emcoui/src/deploymentIntentGroups/DIGform.jsx @@ -11,245 +11,112 @@ // 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. -// ======================================================================== -import React, { useState, useEffect } from 'react'; -import PropTypes from 'prop-types'; -import { withStyles } from '@material-ui/core/styles'; -import Button from '@material-ui/core/Button'; -import Dialog from '@material-ui/core/Dialog'; -import MuiDialogTitle from '@material-ui/core/DialogTitle'; -import MuiDialogContent from '@material-ui/core/DialogContent'; -import MuiDialogActions from '@material-ui/core/DialogActions'; -import IconButton from '@material-ui/core/IconButton'; -import CloseIcon from '@material-ui/icons/Close'; -import Typography from '@material-ui/core/Typography'; -import { TextField, InputLabel, NativeSelect, FormControl, FormHelperText } from '@material-ui/core'; -import * as Yup from "yup"; -import { Formik } from 'formik'; +// ======================================================================== +import React, { useState, useEffect } from "react"; +import PropTypes from "prop-types"; +import { withStyles } from "@material-ui/core/styles"; +import Button from "@material-ui/core/Button"; +import Dialog from "@material-ui/core/Dialog"; +import MuiDialogTitle from "@material-ui/core/DialogTitle"; +import MuiDialogContent from "@material-ui/core/DialogContent"; +import MuiDialogActions from "@material-ui/core/DialogActions"; +import IconButton from "@material-ui/core/IconButton"; +import CloseIcon from "@material-ui/icons/Close"; +import Typography from "@material-ui/core/Typography"; +import Stepper from "./Stepper"; import apiService from "../services/apiService"; const styles = (theme) => ({ - root: { - margin: 0, - padding: theme.spacing(2), - }, - closeButton: { - position: 'absolute', - right: theme.spacing(1), - top: theme.spacing(1), - color: theme.palette.grey[500], - }, + root: { + margin: 0, + padding: theme.spacing(2), + }, + closeButton: { + position: "absolute", + right: theme.spacing(1), + top: theme.spacing(1), + color: theme.palette.grey[500], + }, }); const DialogTitle = withStyles(styles)((props) => { - const { children, classes, onClose, ...other } = props; - return ( - <MuiDialogTitle disableTypography className={classes.root} {...other}> - <Typography variant="h6">{children}</Typography> - {onClose ? ( - <IconButton className={classes.closeButton} onClick={onClose}> - <CloseIcon /> - </IconButton> - ) : null} - </MuiDialogTitle> - ); + const { children, classes, onClose, ...other } = props; + return ( + <MuiDialogTitle disableTypography className={classes.root} {...other}> + <Typography variant="h6">{children}</Typography> + {onClose ? ( + <IconButton className={classes.closeButton} onClick={onClose}> + <CloseIcon /> + </IconButton> + ) : null} + </MuiDialogTitle> + ); }); const DialogActions = withStyles((theme) => ({ - root: { - margin: 0, - padding: theme.spacing(1), - }, + root: { + margin: 0, + padding: theme.spacing(1), + }, }))(MuiDialogActions); const DialogContent = withStyles((theme) => ({ - root: { - padding: theme.spacing(2), - } + root: { + padding: theme.spacing(2), + }, }))(MuiDialogContent); -const schema = Yup.object( - { - name: Yup.string().required(), - description: Yup.string(), - version: Yup.string().required(), - compositeProfile: Yup.string().required(), - overrideValues: Yup.array().of(Yup.object()).typeError("Invalid override values, expected array"), - }) - const DIGform = (props) => { - const { onClose, item, open, onSubmit } = props; - const buttonLabel = item ? "OK" : "Create"; - const title = item ? "Edit Deployment Intent Group" : "Create Deployment Intent Group"; - const [selectedAppIndex, setSelectedAppIndex] = useState(0); - const handleClose = () => { - onClose(); - }; - useEffect(() => { - props.data.compositeApps.forEach(compositeApp => { - let request = { projectName: props.projectName, compositeAppName: compositeApp.metadata.name, compositeAppVersion: compositeApp.spec.version } - apiService.getCompositeProfiles(request).then(res => { - compositeApp.profiles = res; - }).catch(error => { - console.log("error getting cluster providers : ", error) - }).finally(() => { - }) + const { onClose, item, open, onSubmit } = props; + const title = item + ? "Edit Deployment Intent Group" + : "Create Deployment Intent Group"; + const handleClose = () => { + onClose(); + }; + useEffect(() => { + props.data.compositeApps.forEach((compositeApp) => { + let request = { + projectName: props.projectName, + compositeAppName: compositeApp.metadata.name, + compositeAppVersion: compositeApp.spec.version, + }; + apiService + .getCompositeProfiles(request) + .then((res) => { + compositeApp.profiles = res; }) - }, [props.data.compositeApps, props.projectName]); - let initialValues = item ? - { name: item.metadata.name, description: item.metadata.description, overrideValues: JSON.stringify(item.spec["override-values"]), compositeApp: item.compositeAppName, compositeProfile: item.spec.profile, version: item.spec.version } : - { name: "", description: "", overrideValues: undefined, compositeApp: props.data.compositeApps[0].metadata.name, compositeProfile: "", version: "" } - - const handleSetCompositeApp = (val) => { - props.data.compositeApps.forEach((ca, index) => { - if (ca.metadata.name === val) - setSelectedAppIndex(index); - }); - } - - return ( - <Dialog maxWidth={"xs"} onClose={handleClose} aria-labelledby="customized-dialog-title" open={open} disableBackdropClick> - <DialogTitle id="simple-dialog-title">{title}</DialogTitle> - <Formik - initialValues={initialValues} - onSubmit={async values => { - values.compositeAppVersion = props.data.compositeApps[selectedAppIndex].spec.version; - onSubmit(values); - }} - validationSchema={schema} - > - {formicProps => { - const { - values, - touched, - errors, - isSubmitting, - handleChange, - handleBlur, - handleSubmit - } = formicProps; - return ( - <form noValidate onSubmit={handleSubmit} onChange={handleChange}> - <DialogContent dividers> - <div style={{ width: "45%", float: "left" }}> - <InputLabel shrink htmlFor="compositeApp-label-placeholder"> - Composite App - </InputLabel> - <NativeSelect - name="compositeApp" - onChange={(e) => { handleChange(e); handleSetCompositeApp(e.target.value) }} - onBlur={handleBlur} - disabled={item ? true : false} - inputProps={{ - name: 'compositeApp', - id: 'compositeApps-label-placeholder', - }} - > - {item && (<option >{values.compositeApp}</option>)} - {props.data && props.data.compositeApps.map(compositeApp => - (<option value={compositeApp.metadata.name} key={compositeApp.metadata.name} >{compositeApp.metadata.name}</option>) - )} - </NativeSelect> - </div> - - <FormControl style={{ width: "45%", float: "right" }} required error={errors.compositeProfile && touched.compositeProfile}> - <InputLabel htmlFor="compositeProfile-label-placeholder"> - Composite Profile - </InputLabel> - <NativeSelect - name="compositeProfile" - onChange={handleChange} - onBlur={handleBlur} - disabled={item ? true : false} - required - inputProps={{ - name: 'compositeProfile', - id: 'compositeProfile-label-placeholder', - }} - > - <option value="" /> - {props.data.compositeApps[selectedAppIndex].profiles && props.data.compositeApps[selectedAppIndex].profiles.map(compositeProfile => - (<option value={compositeProfile.metadata.name} key={compositeProfile.metadata.name} >{compositeProfile.metadata.name}</option>) - )} - </NativeSelect> - {errors.compositeProfile && touched.compositeProfile && <FormHelperText>Required</FormHelperText>} - </FormControl> - <TextField - style={{ width: "45%", float: "left", marginTop: "10px" }} - id="name" - label="Name" - type="text" - value={values.name} - onChange={handleChange} - onBlur={handleBlur} - helperText={(errors.name && touched.name && ( - "Name is required" - ))} - required - error={errors.name && touched.name} - /> - <TextField - style={{ width: "45%", float: "right", marginTop: "10px" }} - id="version" - label="Version" - type="text" - name="version" - onChange={handleChange} - onBlur={handleBlur} - helperText={(errors.version && touched.version && ( - "Version is required" - ))} - required - error={errors.version && touched.version} - /> - <TextField - style={{ width: "100%", marginTop: "20px" }} - id="overrideValues" - label="Override Values" - type="text" - value={values.overrideValues} - onChange={handleChange} - onBlur={handleBlur} - required - multiline - rows={4} - variant="outlined" - error={errors.overrideValues && touched.overrideValues} - helperText={(errors.overrideValues && touched.overrideValues && ( - (errors["overrideValues"]) - ))} - /> - <TextField - style={{ width: "100%", marginBottom: "25px", marginTop: "10px" }} - name="description" - value={values.description} - onChange={handleChange} - onBlur={handleBlur} - id="description" - label="Description" - multiline - rowsMax={4} - /> - </DialogContent> - <DialogActions> - <Button autoFocus onClick={handleClose} color="secondary"> - Cancel - </Button> - <Button autoFocus type="submit" color="primary" disabled={isSubmitting}> - {buttonLabel} - </Button> - </DialogActions> - </form> - ); - }} - </Formik> - </Dialog> - ); + .catch((error) => { + console.log("error getting cluster providers : ", error); + }) + .finally(() => {}); + }); + }, [props.data.compositeApps, props.projectName]); + return ( + <Dialog + maxWidth={"md"} + fullWidth={true} + onClose={handleClose} + open={open} + disableBackdropClick + > + <DialogTitle id="customized-dialog-title" onClose={handleClose}> + {title} + </DialogTitle> + <DialogContent dividers> + <Stepper + data={props.data} + projectName={props.projectName} + onSubmit={onSubmit} + /> + </DialogContent> + </Dialog> + ); }; DIGform.propTypes = { - onClose: PropTypes.func.isRequired, - open: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired, + open: PropTypes.bool.isRequired, }; export default DIGform; diff --git a/src/tools/emcoui/src/deploymentIntentGroups/DIGtable.jsx b/src/tools/emcoui/src/deploymentIntentGroups/DIGtable.jsx index 5710b52b..3e22dc4c 100644 --- a/src/tools/emcoui/src/deploymentIntentGroups/DIGtable.jsx +++ b/src/tools/emcoui/src/deploymentIntentGroups/DIGtable.jsx @@ -11,7 +11,7 @@ // 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. -// ======================================================================== +// ======================================================================== import React, { useState } from "react"; import { withStyles, makeStyles } from "@material-ui/core/styles"; import Table from "@material-ui/core/Table"; @@ -22,14 +22,14 @@ import TableHead from "@material-ui/core/TableHead"; import TableRow from "@material-ui/core/TableRow"; import Paper from "@material-ui/core/Paper"; import IconButton from "@material-ui/core/IconButton"; -import EditIcon from "@material-ui/icons/Edit"; +// import EditIcon from "@material-ui/icons/Edit"; import DeleteDialog from "../common/Dialogue"; -import AddIcon from "@material-ui/icons/Add"; +// import AddIcon from "@material-ui/icons/Add"; import DeleteIcon from "@material-ui/icons/Delete"; import GetAppIcon from "@material-ui/icons/GetApp"; import apiService from "../services/apiService"; -import { Button } from "@material-ui/core"; -import IntentsForm from "./IntentsForm"; +// import { Button } from "@material-ui/core"; +// import IntentsForm from "./IntentsForm"; import Notification from "../common/Notification"; const StyledTableCell = withStyles((theme) => ({ @@ -58,20 +58,15 @@ const useStyles = makeStyles({ export default function DIGtable({ data, setData, ...props }) { const classes = useStyles(); const [open, setOpen] = useState(false); - // const [openForm, setOpenForm] = useState(false); const [index, setIndex] = useState(0); - const [openIntentsForm, setOpenIntentsForm] = useState(false); + // const [openIntentsForm, setOpenIntentsForm] = useState(false); const [notificationDetails, setNotificationDetails] = useState({}); - let handleEdit = (index) => { - // setIndex(index); - // setOpenForm(true); - }; const handleClose = (el) => { if (el.target.innerText === "Delete") { let request = { projectName: props.projectName, - compositeAppName: data[index].compositeAppName, - compositeAppVersion: data[index].compositeAppVersion, + compositeAppName: data[index].metadata.compositeAppName, + compositeAppVersion: data[index].metadata.compositeAppVersion, deploymentIntentGroupName: data[index].metadata.name, }; apiService @@ -92,50 +87,46 @@ export default function DIGtable({ data, setData, ...props }) { setIndex(index); setOpen(true); }; - const handleAddIntent = (index) => { - setIndex(index); - setOpenIntentsForm(true); - }; - const handleCloseIntentsForm = () => { - setOpenIntentsForm(false); - }; - const handleSubmitIntentForm = (values) => { - setOpenIntentsForm(false); - let request = { - projectName: props.projectName, - compositeAppName: values.compositeAppName, - compositeAppVersion: values.compositeAppVersion, - deploymentIntentGroupName: values.deploymentIntentGroupName, - payload: { - metadata: { name: values.name, description: values.description }, - spec: { - intent: { - genericPlacementIntent: values.genericPlacementIntent, - }, - }, - }, - }; - if (values.networkControllerIntent && values.networkControllerIntent !== "") - request.payload.spec.intent.ovnaction = values.networkControllerIntent; - apiService - .addIntentsToDeploymentIntentGroup(request) - .then((res) => { - if (data[index].intent) { - data[index].intent.push(res.spec.intent); - } else { - data[index].intent = [res.spec.intent]; - } - setData([...data]); - }) - .catch((err) => { - console.log("error adding intent to deployment intent group"); - }); - }; + // const handleCloseIntentsForm = () => { + // setOpenIntentsForm(false); + // }; + // const handleSubmitIntentForm = (values) => { + // setOpenIntentsForm(false); + // let request = { + // projectName: props.projectName, + // compositeAppName: values.compositeAppName, + // compositeAppVersion: values.compositeAppVersion, + // deploymentIntentGroupName: values.deploymentIntentGroupName, + // payload: { + // metadata: { name: values.name, description: values.description }, + // spec: { + // intent: { + // genericPlacementIntent: values.genericPlacementIntent, + // }, + // }, + // }, + // }; + // if (values.networkControllerIntent && values.networkControllerIntent !== "") + // request.payload.spec.intent.ovnaction = values.networkControllerIntent; + // apiService + // .addIntentsToDeploymentIntentGroup(request) + // .then((res) => { + // if (data[index].intent) { + // data[index].intent.push(res.spec.intent); + // } else { + // data[index].intent = [res.spec.intent]; + // } + // setData([...data]); + // }) + // .catch((err) => { + // console.log("error adding intent to deployment intent group"); + // }); + // }; const handleInstantiate = (index) => { let request = { projectName: props.projectName, - compositeAppName: data[index].compositeAppName, - compositeAppVersion: data[index].compositeAppVersion, + compositeAppName: data[index].metadata.compositeAppName, + compositeAppVersion: data[index].metadata.compositeAppVersion, deploymentIntentGroupName: data[index].metadata.name, }; apiService @@ -184,13 +175,6 @@ export default function DIGtable({ data, setData, ...props }) { <Notification notificationDetails={notificationDetails} /> {data && data.length > 0 && ( <> - <IntentsForm - projectName={props.projectName} - open={openIntentsForm} - onClose={handleCloseIntentsForm} - onSubmit={handleSubmitIntentForm} - data={data[index]} - /> <DeleteDialog open={open} onClose={handleClose} @@ -207,7 +191,7 @@ export default function DIGtable({ data, setData, ...props }) { <StyledTableCell>Version</StyledTableCell> <StyledTableCell>Profile</StyledTableCell> <StyledTableCell>Composite App</StyledTableCell> - <StyledTableCell>Intents</StyledTableCell> + {/* <StyledTableCell>Intents</StyledTableCell> */} <StyledTableCell>Description</StyledTableCell> <StyledTableCell style={{ width: "15%" }}> Actions @@ -225,54 +209,39 @@ export default function DIGtable({ data, setData, ...props }) { {row.spec.profile} </StyledTableCell> <StyledTableCell className={classes.cell}> - {row.compositeAppName} + {row.metadata.compositeAppName} </StyledTableCell> - { + {/* { <StyledTableCell className={classes.cell}> - {row.intent - ? row.intent.map((intentEntry) => { - return Object.keys(intentEntry) - .map(function (k) { - return intentEntry[k]; - }) - .join(" | "); - }) - : ""} + {Object.keys(row.spec.deployedIntents[0]).map(function ( + key, + index + ) { + if ( + index === 0 || + row.spec.deployedIntents[0][key] === "" + ) + return row.spec.deployedIntents[0][key]; + else return ", " + row.spec.deployedIntents[0][key]; + })} </StyledTableCell> - } + } */} <StyledTableCell className={classes.cell}> {row.metadata.description} </StyledTableCell> <StyledTableCell className={classes.cell}> - <Button - variant="outlined" - color="primary" - size="small" - onClick={() => { - handleAddIntent(index); - }} - startIcon={<AddIcon />} - > - Intents - </Button> <IconButton - disabled={!(row.intent && row.intent.length > 0)} + color={"primary"} + // disabled={ + // !( + // row.spec.deployedIntents && + // row.spec.deployedIntents.length > 0 + // ) + // } title="Instantiate" onClick={(e) => handleInstantiate(index)} > - <GetAppIcon - color={ - !(row.intent && row.intent.length > 0) - ? "" - : "primary" - } - /> - </IconButton> - <IconButton - onClick={(e) => handleEdit(index)} - title="Edit" - > - <EditIcon color="primary" /> + <GetAppIcon /> </IconButton> <IconButton onClick={(e) => handleDelete(index)} diff --git a/src/tools/emcoui/src/deploymentIntentGroups/DeploymentIntentGroups.jsx b/src/tools/emcoui/src/deploymentIntentGroups/DeploymentIntentGroups.jsx index c1f73bb7..132b9fc3 100644 --- a/src/tools/emcoui/src/deploymentIntentGroups/DeploymentIntentGroups.jsx +++ b/src/tools/emcoui/src/deploymentIntentGroups/DeploymentIntentGroups.jsx @@ -11,14 +11,15 @@ // 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. -// ======================================================================== +// ======================================================================== import React, { useEffect, useState } from "react"; import DIGtable from "./DIGtable"; -import { withStyles, Button, Grid } from "@material-ui/core"; +import { withStyles, Button, Grid, Typography } from "@material-ui/core"; import AddIcon from "@material-ui/icons/Add"; import apiService from "../services/apiService"; import Spinner from "../common/Spinner"; import DIGform from "./DIGform"; +import { ReactComponent as EmptyIcon } from "../assets/icons/empty.svg"; const styles = { root: { @@ -44,84 +45,59 @@ const DeploymentIntentGroups = (props) => { setOpen(true); }; const handleSubmit = (inputFields) => { - let payload = { - metadata: { - name: inputFields.name, - description: inputFields.description, - }, - spec: { - profile: inputFields.compositeProfile, - version: inputFields.version, - }, - projectName: props.projectName, - compositeAppName: inputFields.compositeApp, - compositeAppVersion: inputFields.compositeAppVersion, - }; - if (inputFields.overrideValues && inputFields.overrideValues !== "") { - payload.spec["override-values"] = JSON.parse(inputFields.overrideValues); + try { + let payload = { + spec: { + projectName: props.projectName, + appsData: inputFields.intents.apps, + }, + }; + if (inputFields.overrideValues && inputFields.overrideValues !== "") { + payload.spec["override-values"] = JSON.parse( + inputFields.overrideValues + ); + } + payload = { ...payload, ...inputFields.general }; + apiService + .createDeploymentIntentGroup(payload) + .then((response) => { + response.metadata.compositeAppName = inputFields.general.compositeApp; + response.metadata.compositeAppVersion = + inputFields.general.compositeAppVersion; + data && data.length > 0 + ? setData([...data, response]) + : setData([response]); + }) + .catch((error) => { + console.log("error creating DIG : ", error); + }) + .finally(() => { + setIsloading(false); + setOpen(false); + }); + } catch (error) { + console.error(error); } - apiService - .createDeploymentIntentGroup(payload) - .then((response) => { - response.compositeAppName = inputFields.compositeApp; - response.compositeAppVersion = inputFields.compositeAppVersion; - data && data.length > 0 - ? setData([...data, response]) - : setData([response]); - }) - .catch((error) => { - console.log("error creating DIG : ", error); - }) - .finally(() => { - setIsloading(false); - setOpen(false); - }); }; useEffect(() => { + let getDigs = () => { + apiService + .getDeploymentIntentGroups({ projectName: props.projectName }) + .then((res) => { + setData(res); + }) + .catch((err) => { + console.log("error getting deplotment intent groups : " + err); + }) + .finally(() => setIsloading(false)); + }; + apiService .getCompositeApps({ projectName: props.projectName }) .then((response) => { - const getDigIntents = (input) => { - let request = { - projectName: props.projectName, - compositeAppName: input.compositeAppName, - compositeAppVersion: input.compositeAppVersion, - deploymentIntentGroupName: input.metadata.name, - }; - apiService - .getDeploymentIntentGroupIntents(request) - .then((res) => { - input.intent = res.intent; - }) - .catch((err) => {}) - .finally(() => { - setData((data) => [...data, input]); - }); - }; - response.forEach((compositeApp) => { - let request = { - projectName: props.projectName, - compositeAppName: compositeApp.metadata.name, - compositeAppVersion: compositeApp.spec.version, - }; - apiService - .getDeploymentIntentGroups(request) - .then((digResponse) => { - digResponse.forEach((res) => { - res.compositeAppName = compositeApp.metadata.name; - res.compositeAppVersion = compositeApp.spec.version; - getDigIntents(res); - }); - }) - .catch((error) => { - console.log("unable to get deployment intent groups", error); - }) - .finally(() => { - setCompositeApps(response); - setIsloading(false); - }); - }); + setCompositeApps(response); + getDigs(); }) .catch((err) => { console.log("Unable to get composite apps : ", err); @@ -131,16 +107,8 @@ const DeploymentIntentGroups = (props) => { return ( <> {isLoading && <Spinner />} - {!isLoading && compositeApps && compositeApps.length > 0 && ( + {!isLoading && compositeApps && ( <> - <Button - variant="outlined" - color="primary" - startIcon={<AddIcon />} - onClick={onCreateDIG} - > - Create Deployment Intent Group - </Button> <DIGform projectName={props.projectName} open={open} @@ -148,15 +116,41 @@ const DeploymentIntentGroups = (props) => { onSubmit={handleSubmit} data={{ compositeApps: compositeApps }} /> - <Grid container spacing={2} alignItems="center"> - <Grid item xs style={{ marginTop: "20px" }}> - <DIGtable - data={data} - setData={setData} - projectName={props.projectName} - /> - </Grid> + <Grid item xs={12}> + <Button + variant="outlined" + color="primary" + startIcon={<AddIcon />} + onClick={onCreateDIG} + > + Create Deployment Intent Group + </Button> </Grid> + + {data && data.length > 0 && ( + <Grid container spacing={2} alignItems="center"> + <Grid item xs style={{ marginTop: "20px" }}> + <DIGtable + data={data} + setData={setData} + projectName={props.projectName} + /> + </Grid> + </Grid> + )} + + {(data === null || (data && data.length < 1)) && ( + <Grid container spacing={2} direction="column" alignItems="center"> + <Grid style={{ marginTop: "60px" }} item xs={6}> + <EmptyIcon style={{ height: "100px", width: "100px" }} /> + </Grid> + <Grid item xs={12}> + <Typography variant="h6"> + No deployment group found, start by adding a deployment group + </Typography> + </Grid> + </Grid> + )} </> )} </> diff --git a/src/tools/emcoui/src/deploymentIntentGroups/DigFormApp.jsx b/src/tools/emcoui/src/deploymentIntentGroups/DigFormApp.jsx new file mode 100644 index 00000000..e0662455 --- /dev/null +++ b/src/tools/emcoui/src/deploymentIntentGroups/DigFormApp.jsx @@ -0,0 +1,201 @@ +//======================================================================= +// Copyright (c) 2017-2020 Aarna Networks, Inc. +// All rights reserved. +// ====================================================================== +// 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. +// ======================================================================== +import { makeStyles } from "@material-ui/core/styles"; +import PropTypes from "prop-types"; +import Tabs from "@material-ui/core/Tabs"; +import Tab from "@material-ui/core/Tab"; +import Box from "@material-ui/core/Box"; +import React, { useState } from "react"; +import Typography from "@material-ui/core/Typography"; +import { Formik } from "formik"; +import ExpandableCard from "../common/ExpandableCard"; +import AppPlacementForm from "../compositeApps/dialogs/AppFormPlacement"; +import NetworkForm from "../compositeApps/dialogs/AppNetworkForm"; + +const useStyles = makeStyles((theme) => ({ + tableRoot: { + width: "100%", + }, + paper: { + width: "100%", + marginBottom: theme.spacing(2), + }, + table: { + minWidth: 550, + }, + visuallyHidden: { + border: 0, + clip: "rect(0 0 0 0)", + height: 1, + margin: -1, + overflow: "hidden", + padding: 0, + position: "absolute", + top: 20, + width: 1, + }, + appBar: { + position: "relative", + }, + title: { + marginLeft: theme.spacing(2), + flex: 1, + }, + demo: { + backgroundColor: theme.palette.background.paper, + }, + root: { + flexGrow: 1, + backgroundColor: theme.palette.background.paper, + display: "flex", + height: 424, + }, + tabs: { + borderRight: `1px solid ${theme.palette.divider}`, + }, +})); +function TabPanel(props) { + const { children, value, index, ...other } = props; + return ( + <div + role="tabpanel" + hidden={value !== index} + id={`vertical-tabpanel-${index}`} + aria-labelledby={`vertical-tab-${index}`} + {...other} + > + {value === index && <Box style={{ padding: "0 24px" }}>{children}</Box>} + </div> + ); +} + +function AppDetailsForm({ formikProps, ...props }) { + const classes = useStyles(); + const [value, setValue] = useState(0); + const handleChange = (event, newValue) => { + setValue(newValue); + }; + const handleRowSelect = (clusterProvider, selectedClusters) => { + if ( + !formikProps.values.apps[props.index].clusters || + formikProps.values.apps[props.index].clusters === undefined + ) { + if (selectedClusters.length > 0) { + let selectedClusterData = []; + selectedClusters.forEach((selectedCluster) => { + selectedClusterData.push({ name: selectedCluster, interfaces: [] }); + }); + formikProps.setFieldValue(`apps[${props.index}].clusters`, [ + { + provider: clusterProvider, + selectedClusters: selectedClusterData, + }, + ]); + } + } else { + let selectedClusterData = []; + //filter out the value of cluster provider so that it can be completely replaced by the new values + let updatedClusterValues = formikProps.values.apps[ + props.index + ].clusters.filter((cluster) => cluster.provider !== clusterProvider); + selectedClusters.forEach((selectedCluster) => { + selectedClusterData.push({ name: selectedCluster, interfaces: [] }); + }); + if (selectedClusters.length > 0) + updatedClusterValues.push({ + provider: clusterProvider, + selectedClusters: selectedClusterData, + }); + formikProps.setFieldValue( + `apps[${props.index}].clusters`, + updatedClusterValues + ); + } + }; + return ( + <div className={classes.root}> + <Formik> + {() => { + return ( + <> + <Tabs + orientation="vertical" + variant="scrollable" + value={value} + onChange={handleChange} + aria-label="Vertical tabs example" + className={classes.tabs} + > + <Tab label="Placement" {...a11yProps(1)} /> + <Tab label="Network" {...a11yProps(2)} /> + </Tabs> + <TabPanel style={{ width: "85%" }} value={value} index={0}> + <AppPlacementForm + formikProps={formikProps} + index={props.index} + clusterProviders={props.clusterProviders} + handleRowSelect={handleRowSelect} + /> + </TabPanel> + <TabPanel style={{ width: "85%" }} value={value} index={1}> + <Typography variant="subtitle1">Select Network</Typography> + <NetworkForm + clusters={formikProps.values.apps[props.index].clusters} + formikProps={formikProps} + index={props.index} + /> + </TabPanel> + </> + ); + }} + </Formik> + </div> + ); +} + +TabPanel.propTypes = { + children: PropTypes.node, + index: PropTypes.any.isRequired, + value: PropTypes.any.isRequired, +}; + +function a11yProps(index) { + return { + id: `vertical-tab-${index}`, + "aria-controls": `vertical-tabpanel-${index}`, + }; +} + +const AppForm2 = (props) => { + return ( + <ExpandableCard + error={ + props.formikProps.errors.apps && + props.formikProps.errors.apps[props.index] + } + title={props.name} + description={props.description} + content={ + <AppDetailsForm + formikProps={props.formikProps} + name={props.name} + index={props.index} + clusterProviders={props.clusterProviders} + /> + } + /> + ); +}; +export default AppForm2; diff --git a/src/tools/emcoui/src/deploymentIntentGroups/DigFormGeneral.jsx b/src/tools/emcoui/src/deploymentIntentGroups/DigFormGeneral.jsx new file mode 100644 index 00000000..5b5c4191 --- /dev/null +++ b/src/tools/emcoui/src/deploymentIntentGroups/DigFormGeneral.jsx @@ -0,0 +1,265 @@ +//======================================================================= +// Copyright (c) 2017-2020 Aarna Networks, Inc. +// All rights reserved. +// ====================================================================== +// 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. +// ======================================================================== +import React, { useEffect, useState } from "react"; +import { Formik } from "formik"; +import * as Yup from "yup"; + +import { + Button, + DialogActions, + FormControl, + FormHelperText, + Grid, + InputLabel, + MenuItem, + Select, + TextField, +} from "@material-ui/core"; + +const schema = Yup.object({ + name: Yup.string().required(), + description: Yup.string(), + version: Yup.string() + .matches(/^[A-Za-z0-9\\s]+$/, "Special characters and space not allowed") + .required("Version is required"), + compositeProfile: Yup.string().required(), + overrideValues: Yup.array() + .of(Yup.object()) + .typeError("Invalid override values, expected array"), +}); + +function DigFormGeneral(props) { + const { item, onSubmit } = props; + const [selectedAppIndex, setSelectedAppIndex] = useState(0); //let the first composite app as default selection + useEffect(() => { + if (item) { + props.data.compositeApps.forEach((ca, index) => { + if (ca.metadata.name === item.compositeApp) { + setSelectedAppIndex(index); + } + }); + } + }, []); + + let initialValues = item + ? { + ...item, + } + : { + name: "", + description: "", + overrideValues: undefined, + compositeApp: props.data.compositeApps[selectedAppIndex].metadata.name, + compositeProfile: "", + version: "", + }; + + const handleSetCompositeApp = (val) => { + props.data.compositeApps.forEach((ca, index) => { + if (ca.metadata.name === val) setSelectedAppIndex(index); + }); + }; + return ( + <Formik + initialValues={initialValues} + onSubmit={(values) => { + values.compositeAppVersion = + props.data.compositeApps[selectedAppIndex].spec.version; + onSubmit(values); + }} + validationSchema={schema} + > + {(formicProps) => { + const { + values, + touched, + errors, + isSubmitting, + handleChange, + handleBlur, + handleSubmit, + } = formicProps; + return ( + <form noValidate onSubmit={handleSubmit} onChange={handleChange}> + <Grid container spacing={4} justify="center"> + <Grid container item xs={12} spacing={8}> + <Grid item xs={12} md={6}> + <TextField + fullWidth + id="name" + label="Name" + type="text" + value={values.name} + onChange={handleChange} + onBlur={handleBlur} + helperText={ + errors.name && touched.name && "Name is required" + } + required + error={errors.name && touched.name} + /> + </Grid> + <Grid item xs={12} md={6}> + <TextField + fullWidth + id="version" + label="Version" + type="text" + name="version" + value={values.version} + onChange={handleChange} + onBlur={handleBlur} + helperText={ + errors.version && touched.version && errors["version"] + } + required + error={errors.version && touched.version} + /> + </Grid> + </Grid> + + <Grid item container xs={12} spacing={8}> + <Grid item xs={12} md={6}> + <InputLabel shrink htmlFor="compositeApp-label-placeholder"> + Composite App + </InputLabel> + <Select + fullWidth + name="compositeApp" + value={values.compositeApp} + onChange={(e) => { + handleChange(e); + handleSetCompositeApp(e.target.value); + }} + onBlur={handleBlur} + inputProps={{ + name: "compositeApp", + id: "compositeApps-label-placeholder", + }} + > + {props.data && + props.data.compositeApps.map((compositeApp) => ( + <MenuItem + value={compositeApp.metadata.name} + key={compositeApp.metadata.name} + > + {compositeApp.metadata.name} + </MenuItem> + ))} + </Select> + </Grid> + <Grid item xs={12} md={6}> + <FormControl + fullWidth + required + error={errors.compositeProfile && touched.compositeProfile} + > + <InputLabel htmlFor="compositeProfile-label-placeholder"> + Composite Profile + </InputLabel> + <Select + name="compositeProfile" + onChange={handleChange} + onBlur={handleBlur} + required + value={values.compositeProfile} + inputProps={{ + name: "compositeProfile", + id: "compositeProfile-label-placeholder", + }} + > + {props.data.compositeApps[selectedAppIndex].profiles && + props.data.compositeApps[selectedAppIndex].profiles.map( + (compositeProfile) => ( + <MenuItem + value={compositeProfile.metadata.name} + key={compositeProfile.metadata.name} + > + {compositeProfile.metadata.name} + </MenuItem> + ) + )} + </Select> + {errors.compositeProfile && touched.compositeProfile && ( + <FormHelperText>Required</FormHelperText> + )} + </FormControl> + </Grid> + </Grid> + + <Grid item container xs={12} spacing={8}> + <Grid item xs={12} md={6}> + <TextField + fullWidth + name="description" + value={values.description} + onChange={handleChange} + onBlur={handleBlur} + id="description" + label="Description" + multiline + rowsMax={4} + /> + </Grid> + <Grid item xs={12} md={6}> + <TextField + fullWidth + id="overrideValues" + label="Override Values" + type="text" + value={values.overrideValues} + onChange={handleChange} + onBlur={handleBlur} + multiline + rows={4} + variant="outlined" + error={errors.overrideValues && touched.overrideValues} + helperText={ + errors.overrideValues && + touched.overrideValues && + errors["overrideValues"] + } + /> + </Grid> + </Grid> + <Grid item xs={12}> + <DialogActions> + <Button + autoFocus + disabled + onClick={props.onClickBack} + color="secondary" + > + Back + </Button> + <Button + autoFocus + type="submit" + color="primary" + disabled={isSubmitting} + > + Next + </Button> + </DialogActions> + </Grid> + </Grid> + </form> + ); + }} + </Formik> + ); +} + +export default DigFormGeneral; diff --git a/src/tools/emcoui/src/deploymentIntentGroups/DigFormIntents.jsx b/src/tools/emcoui/src/deploymentIntentGroups/DigFormIntents.jsx new file mode 100644 index 00000000..580044ac --- /dev/null +++ b/src/tools/emcoui/src/deploymentIntentGroups/DigFormIntents.jsx @@ -0,0 +1,160 @@ +//======================================================================= +// Copyright (c) 2017-2020 Aarna Networks, Inc. +// All rights reserved. +// ====================================================================== +// 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. +// ======================================================================== +import React, { useEffect, useState } from "react"; +import { Formik } from "formik"; +import * as Yup from "yup"; +import AppForm from "./DigFormApp"; +import apiService from "../services/apiService"; + +import { Button, DialogActions, Grid } from "@material-ui/core"; + +DigFormIntents.propTypes = {}; +const schema = Yup.object({ + apps: Yup.array() + .of( + Yup.object({ + clusters: Yup.array() + .of( + Yup.object({ + provider: Yup.string(), + selectedClusters: Yup.array().of( + Yup.object({ + name: Yup.string(), + interfaces: Yup.array().of( + Yup.object({ + networkName: Yup.string().required(), + subnet: Yup.string().required(), + ip: Yup.string().matches( + /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/, + "invalid ip address" + ), + }) + ), + }) + ), + }) + ) + .required("Select at least one cluster"), + }) + ) + .required("At least one app is required"), +}); + +function DigFormIntents(props) { + const { onSubmit, appsData } = props; + const [isLoading, setIsloading] = useState(true); + const [clusterProviders, setClusterProviders] = useState([]); + let initialValues = { apps: appsData }; + useEffect(() => { + let clusterProviderData = []; + apiService + .getClusterProviders() + .then((res) => { + res.forEach((clusterProvider, providerIndex) => { + clusterProviderData.push({ + name: clusterProvider.metadata.name, + clusters: [], + }); + apiService + .getClusters(clusterProvider.metadata.name) + .then((clusters) => { + clusters.forEach((cluster) => { + clusterProviderData[providerIndex].clusters.push({ + name: cluster.metadata.name, + description: cluster.metadata.description, + }); + }); + if (providerIndex + 1 === res.length) { + setClusterProviders(clusterProviderData); + setIsloading(false); + } + }) + .catch((err) => { + console.log( + `error getting clusters for ${clusterProvider.metadata.name} : ` + + err + ); + }); + }); + }) + .catch((err) => { + console.log("error getting cluster providers : " + err); + }); + }, []); + useEffect(() => {}, []); + + return ( + <Formik + initialValues={initialValues} + onSubmit={(values) => { + values.compositeAppVersion = onSubmit(values); + }} + validationSchema={schema} + > + {(formikProps) => { + const { + values, + isSubmitting, + handleChange, + handleSubmit, + } = formikProps; + return ( + !isLoading && ( + <form noValidate onSubmit={handleSubmit} onChange={handleChange}> + <Grid container spacing={4} justify="center"> + {initialValues.apps && + initialValues.apps.length > 0 && + initialValues.apps.map((app, index) => ( + <Grid key={index} item sm={12} xs={12}> + <AppForm + clusterProviders={clusterProviders} + formikProps={formikProps} + name={app.metadata.name} + description={app.metadata.description} + index={index} + initialValues={values} + /> + </Grid> + ))} + + <Grid item xs={12}> + <DialogActions> + <Button + autoFocus + onClick={props.onClickBack} + color="secondary" + > + Back + </Button> + <Button + autoFocus + type="submit" + color="primary" + disabled={isSubmitting} + > + Submit + </Button> + </DialogActions> + </Grid> + </Grid> + </form> + ) + ); + }} + </Formik> + ); +} + +export default DigFormIntents; diff --git a/src/tools/emcoui/src/deploymentIntentGroups/Stepper.jsx b/src/tools/emcoui/src/deploymentIntentGroups/Stepper.jsx new file mode 100644 index 00000000..746f49e3 --- /dev/null +++ b/src/tools/emcoui/src/deploymentIntentGroups/Stepper.jsx @@ -0,0 +1,118 @@ +//======================================================================= +// Copyright (c) 2017-2020 Aarna Networks, Inc. +// All rights reserved. +// ====================================================================== +// 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. +// ======================================================================== +import React, { useState } from "react"; +import { makeStyles } from "@material-ui/core/styles"; +import Stepper from "@material-ui/core/Stepper"; +import Step from "@material-ui/core/Step"; +import StepLabel from "@material-ui/core/StepLabel"; +import DigFormGeneral from "./DigFormGeneral"; +import DigFormIntents from "./DigFormIntents"; +import apiService from "../services/apiService"; + +const useStyles = makeStyles((theme) => ({ + root: { + width: "100%", + }, + backButton: { + marginRight: theme.spacing(1), + }, + instructions: { + marginTop: theme.spacing(1), + marginBottom: theme.spacing(1), + }, +})); + +function getSteps() { + return ["General", "Intents"]; +} + +export default function HorizontalStepper(props) { + const classes = useStyles(); + const [activeStep, setActiveStep] = useState(0); + const [generalData, setGeneralData] = useState(null); + const [intentsData, setIntentsData] = useState(null); + const [appsData, setAppsData] = useState([]); + + const steps = getSteps(); + + function getStepContent(stepIndex) { + switch (stepIndex) { + case 0: + return ( + <DigFormGeneral + data={props.data} + onSubmit={handleGeneralFormSubmit} + item={generalData} + /> + ); + case 1: + return ( + <DigFormIntents + appsData={appsData} + onSubmit={handleIntentsFormSubmit} + onClickBack={handleBack} + item={intentsData} + /> + ); + default: + return "Unknown stepIndex"; + } + } + + const handleNext = () => { + setActiveStep((prevActiveStep) => prevActiveStep + 1); + }; + + const handleBack = () => { + setActiveStep((prevActiveStep) => prevActiveStep - 1); + }; + const handleGeneralFormSubmit = (values) => { + setGeneralData(values); + let request = { + projectName: props.projectName, + compositeAppName: values.compositeApp, + compositeAppVersion: values.compositeAppVersion, + }; + apiService + .getApps(request) + .then((res) => { + setAppsData(res); + handleNext((prevActiveStep) => prevActiveStep + 1); + }) + .catch((err) => { + console.log("Error getting apps : " + err); + }); + }; + + const handleIntentsFormSubmit = (values) => { + setIntentsData(values); + let digPayload = { general: generalData, intents: values }; + props.onSubmit(digPayload); + }; + return ( + <div className={classes.root}> + <Stepper activeStep={activeStep} alternativeLabel> + {steps.map((label) => ( + <Step key={label}> + <StepLabel>{label}</StepLabel> + </Step> + ))} + </Stepper> + <div> + <div>{getStepContent(activeStep)}</div> + </div> + </div> + ); +} diff --git a/src/tools/emcoui/src/networkIntents/NetworkIntentCard.jsx b/src/tools/emcoui/src/networkIntents/NetworkIntentCard.jsx index b641737b..ed93bd0f 100644 --- a/src/tools/emcoui/src/networkIntents/NetworkIntentCard.jsx +++ b/src/tools/emcoui/src/networkIntents/NetworkIntentCard.jsx @@ -11,7 +11,7 @@ // 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. -// ======================================================================== +// ======================================================================== import React, { useState } from "react"; import { makeStyles } from "@material-ui/core/styles"; import clsx from "clsx"; @@ -54,7 +54,7 @@ const NetworkIntentCard = (props) => { const [expanded, setExpanded] = useState(false); const [workloadData, setWorkloadData] = useState([]); const handleExpandClick = () => { - if (!expanded && workloadData.length < 1) { + if (!expanded && workloadData && workloadData.length < 1) { let request = { projectName: props.projectName, compositeAppName: props.compositeAppName, @@ -189,6 +189,7 @@ const NetworkIntentCard = (props) => { variant="outlined" size="small" color="secondary" + disabled={workloadData && workloadData.length > 0} style={{ float: "right" }} startIcon={<DeleteIcon />} onClick={props.onDeleteNetworkControllerIntent.bind( @@ -210,6 +211,9 @@ const NetworkIntentCard = (props) => { } /> )} + {!(props.appsData && props.appsData.length > 0) && ( + <div>No app found for adding workload intent</div> + )} </CardContent> </Collapse> </Card> diff --git a/src/tools/emcoui/src/networkIntents/WorkloadIntentTable.jsx b/src/tools/emcoui/src/networkIntents/WorkloadIntentTable.jsx index 5e196ed9..540de8a0 100644 --- a/src/tools/emcoui/src/networkIntents/WorkloadIntentTable.jsx +++ b/src/tools/emcoui/src/networkIntents/WorkloadIntentTable.jsx @@ -11,207 +11,268 @@ // 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. -// ======================================================================== -import React, { useState } from 'react'; -import { TableContainer, Table, TableRow, TableHead, withStyles, Chip, TableCell } from '@material-ui/core'; +// ======================================================================== +import React, { useState } from "react"; +import { + TableContainer, + Table, + TableRow, + TableHead, + withStyles, + Chip, + TableCell, +} from "@material-ui/core"; import Paper from "@material-ui/core/Paper"; import TableBody from "@material-ui/core/TableBody"; import EditIcon from "@material-ui/icons/Edit"; import DeleteIcon from "@material-ui/icons/Delete"; -import PropTypes from 'prop-types'; +import PropTypes from "prop-types"; import apiService from "../services/apiService"; import DeleteDialog from "../common/Dialogue"; -import InfoOutlinedIcon from '@material-ui/icons/InfoOutlined'; -import IconButton from '@material-ui/core/IconButton'; -import AddIconOutline from '@material-ui/icons/AddCircleOutline'; +import InfoOutlinedIcon from "@material-ui/icons/InfoOutlined"; +import IconButton from "@material-ui/core/IconButton"; +import AddIconOutline from "@material-ui/icons/AddCircleOutline"; import Form from "./InterfaceForm"; import InterfaceDetailsDialog from "../common/DetailsDialog"; - const StyledTableCell = withStyles((theme) => ({ - body: { - fontSize: 14, - }, + body: { + fontSize: 14, + }, }))(TableCell); const StyledTableRow = withStyles((theme) => ({ - root: { - "&:nth-of-type(odd)": { - backgroundColor: theme.palette.action.hover, - }, + root: { + "&:nth-of-type(odd)": { + backgroundColor: theme.palette.action.hover, }, + }, }))(TableRow); const WokloadIntentTable = ({ data, setData, ...props }) => { - const [formOpen, setFormOpen] = useState(false); - const [index, setIndex] = useState(0); - const [openDialog, setOpenDialog] = useState(false); - const [openInterfaceDetails, setOpenInterfaceDetails] = useState(false); - const [selectedInterface, setSelectedInterface] = useState({}); - const [openInterfaceDialog, setOpenInterfaceDialog] = useState(false); - const handleDelete = (index) => { - setIndex(index); - setOpenDialog(true); - } - const handleEdit = () => { + const [formOpen, setFormOpen] = useState(false); + const [index, setIndex] = useState(0); + const [openDialog, setOpenDialog] = useState(false); + const [openInterfaceDetails, setOpenInterfaceDetails] = useState(false); + const [selectedInterface, setSelectedInterface] = useState({}); + const [openInterfaceDialog, setOpenInterfaceDialog] = useState(false); + const handleDelete = (index) => { + setIndex(index); + setOpenDialog(true); + }; + const handleEdit = () => {}; + const handleInterfaceDetailOpen = (entry) => { + setSelectedInterface(entry); + setOpenInterfaceDetails(true); + }; + const handleDeleteInterface = (index, entry) => { + setIndex(index); + setSelectedInterface(entry); + setOpenInterfaceDialog(true); + }; + const handleAddInterface = (index) => { + setIndex(index); + setFormOpen(true); + }; + const handleCloseForm = () => { + setFormOpen(false); + }; + const handleCloseInterfaceDialog = (el) => { + if (el.target.innerText === "Delete") { + let request = { + projectName: props.projectName, + compositeAppName: props.compositeAppName, + compositeAppVersion: props.compositeAppVersion, + networkControllerIntentName: props.networkControllerIntentName, + workloadIntentName: data[index].metadata.name, + interfaceName: selectedInterface.metadata.name, + }; + apiService + .deleteInterface(request) + .then(() => { + console.log("Interface deleted"); + let updatedInterfaceData = data[index].interfaces.filter(function ( + obj + ) { + return obj.metadata.name !== selectedInterface.metadata.name; + }); + data[index].interfaces = updatedInterfaceData; + setData([...data]); + }) + .catch((err) => { + console.log("Error deleting interface : ", err); + }) + .finally(() => { + setIndex(0); + setSelectedInterface({}); + }); } - const handleInterfaceDetailOpen = (entry) => { - setSelectedInterface(entry); - setOpenInterfaceDetails(true); + setOpenInterfaceDialog(false); + }; + const handleCloseDialog = (el) => { + if (el.target.innerText === "Delete") { + let request = { + projectName: props.projectName, + compositeAppName: props.compositeAppName, + compositeAppVersion: props.compositeAppVersion, + networkControllerIntentName: props.networkControllerIntentName, + workloadIntentName: data[index].metadata.name, + }; + apiService + .deleteWorkloadIntent(request) + .then(() => { + console.log("workload intent deleted"); + data.splice(index, 1); + setData([...data]); + }) + .catch((err) => { + console.log("Error deleting workload intent : ", err); + }) + .finally(() => { + setIndex(0); + }); } - - const handleDeleteInterface = (index, entry) => { - setIndex(index); - setSelectedInterface(entry); - setOpenInterfaceDialog(true); - } - const handleAddInterface = (index) => { - setIndex(index); - setFormOpen(true); - } - const handleCloseForm = () => { - setFormOpen(false); - } - const handleCloseInterfaceDialog = (el) => { - if (el.target.innerText === "Delete") { - let request = { - projectName: props.projectName, - compositeAppName: props.compositeAppName, - compositeAppVersion: props.compositeAppVersion, - networkControllerIntentName: props.networkControllerIntentName, - workloadIntentName: data[index].metadata.name, - interfaceName: selectedInterface.metadata.name - } - apiService.deleteInterface(request).then(() => { - console.log("Interface deleted"); - let updatedInterfaceData = data[index].interfaces.filter(function (obj) { - return obj.metadata.name !== selectedInterface.metadata.name; - }); - data[index].interfaces = updatedInterfaceData; - setData([...data]); - }).catch(err => { - console.log("Error deleting interface : ", err) - }).finally(() => { - setIndex(0); - setSelectedInterface({}); - }) - } - setOpenInterfaceDialog(false); - } - const handleCloseDialog = (el) => { - if (el.target.innerText === "Delete") { - let request = { - projectName: props.projectName, - compositeAppName: props.compositeAppName, - compositeAppVersion: props.compositeAppVersion, - networkControllerIntentName: props.networkControllerIntentName, - workloadIntentName: data[index].metadata.name - } - apiService.deleteWorkloadIntent(request).then(() => { - console.log("workload intent deleted"); - data.splice(index, 1); - setData([...data]); - }).catch(err => { - console.log("Error deleting workload intent : ", err) - }).finally(() => { - setIndex(0); - }) + setOpenDialog(false); + }; + const handleSubmit = (values) => { + let spec = values.spec ? JSON.parse(values.spec) : ""; + let request = { + payload: { + metadata: { name: values.name, description: values.description }, + spec: spec, + }, + projectName: props.projectName, + compositeAppName: props.compositeAppName, + compositeAppVersion: props.compositeAppVersion, + networkControllerIntentName: props.networkControllerIntentName, + workloadIntentName: data[index].metadata.name, + }; + apiService + .addInterface(request) + .then((res) => { + if (data[index].interfaces && data[index].interfaces.length > 0) { + data[index].interfaces.push(res); + } else { + data[index].interfaces = [res]; } - setOpenDialog(false); - } - const handleSubmit = (values) => { - let spec = values.spec ? JSON.parse(values.spec) : ""; - let request = { - payload: { metadata: { name: values.name, description: values.description }, spec: spec }, - projectName: props.projectName, - compositeAppName: props.compositeAppName, - compositeAppVersion: props.compositeAppVersion, - networkControllerIntentName: props.networkControllerIntentName, - workloadIntentName: data[index].metadata.name - }; - apiService.addInterface(request) - .then(res => { - if (data[index].interfaces && data[index].interfaces.length > 0) { - data[index].interfaces.push(res); - } - else { - data[index].interfaces = [res]; - } - setData([...data]); - }) - .catch(err => { - console.log("error creating composite profile : ", err); - }) - .finally(() => { setFormOpen(false); }) - } - return ( - <> - <InterfaceDetailsDialog open={openInterfaceDetails} onClose={setOpenInterfaceDetails} item={selectedInterface} type="Interface" /> - <Form open={formOpen} onClose={handleCloseForm} onSubmit={handleSubmit} /> - <DeleteDialog open={openDialog} onClose={handleCloseDialog} title={"Delete Profile"} - content={`Are you sure you want to delete "${data && data[index] ? data[index].metadata.name : ""}"`} /> - <DeleteDialog open={openInterfaceDialog} onClose={handleCloseInterfaceDialog} title={"Delete Interface"} - content={`Are you sure you want to delete "${selectedInterface.metadata ? selectedInterface.metadata.name : ""}"`} /> - <TableContainer component={Paper}> - <Table> - <TableHead> - <TableRow> - <StyledTableCell>Name</StyledTableCell> - <StyledTableCell>Description</StyledTableCell> - <StyledTableCell>App</StyledTableCell> - <StyledTableCell>Workload Resource</StyledTableCell> - <StyledTableCell style={{ width: "27%" }}>Interfaces</StyledTableCell> - <StyledTableCell>Actions</StyledTableCell> - </TableRow> - </TableHead> - <TableBody> - {data.map((entry, index) => - <StyledTableRow key={entry.metadata.name + index}> - <StyledTableCell> - {entry.metadata.name} - </StyledTableCell> - <StyledTableCell > - {entry.metadata.description} - </StyledTableCell> - <StyledTableCell > - {entry.spec["application-name"]} - </StyledTableCell> - <StyledTableCell> - {entry.spec["workload-resource"]} - </StyledTableCell> - <StyledTableCell> - {entry.interfaces && (entry.interfaces.length > 0) && entry.interfaces.map((interfaceEntry, interfacekIndex) => - (<Chip - key={interfaceEntry.metadata.name + "" + interfacekIndex} - size="small" - icon={<InfoOutlinedIcon onClick={() => { handleInterfaceDetailOpen(interfaceEntry) }} style={{ cursor: "pointer" }} />} - onDelete={(e) => { handleDeleteInterface(index, interfaceEntry) }} - label={interfaceEntry.spec.ipAddress} - style={{ marginRight: "10px", marginBottom: "5px" }} - />) - )} - <IconButton color="primary" onClick={() => { handleAddInterface(index) }}> - <AddIconOutline /> - </IconButton> - </StyledTableCell> - <StyledTableCell > - <IconButton onClick={(e) => handleEdit(index)} title="Edit" > - <EditIcon color="primary" /> - </IconButton> - <IconButton onClick={(e) => handleDelete(index)} title="Delete" > - <DeleteIcon color="secondary" /> - </IconButton> - </StyledTableCell> - </StyledTableRow> - )} - </TableBody> - </Table> - </TableContainer></> - ); + setData([...data]); + }) + .catch((err) => { + console.log("error creating composite profile : ", err); + }) + .finally(() => { + setFormOpen(false); + }); + }; + return ( + <> + <InterfaceDetailsDialog + open={openInterfaceDetails} + onClose={setOpenInterfaceDetails} + item={selectedInterface} + type="Interface" + /> + <Form open={formOpen} onClose={handleCloseForm} onSubmit={handleSubmit} /> + <DeleteDialog + open={openDialog} + onClose={handleCloseDialog} + title={"Delete Profile"} + content={`Are you sure you want to delete "${ + data && data[index] ? data[index].metadata.name : "" + }"`} + /> + <DeleteDialog + open={openInterfaceDialog} + onClose={handleCloseInterfaceDialog} + title={"Delete Interface"} + content={`Are you sure you want to delete "${ + selectedInterface.metadata ? selectedInterface.metadata.name : "" + }"`} + /> + <TableContainer component={Paper}> + <Table> + <TableHead> + <TableRow> + <StyledTableCell>Name</StyledTableCell> + <StyledTableCell>Description</StyledTableCell> + <StyledTableCell>App</StyledTableCell> + <StyledTableCell>Workload Resource</StyledTableCell> + <StyledTableCell style={{ width: "27%" }}> + Interfaces + </StyledTableCell> + <StyledTableCell>Actions</StyledTableCell> + </TableRow> + </TableHead> + <TableBody> + {data.map((entry, index) => ( + <StyledTableRow key={entry.metadata.name + index}> + <StyledTableCell>{entry.metadata.name}</StyledTableCell> + <StyledTableCell>{entry.metadata.description}</StyledTableCell> + <StyledTableCell> + {entry.spec["application-name"]} + </StyledTableCell> + <StyledTableCell> + {entry.spec["workload-resource"]} + </StyledTableCell> + <StyledTableCell> + {entry.interfaces && + entry.interfaces.length > 0 && + entry.interfaces.map((interfaceEntry, interfacekIndex) => ( + <Chip + key={ + interfaceEntry.metadata.name + "" + interfacekIndex + } + size="small" + icon={ + <InfoOutlinedIcon + onClick={() => { + handleInterfaceDetailOpen(interfaceEntry); + }} + style={{ cursor: "pointer" }} + /> + } + onDelete={(e) => { + handleDeleteInterface(index, interfaceEntry); + }} + label={interfaceEntry.spec.ipAddress} + style={{ marginRight: "10px", marginBottom: "5px" }} + /> + ))} + <IconButton + color="primary" + onClick={() => { + handleAddInterface(index); + }} + > + <AddIconOutline /> + </IconButton> + </StyledTableCell> + <StyledTableCell> + {/* + //edit workload intent api has not been added yet + <IconButton onClick={(e) => handleEdit(index)} title="Edit" > + <EditIcon color="primary" /> + </IconButton> */} + <IconButton + color="secondary" + disabled={entry.interfaces && entry.interfaces.length > 0} + onClick={(e) => handleDelete(index)} + title="Delete" + > + <DeleteIcon /> + </IconButton> + </StyledTableCell> + </StyledTableRow> + ))} + </TableBody> + </Table> + </TableContainer> + </> + ); }; WokloadIntentTable.propTypes = { - data: PropTypes.arrayOf(PropTypes.object).isRequired, - setData: PropTypes.func.isRequired + data: PropTypes.arrayOf(PropTypes.object).isRequired, + setData: PropTypes.func.isRequired, }; export default WokloadIntentTable; diff --git a/src/tools/emcoui/src/services/apiService.js b/src/tools/emcoui/src/services/apiService.js index 4bff9305..0c830768 100644 --- a/src/tools/emcoui/src/services/apiService.js +++ b/src/tools/emcoui/src/services/apiService.js @@ -11,10 +11,9 @@ // 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. -// ======================================================================== +// ======================================================================== import axios from "axios"; axios.defaults.baseURL = process.env.REACT_APP_BACKEND || ""; - //orchestrator //projects const createProject = (request) => { @@ -45,6 +44,14 @@ const getCompositeApps = (request) => { return res.data; }); }; +const addService = ({ projectName, ...request }) => { + return axios + .post(`/middleend/projects/${projectName}/composite-apps`, request.payload) + .then((res) => { + return res.data; + }); +}; + const createCompositeApp = ({ projectName, ...request }) => { return axios .post(`/v2/projects/${projectName}/composite-apps`, request.payload) @@ -65,7 +72,7 @@ const updateCompositeApp = (request) => { const deleteCompositeApp = (request) => { return axios .delete( - `/v2/projects/${request.projectName}/composite-apps/${request.compositeAppName}/${request.compositeAppVersion}` + `/middleend/projects/${request.projectName}/composite-apps/${request.compositeAppName}/${request.compositeAppVersion}` ) .then((res) => { return res.data; @@ -302,15 +309,10 @@ const deleteInterface = (request) => { }; //deployment intent group -const createDeploymentIntentGroup = ({ - projectName, - compositeAppName, - compositeAppVersion, - ...request -}) => { +const createDeploymentIntentGroup = (request) => { return axios .post( - `/v2/projects/${projectName}/composite-apps/${compositeAppName}/${compositeAppVersion}/deployment-intent-groups`, + `/middleend/projects/${request.spec.projectName}/composite-apps/${request.compositeApp}/${request.compositeAppVersion}/deployment-intent-groups`, { ...request } ) .then((res) => { @@ -329,9 +331,7 @@ const addIntentsToDeploymentIntentGroup = (request) => { }; const getDeploymentIntentGroups = (request) => { return axios - .get( - `/v2/projects/${request.projectName}/composite-apps/${request.compositeAppName}/${request.compositeAppVersion}/deployment-intent-groups` - ) + .get(`/middleend/projects/${request.projectName}/deployment-intent-groups`) .then((res) => { return res.data; }); @@ -349,7 +349,7 @@ const editDeploymentIntentGroup = (request) => { const deleteDeploymentIntentGroup = (request) => { return axios .delete( - `/v2/projects/${request.projectName}/composite-apps/${request.compositeAppName}/${request.compositeAppVersion}/deployment-intent-groups/${request.deploymentIntentGroupName}` + `/middleend/projects/${request.projectName}/composite-apps/${request.compositeAppName}/${request.compositeAppVersion}/deployment-intent-groups/${request.deploymentIntentGroupName}` ) .then((res) => { return res.data; @@ -421,7 +421,7 @@ const updateClusterProvider = (request) => { const addCluster = (request) => { return axios .post( - `/v2/cluster-providers/${request.get("providerName")}/clusters`, + `/middleend/clusterproviders/${request.get("providerName")}/clusters`, request ) .then((res) => { @@ -577,6 +577,7 @@ const vimService = { getCompositeApps, getProfiles, createCompositeApp, + addService, updateCompositeApp, deleteCompositeApp, getApps, diff --git a/src/tools/emcoui/startup.sh b/src/tools/emcoui/startup.sh index b1d42ffc..8e2b90e0 100755 --- a/src/tools/emcoui/startup.sh +++ b/src/tools/emcoui/startup.sh @@ -1,2 +1,17 @@ +#======================================================================= +# Copyright (c) 2017-2020 Aarna Networks, Inc. +# All rights reserved. +# ====================================================================== +# 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. +# ======================================================================== + # startup script for development. Give backend address if backend server is not running locally -REACT_APP_BACKEND= npm start +REACT_APP_BACKEND=http://emco npm start |