From 8371ccd29229c418dd8bb534fda3d28184c4e986 Mon Sep 17 00:00:00 2001 From: wutianwei Date: Wed, 22 Aug 2018 10:49:56 +0800 Subject: Spinnaker as a Service JIRA: CLOVER-52 1. Add mainfest to install the spinnaker in kubernetes cluster 2. after using mainfest to install spinnaker, we can interacte with the halyard daemon with its REST API and we can add/delete/list the dockerRegistry/kubernetes accounts. 3. Add the cloverctl to interate with the halyard daemon Change-Id: I71bc5977f2d65aab88fa55f7d7a53ab75eb6a46b Signed-off-by: wutianwei --- clover/cloverctl/build.sh | 1 + .../src/cloverctl/cmd/create_docker_registry.go | 57 ++++ .../src/cloverctl/cmd/create_kubernetes.go | 87 +++++ .../src/cloverctl/cmd/delete_docker_registry.go | 52 +++ .../src/cloverctl/cmd/delete_kubernetes.go | 53 +++ .../src/cloverctl/cmd/get_docker_registry.go | 59 ++++ .../cloverctl/src/cloverctl/cmd/get_kubernetes.go | 59 ++++ clover/cloverctl/src/cloverctl/cmd/provider.go | 45 +++ clover/cloverctl/src/cloverkube/main.go | 100 +++++- clover/controller/control/api/halyard.py | 167 +++++++++ clover/controller/control/control.py | 2 + clover/controller/docker/Dockerfile | 3 +- clover/controller/process/gunicorn_process.sh | 5 +- clover/controller/process/nginx.conf | 1 + clover/orchestration/kube_client.py | 34 ++ clover/spinnaker/__init__.py | 0 clover/spinnaker/halyard.py | 120 +++++++ clover/spinnaker/halyard_sample.py | 41 +++ .../spinnaker/install/quick-install-spinnaker.yml | 376 +++++++++++++++++++++ clover/spinnaker/lib/__init__.py | 0 clover/spinnaker/lib/halyard_base.py | 157 +++++++++ clover/tools/yaml/cassandra.yaml | 2 +- 22 files changed, 1416 insertions(+), 5 deletions(-) create mode 100644 clover/cloverctl/src/cloverctl/cmd/create_docker_registry.go create mode 100644 clover/cloverctl/src/cloverctl/cmd/create_kubernetes.go create mode 100644 clover/cloverctl/src/cloverctl/cmd/delete_docker_registry.go create mode 100644 clover/cloverctl/src/cloverctl/cmd/delete_kubernetes.go create mode 100644 clover/cloverctl/src/cloverctl/cmd/get_docker_registry.go create mode 100644 clover/cloverctl/src/cloverctl/cmd/get_kubernetes.go create mode 100644 clover/cloverctl/src/cloverctl/cmd/provider.go create mode 100644 clover/controller/control/api/halyard.py create mode 100644 clover/spinnaker/__init__.py create mode 100755 clover/spinnaker/halyard.py create mode 100644 clover/spinnaker/halyard_sample.py create mode 100644 clover/spinnaker/install/quick-install-spinnaker.yml create mode 100644 clover/spinnaker/lib/__init__.py create mode 100755 clover/spinnaker/lib/halyard_base.py diff --git a/clover/cloverctl/build.sh b/clover/cloverctl/build.sh index 2f7be14..e67e4da 100755 --- a/clover/cloverctl/build.sh +++ b/clover/cloverctl/build.sh @@ -25,6 +25,7 @@ go get github.com/tools/godep go get -u github.com/spf13/cobra/cobra go get -u gopkg.in/resty.v1 +go get k8s.io/apimachinery/pkg/runtime go get k8s.io/client-go/... cd $GOPATH/src/k8s.io/client-go git checkout $CLIENTGOVERSION diff --git a/clover/cloverctl/src/cloverctl/cmd/create_docker_registry.go b/clover/cloverctl/src/cloverctl/cmd/create_docker_registry.go new file mode 100644 index 0000000..77045f6 --- /dev/null +++ b/clover/cloverctl/src/cloverctl/cmd/create_docker_registry.go @@ -0,0 +1,57 @@ +// Copyright (c) Authors of Clover +// +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Apache License, Version 2.0 +// which accompanies this distribution, and is available at +// http://www.apache.org/licenses/LICENSE-2.0 + +package cmd + +import ( + "fmt" + "io/ioutil" + + "gopkg.in/resty.v1" + "github.com/ghodss/yaml" + "github.com/spf13/cobra" +) + + +var dockerregistryCmd = &cobra.Command{ + Use: "docker-registry", + Short: "Add one docker registry from yaml file to spinnaker", + Long: ``, + Run: func(cmd *cobra.Command, args []string) { + createDockerRegistry() + }, +} + +func init() { + providercreateCmd.AddCommand(dockerregistryCmd) + dockerregistryCmd.Flags().StringVarP(&cloverFile, "file", "f", "", "Input yaml file to add kubernetes provider") + dockerregistryCmd.MarkFlagRequired("file") + +} + +func createDockerRegistry() { + url := controllerIP + "/halyard/addregistry" + in, err := ioutil.ReadFile(cloverFile) + if err != nil { + fmt.Println("Please specify a valid rule definition yaml file") + return + } + out_json, err := yaml.YAMLToJSON(in) + if err != nil { + panic(err.Error()) + } + + resp, err := resty.R(). + SetHeader("Content-Type", "application/json"). + SetBody(out_json). + Post(url) + if err != nil { + panic(err.Error()) + } + fmt.Printf("\n%v\n", resp) + +} diff --git a/clover/cloverctl/src/cloverctl/cmd/create_kubernetes.go b/clover/cloverctl/src/cloverctl/cmd/create_kubernetes.go new file mode 100644 index 0000000..7311090 --- /dev/null +++ b/clover/cloverctl/src/cloverctl/cmd/create_kubernetes.go @@ -0,0 +1,87 @@ +// Copyright (c) Authors of Clover +// +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Apache License, Version 2.0 +// which accompanies this distribution, and is available at +// http://www.apache.org/licenses/LICENSE-2.0 + +package cmd + +import ( + "fmt" + "time" + "io/ioutil" + "strings" + + "gopkg.in/resty.v1" + "github.com/ghodss/yaml" + "github.com/spf13/cobra" + "cloverkube" +) + +type Kubernetes struct { + Name string + ProviderVersion string + KubeconfigFile string + DockerRegistries []DockerRegistry +} + +type DockerRegistry struct { + AccountName string +} + + +var kubeproviderCmd = &cobra.Command{ + Use: "kubernetes", + Short: "Add one kubernete provider from yaml file to spinnaker", + Long: ``, + Run: func(cmd *cobra.Command, args []string) { + createProvider() + }, +} + +func init() { + providercreateCmd.AddCommand(kubeproviderCmd) + kubeproviderCmd.Flags().StringVarP(&cloverFile, "file", "f", "", "Input yaml file to add kubernetes provider") + kubeproviderCmd.MarkFlagRequired("file") + +} + +func createProvider() { + url := controllerIP + "/halyard/addkube" + in, err := ioutil.ReadFile(cloverFile) + if err != nil { + fmt.Println("Please specify a valid rule definition yaml file") + return + } + + t := Kubernetes{} + yaml.Unmarshal(in, &t) + if(t.KubeconfigFile==""){ + fmt.Println("error") + return; + } + filename := t.KubeconfigFile + hal_path := "/home/spinnaker/config" + timestamp := time.Now().Unix() + tm := time.Unix(timestamp, 0) + t.KubeconfigFile = hal_path + tm.Format("2006-01-02-15-04-05") + dest_container := "spinnaker/spin-halyard/halyard-daemon" + destPath := t.KubeconfigFile + dest := strings.Join([]string{dest_container, destPath}, ":") + cloverkube.CopyFileToPod(filename, dest) + newconfig, _ := yaml.Marshal(&t) + out_json, err := yaml.YAMLToJSON(newconfig) + if err != nil { + panic(err.Error()) + } + resp, err := resty.R(). + SetHeader("Content-Type", "application/json"). + SetBody(out_json). + Post(url) + if err != nil { + panic(err.Error()) + } + fmt.Printf("\n%v\n", resp) + +} diff --git a/clover/cloverctl/src/cloverctl/cmd/delete_docker_registry.go b/clover/cloverctl/src/cloverctl/cmd/delete_docker_registry.go new file mode 100644 index 0000000..d4403a5 --- /dev/null +++ b/clover/cloverctl/src/cloverctl/cmd/delete_docker_registry.go @@ -0,0 +1,52 @@ +// Copyright (c) Authors of Clover +// +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Apache License, Version 2.0 +// which accompanies this distribution, and is available at +// http://www.apache.org/licenses/LICENSE-2.0 + +package cmd + +import ( + "fmt" + "encoding/json" + + "gopkg.in/resty.v1" + "github.com/spf13/cobra" +) + +var deldockerproviderCmd = &cobra.Command{ + Use: "docker-registry", + Short: "delete one docker registry provider by name from spinnaker", + Long: ``, + Run: func(cmd *cobra.Command, args []string) { + deldockerProvider() + }, +} + +func init() { + providerdelCmd.AddCommand(deldockerproviderCmd) + deldockerproviderCmd.Flags().StringVarP(&name, "name", "n", "", "Input docker-registry account name") + deldockerproviderCmd.MarkFlagRequired("name") + +} + +func deldockerProvider() { + url := controllerIP + "/halyard/delprovider" + + var in = map[string]string{"name": name, "provider":"dockerRegistry"} + out_json, err := json.Marshal(in) + if err != nil { + fmt.Println("json.Marshal failed:", err) + return + } + resp, err := resty.R(). + SetHeader("Content-Type", "application/json"). + SetBody(out_json). + Post(url) + if err != nil { + panic(err.Error()) + } + fmt.Printf("\n%v\n", resp) + +} diff --git a/clover/cloverctl/src/cloverctl/cmd/delete_kubernetes.go b/clover/cloverctl/src/cloverctl/cmd/delete_kubernetes.go new file mode 100644 index 0000000..77b466a --- /dev/null +++ b/clover/cloverctl/src/cloverctl/cmd/delete_kubernetes.go @@ -0,0 +1,53 @@ +// Copyright (c) Authors of Clover +// +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Apache License, Version 2.0 +// which accompanies this distribution, and is available at +// http://www.apache.org/licenses/LICENSE-2.0 + +package cmd + +import ( + "fmt" + "encoding/json" + + "gopkg.in/resty.v1" + "github.com/spf13/cobra" +) + +var name string +var delkubeproviderCmd = &cobra.Command{ + Use: "kubernetes", + Short: "delete one kubernete provider by name from spinnaker", + Long: ``, + Run: func(cmd *cobra.Command, args []string) { + delProvider() + }, +} + +func init() { + providerdelCmd.AddCommand(delkubeproviderCmd) + delkubeproviderCmd.Flags().StringVarP(&name, "name", "n", "", "Input kubernetes account name") + delkubeproviderCmd.MarkFlagRequired("name") + +} + +func delProvider() { + url := controllerIP + "/halyard/delprovider" + + var in = map[string]string{"name": name, "provider":"kubernetes"} + out_json, err := json.Marshal(in) + if err != nil { + fmt.Println("json.Marshal failed:", err) + return + } + resp, err := resty.R(). + SetHeader("Content-Type", "application/json"). + SetBody(out_json). + Post(url) + if err != nil { + panic(err.Error()) + } + fmt.Printf("\n%v\n", resp) + +} diff --git a/clover/cloverctl/src/cloverctl/cmd/get_docker_registry.go b/clover/cloverctl/src/cloverctl/cmd/get_docker_registry.go new file mode 100644 index 0000000..93c1b3e --- /dev/null +++ b/clover/cloverctl/src/cloverctl/cmd/get_docker_registry.go @@ -0,0 +1,59 @@ +// Copyright (c) Authors of Clover +// +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Apache License, Version 2.0 +// which accompanies this distribution, and is available at +// http://www.apache.org/licenses/LICENSE-2.0 + +package cmd + +import ( + "fmt" + "strings" + "encoding/json" + + "gopkg.in/resty.v1" + "github.com/spf13/cobra" +) + + +var getdockerregistry = &cobra.Command{ + Use: "docker-registry", + Short: "Get docker registry from spinnaker", + Long: ``, + Run: func(cmd *cobra.Command, args []string) { + getdocker() + }, +} + +func init() { + providergetCmd.AddCommand(getdockerregistry) +} + +func getdocker() { + url := controllerIP + "/halyard/account" + + var provider = map[string]string{"name": "dockerRegistry"} + out_json, err := json.Marshal(provider) + if err != nil { + fmt.Println("json.Marshal failed:", err) + return + } + resp, err := resty.SetAllowGetMethodPayload(true).R(). + SetHeader("Content-Type", "application/json"). + SetBody(out_json). + Get(url) + if err != nil { + panic(err.Error()) + } + if resp.StatusCode() != 200 { + fmt.Printf("\n%v\n", resp) + return + } + + accounts := strings.Split(resp.String(), ":") + fmt.Printf("\n") + for index, account := range accounts{ + fmt.Printf("%d. %v\n",index + 1, account) + } +} diff --git a/clover/cloverctl/src/cloverctl/cmd/get_kubernetes.go b/clover/cloverctl/src/cloverctl/cmd/get_kubernetes.go new file mode 100644 index 0000000..16dcca1 --- /dev/null +++ b/clover/cloverctl/src/cloverctl/cmd/get_kubernetes.go @@ -0,0 +1,59 @@ +// Copyright (c) Authors of Clover +// +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Apache License, Version 2.0 +// which accompanies this distribution, and is available at +// http://www.apache.org/licenses/LICENSE-2.0 + +package cmd + +import ( + "fmt" + "strings" + "encoding/json" + + "gopkg.in/resty.v1" + "github.com/spf13/cobra" +) + + +var getkubeprovider = &cobra.Command{ + Use: "kubernetes", + Short: "Get kubernetes provider from spinnaker", + Long: ``, + Run: func(cmd *cobra.Command, args []string) { + getkube() + }, +} + +func init() { + providergetCmd.AddCommand(getkubeprovider) +} + +func getkube() { + url := controllerIP + "/halyard/account" + + var provider = map[string]string{"name": "kubernetes"} + out_json, err := json.Marshal(provider) + if err != nil { + fmt.Println("json.Marshal failed:", err) + return + } + resp, err := resty.SetAllowGetMethodPayload(true).R(). + SetHeader("Content-Type", "application/json"). + SetBody(out_json). + Get(url) + if err != nil { + panic(err.Error()) + } + if resp.StatusCode() != 200 { + fmt.Printf("\n%v\n", resp) + return + } + + accounts := strings.Split(resp.String(), ":") + fmt.Printf("\n") + for index, account := range accounts{ + fmt.Printf("%d. %v\n",index + 1, account) + } +} diff --git a/clover/cloverctl/src/cloverctl/cmd/provider.go b/clover/cloverctl/src/cloverctl/cmd/provider.go new file mode 100644 index 0000000..fc8e888 --- /dev/null +++ b/clover/cloverctl/src/cloverctl/cmd/provider.go @@ -0,0 +1,45 @@ +// Copyright (c) Authors of Clover +// +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Apache License, Version 2.0 +// which accompanies this distribution, and is available at +// http://www.apache.org/licenses/LICENSE-2.0 + +package cmd + +import ( + "fmt" + "github.com/spf13/cobra" +) + +var providercreateCmd = &cobra.Command{ + Use: "provider", + Short: "Add spinnaker provider", + Long: ``, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("provider called") + }, +} + +var providerdelCmd = &cobra.Command{ + Use: "provider", + Short: "Delete spinnaker provider", + Long: ``, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("provider called") + }, +} + +var providergetCmd = &cobra.Command{ + Use: "provider", + Short: "Get spinnaker provider", + Long: ``, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("provider called") + }, +} +func init() { + createCmd.AddCommand(providercreateCmd) + deleteCmd.AddCommand(providerdelCmd) + getCmd.AddCommand(providergetCmd) +} diff --git a/clover/cloverctl/src/cloverkube/main.go b/clover/cloverctl/src/cloverkube/main.go index e2854b5..7710a13 100644 --- a/clover/cloverctl/src/cloverkube/main.go +++ b/clover/cloverctl/src/cloverkube/main.go @@ -10,14 +10,20 @@ package cloverkube import ( "fmt" "os" - "path/filepath" - "strings" + "path/filepath" + "strings" + "io/ioutil" + "io" + "bytes" appsv1 "k8s.io/api/apps/v1" apiv1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/remotecommand" + ) func setClient() kubernetes.Interface { @@ -398,3 +404,93 @@ func GetPodsIP(pod_name string, namespace string) []string { return ips } + +func CopyFileToPod(src, dest string) error { + + // dest must be "namespace/podname/containername:" + pSplit := strings.Split(dest, ":") + pathPrefix := pSplit[0] + pathToCopy := pSplit[1] + + buffer, err := ioutil.ReadFile(src) + if err != nil { + fmt.Print(err) + } + + dir, _ := filepath.Split(pathToCopy) + command := "mkdir -p " + dir + _, stderr, err := Exec(pathPrefix, command, nil) + + if err != nil { + fmt.Print(err) + fmt.Print(stderr) + return err + } + + command = "cp /dev/stdin " + pathToCopy + stdin := bytes.NewReader(buffer) + _, stderr, err = Exec(pathPrefix, command, stdin) + + if err != nil { + fmt.Print(err) + fmt.Print(stderr) + return err + } + + return nil +} + + +func Exec(pathPrefix, command string, stdin io.Reader) ([]byte, []byte, error) { + clientset := setClient() + kubeconfig := filepath.Join( + os.Getenv("HOME"), ".kube", "config", + ) + config, err := clientcmd.BuildConfigFromFlags("", kubeconfig) + if err != nil { + panic(err.Error()) + } + + prefixSplit := strings.Split(pathPrefix, "/") + namespace := prefixSplit[0] + podName := prefixSplit[1] + containerName := prefixSplit[2] + + req := clientset.Core().RESTClient().Post(). + Resource("pods"). + Name(podName). + Namespace(namespace). + SubResource("exec") + scheme := runtime.NewScheme() + if err := apiv1.AddToScheme(scheme); err != nil { + return nil, nil, fmt.Errorf("error adding to scheme: %v", err) + } + + parameterCodec := runtime.NewParameterCodec(scheme) + req.VersionedParams(&apiv1.PodExecOptions{ + Command: strings.Fields(command), + Container: containerName, + Stdin: stdin != nil, + Stdout: true, + Stderr: true, + TTY: false, + }, parameterCodec) + + exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL()) + if err != nil { + return nil, nil, fmt.Errorf("error while creating Executor: %v", err) + } + + var stdout, stderr bytes.Buffer + err = exec.Stream(remotecommand.StreamOptions{ + Stdin: stdin, + Stdout: &stdout, + Stderr: &stderr, + Tty: false, + }) + if err != nil { + return nil, nil, fmt.Errorf("error in Stream: %v", err) + } + + return stdout.Bytes(), stderr.Bytes(), nil +} diff --git a/clover/controller/control/api/halyard.py b/clover/controller/control/api/halyard.py new file mode 100644 index 0000000..861de59 --- /dev/null +++ b/clover/controller/control/api/halyard.py @@ -0,0 +1,167 @@ +# Copyright (c) Authors of Clover +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 + +from flask import Blueprint, request, Response +import logging +import lib.halyard_base as base + +halyard = Blueprint('halyard', __name__) + +@halyard.route("/halyard/addkube", methods=['GET', 'POST']) +def addkubernetes(): + try: + p = request.json + accountname = p['Name'] + providerversion = p['ProviderVersion'] + Registries = p['DockerRegistries'] + kubeconfigFile = p['KubeconfigFile'] + + except (KeyError, ValueError) as e: + logging.debug(e) + return Response('Invalid value in kubernetes yaml', status=400) + + try: + if base.is_account_exist("kubernetes",accountname): + return Response("account name has already exist", status=400) + if providerversion == None or providerversion == 'V1': + providerversion = None + if 0 == len(Registries) or isinstance(Registries, list) == False: + return Response("V1 provider require dockerRegistries", status=400) + + dockerRegistries = [] + for registry in Registries: + registryname = registry['AccountName'] + if not base.is_account_exist("dockerRegistry",registryname): + return Response("docker registry: {0} don't exist".format(registryname), + status=400) + docker_dict = {"accountName":registryname, "namespaces":[]} + dockerRegistries.append(docker_dict) + data = { + "name": accountname, + "requiredGroupMembership": [], + "providerVersion": providerversion, + "permissions": {}, + "dockerRegistries": dockerRegistries, + "context": None, + "cluster": None, + "user": None, + "configureImagePullSecrets": "true", + "serviceAccount": None, + "cacheThreads": 1, + "namespaces": [], + "omitNamespaces": [], + "kinds": [], + "omitKinds": [], + "customResources": [], + "cachingPolicies": [], + "kubeconfigFile": kubeconfigFile, + "kubeconfigContents": None, + "kubectlPath": None, + "namingStrategy": None, + "skin": None, + "debug": None, + "oauthScopes": [], + "oauthServiceAccount": None, + "oAuthServiceAccount": None, + "oAuthScopes": [] + } + result = base.add_account("kubernetes",data) + except Exception as e: + logging.debug(e) + return Response('Failed add the kubernetes provider', status=400) + return result + +@halyard.route("/halyard/addregistry", methods=['GET', 'POST']) +def add_docker_registry(): + try: + p = request.json + accountname = p['name'] + address = p['address'] + repositories = p['repositories'] + if p.has_key('username') and p.has_key('password'): + username = p['username'] + password = p['password'] + else: + username = None + password = None + + except (KeyError, ValueError) as e: + logging.debug(e) + return Response('Invalid value in kubernetes yaml', status=400) + + try: + if base.is_account_exist("dockerRegistry",accountname): + return Response("account name has already exist", status=400) + + data = { + "name": accountname, + "requiredGroupMembership": [], + "providerVersion": None, + "permissions": {}, + "address": address, + "username": username, + "password": password, + "email": "fake.email@spinnaker.io", + "cacheIntervalSeconds": 30, + "clientTimeoutMillis": 60000, + "cacheThreads": 1, + "paginateSize": 100, + "sortTagsByDate": False, + "trackDigests": False, + "insecureRegistry": False, + "repositories": repositories, + "passwordFile": None, + "dockerconfigFile": None + } + result = base.add_account("dockerRegistry",data) + if result != "SUCCEEDED": + return Response('Failed to add the docker registry', status=400) + + except Exception as e: + logging.debug(e) + return Response('Failed to add the docker registry', status=400) + return result + +@halyard.route("/halyard/delprovider", methods=['GET', 'POST']) +def delprovider(): + try: + p = request.json + provider = p['provider'] + name = p['name'] + except (KeyError, ValueError) as e: + logging.debug(e) + return Response('Input invalid value', status=400) + try: + result = base.delete_account(provider, name) + if result != "SUCCEEDED": + print "Delete account failed" + return Response('Failed to delete the {0} provider'.format(provider), status=400) + + apply_result = base.apply_deploy() + if apply_result != "SUCCEEDED": + print "Delete account failed" + return Response('Failed to delete the {0} provider'.format(provider), status=400) + + except Exception as e: + logging.debug(e) + return Response('Failed to delete the kubernetes provider', status=400) + + return apply_result + + +@halyard.route("/halyard/account", methods=['GET', 'POST']) +def getprovider(): + try: + provider = "" + p = request.json + provider = p['name'] + account_list = base.list_accounts(provider) + result = ':'.join(account_list) + except Exception as e: + logging.debug(e) + return Response('get {0} failed'.format(provider), status=400) + return result diff --git a/clover/controller/control/control.py b/clover/controller/control/control.py index 54f713a..70eeacd 100644 --- a/clover/controller/control/control.py +++ b/clover/controller/control/control.py @@ -9,6 +9,7 @@ from flask import Flask, request, jsonify from views.dashboard import simple_page from api.collector import collector from api.snort import snort +from api.halyard import halyard from api.nginx import nginx from api.jmeter import jmeter from api.file_upload import file_upload @@ -23,6 +24,7 @@ try: application.register_blueprint(simple_page) application.register_blueprint(collector) application.register_blueprint(snort) + application.register_blueprint(halyard) application.register_blueprint(nginx) application.register_blueprint(jmeter) application.register_blueprint(file_upload) diff --git a/clover/controller/docker/Dockerfile b/clover/controller/docker/Dockerfile index 52d4673..9b8241f 100644 --- a/clover/controller/docker/Dockerfile +++ b/clover/controller/docker/Dockerfile @@ -15,7 +15,7 @@ RUN apt-get update && apt-get install -y \ # Install required python packages RUN python -m pip install gunicorn flask \ - grpcio protobuf jinja2 redis + grpcio protobuf jinja2 redis requests COPY /control /control COPY /process /process @@ -35,6 +35,7 @@ RUN cp clover/clover/collector/grpc/collector_pb2_grpc.py /control/api RUN cp clover/clover/collector/grpc/collector_pb2.py /control/api RUN cp clover/clover/tools/jmeter/jmeter-master/grpc/jmeter_pb2_grpc.py /control/api RUN cp clover/clover/tools/jmeter/jmeter-master/grpc/jmeter_pb2.py /control/api +RUN cp -rf clover/clover/spinnaker/lib/ /control/api RUN rm -rf /grpc_temp WORKDIR /process diff --git a/clover/controller/process/gunicorn_process.sh b/clover/controller/process/gunicorn_process.sh index 033596f..d2fa4ef 100755 --- a/clover/controller/process/gunicorn_process.sh +++ b/clover/controller/process/gunicorn_process.sh @@ -8,4 +8,7 @@ # http://www.apache.org/licenses/LICENSE-2.0 # -gunicorn --bind 0.0.0.0:8000 --chdir /control wsgi +# it take a long time to add kubernetes. So I increse the timeout +# and workers + +gunicorn --bind 0.0.0.0:8000 -t 1200 -w 5 --chdir /control wsgi diff --git a/clover/controller/process/nginx.conf b/clover/controller/process/nginx.conf index 5b26922..5d2dca9 100644 --- a/clover/controller/process/nginx.conf +++ b/clover/controller/process/nginx.conf @@ -11,6 +11,7 @@ http { listen 80; location / { + proxy_read_timeout 1200; include proxy_params; proxy_pass http://localhost:8000; } diff --git a/clover/orchestration/kube_client.py b/clover/orchestration/kube_client.py index f7fa708..582447b 100644 --- a/clover/orchestration/kube_client.py +++ b/clover/orchestration/kube_client.py @@ -9,6 +9,7 @@ from os import path import yaml from kubernetes import client, config +from kubernetes.stream import stream class KubeClient(object): @@ -120,3 +121,36 @@ class KubeClient(object): svc_name = body.get('metadata').get('name') return svc_name + + def copy_file_to_pod(self, source, destination, podname, namespace='default'): + # Note: only can copy file to the pod, which only include one container + exec_command = ['/bin/sh'] + resp = stream(self.core_v1.connect_get_namespaced_pod_exec, podname, + namespace, + command=exec_command, + stderr=True, stdin=True, + stdout=True, tty=False, + _preload_content=False) + + buffer = '' + with open(source, "rb") as file: + buffer += file.read() + + commands = [] + commands.append(bytes("cat <<'EOF' >" + destination + "\n")) + commands.append(buffer) + commands.append(bytes("EOF\n")) + + while resp.is_open(): + resp.update(timeout=1) + if resp.peek_stdout(): + print("STDOUT: %s" % resp.read_stdout()) + if resp.peek_stderr(): + print("STDERR: %s" % resp.read_stderr()) + if commands: + c = commands.pop(0) + resp.write_stdin(c) + else: + break + + resp.close() diff --git a/clover/spinnaker/__init__.py b/clover/spinnaker/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/clover/spinnaker/halyard.py b/clover/spinnaker/halyard.py new file mode 100755 index 0000000..20c0f4b --- /dev/null +++ b/clover/spinnaker/halyard.py @@ -0,0 +1,120 @@ +#!/bin/python + +import lib.halyard_base as base +import time +from clover.orchestration.kube_client import KubeClient + +namespace = 'spinnaker' +client = KubeClient() + +def list_accounts(provider): + account_list = base.list_accounts(provider) + return account_list + +def delete_account(provider, accountname): + result = base.delete_account(provider, accountname) + if result != "SUCCEEDED": + print "Delete account failed" + return result + + apply_result = base.apply_deploy() + if apply_result == "SUCCEEDED": + print "Delete account successfully" + else: + print "Delete account failed" + + return apply_result + +def add_k8s_account(accountname, kubeconfigfile, + providerversion=None, registries=[]): + # Note: if providerversion is V1, must provider registries. + if base.is_account_exist("kubernetes",accountname): + return "FAILED" + if providerversion == None or providerversion == 'V1': + providerversion = None + if 0 == len(registries) or isinstance(registries, list) == False: + print "please provider docker registries or the type of registries is not list" + return "FAILED" + # Copy kubectl file to halyard pod + hal_kubeconfigfile = "/home/spinnaker/config" + \ + time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime()) + spinnaker_pods = client.find_pod_by_namespace(namespace) + for pod in spinnaker_pods: + if pod.find("spin-halyard") == 0: + client.copy_file_to_pod(kubeconfigfile, + hal_kubeconfigfile, + pod, namespace) + dockerRegistries = [] + for registry in registries: + if not base.is_account_exist("dockerRegistry",registry): + print ("Please add docker registry: %s" %registry) + return "FAILED" + docker_dict = {"accountName":registry, "namespaces":[]} + dockerRegistries.append(docker_dict) + + data = { + "name": accountname, + "requiredGroupMembership": [], + "providerVersion": providerversion, + "permissions": {}, + "dockerRegistries": dockerRegistries, + "context": None, + "cluster": None, + "user": None, + "configureImagePullSecrets": "true", + "serviceAccount": None, + "cacheThreads": 1, + "namespaces": [], + "omitNamespaces": [], + "kinds": [], + "omitKinds": [], + "customResources": [], + "cachingPolicies": [], + "kubeconfigFile": hal_kubeconfigfile, + "kubeconfigContents": None, + "kubectlPath": None, + "namingStrategy": None, + "skin": None, + "debug": None, + "oauthScopes": [], + "oauthServiceAccount": None, + "oAuthServiceAccount": None, + "oAuthScopes": [] + } +# print data + result = base.add_account("kubernetes",data) + return result + +def add_docker_account(address, accountname, repositories=[], + username=None, password=None, validate='true'): + + if base.is_account_exist("dockerRegistry",accountname): + return "FAILED" + + data = { + "name": accountname, + "requiredGroupMembership": [], + "providerVersion": None, + "permissions": {}, + "address": address, + "username": username, + "password": password, + "email": "fake.email@spinnaker.io", + "cacheIntervalSeconds": 30, + "clientTimeoutMillis": 60000, + "cacheThreads": 1, + "paginateSize": 100, + "sortTagsByDate": False, + "trackDigests": False, + "insecureRegistry": False, + "repositories": repositories, + "passwordFile": None, + "dockerconfigFile": None + } + result = base.add_account("dockerRegistry",data) + if result == "SUCCEEDED": + print "Add account successfully" + else: + print "Add account failed" + return result + diff --git a/clover/spinnaker/halyard_sample.py b/clover/spinnaker/halyard_sample.py new file mode 100644 index 0000000..b78be9b --- /dev/null +++ b/clover/spinnaker/halyard_sample.py @@ -0,0 +1,41 @@ +# Copyright (c) Authors of Clover +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 + +import halyard + + +# please install the spinnaker useing the spinnaker/install/quick-install-spinnaker.yml first +print "######## add docker provider ##########" +address = "https://index.docker.io" +repositories = ['wtwde/onap', 'wtwde/spinnaker', 'library/nginx'] +add_docker = halyard.add_docker_account(address, "docker-test", repositories) +print "######## result ##########" +print add_docker + +print "######## add kubernetes provider ##########" +add_k8s = halyard.add_k8s_account('my-k8s-v1-t11561', "/root/config.115", "V1", ['dockerhub']) +print "######## result ##########" +print add_k8s + +print "######## k8s account list ##########" +k8s_list = halyard.list_accounts('kubernetes') +print "####### result ##########" +print k8s_list + +print "######## docker account list ##########" +docker_list = halyard.list_accounts('dockerRegistry') +print "####### result ##########" +print docker_list + +print "######## delete kubernetes provider ##########" +del_k8s = halyard.delete_account('kubernetes','my-k8s-v1-t11561') +print "######## result ##########" + +print "######## delete docker registry provider ##########" +del_docker = halyard.delete_account('dockerRegistry','docker-test') +print "######## result ##########" +print del_docker diff --git a/clover/spinnaker/install/quick-install-spinnaker.yml b/clover/spinnaker/install/quick-install-spinnaker.yml new file mode 100644 index 0000000..c935453 --- /dev/null +++ b/clover/spinnaker/install/quick-install-spinnaker.yml @@ -0,0 +1,376 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: spinnaker + +--- + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: spinnaker-admin +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: default + namespace: spinnaker + +--- + +apiVersion: v1 +kind: Pod +metadata: + name: spin-halyard + namespace: spinnaker + labels: + app: spin + stack: halyard +spec: + containers: + - name: halyard-daemon + # todo - make :stable or digest of :stable + image: gcr.io/spinnaker-marketplace/halyard:stable + imagePullPolicy: Always + command: + - /bin/sh + args: + - -c + # when the configmap is mounted directly at /home/spinnaker/.hal the halyard daemon + # isn't able to replace the contents of the mount with user modifications. + # so instead we mount the configmap elsewhere and copy the files into + # place when the container starts. + - "cp -R /home/spinnaker/staging/.hal /home/spinnaker/.hal && /opt/halyard/bin/halyard" + readinessProbe: + exec: + command: + - wget + - -q + - --spider + - http://localhost:8064/health + ports: + - containerPort: 8064 + volumeMounts: + - name: halconfig + mountPath: /home/spinnaker/staging/.hal/config + subPath: config + - name: halconfig + mountPath: /home/spinnaker/staging/.hal/default/service-settings/deck.yml + subPath: deck.yml + - name: halconfig + mountPath: /home/spinnaker/staging/.hal/default/service-settings/gate.yml + subPath: gate.yml + - name: halconfig + mountPath: /home/spinnaker/staging/.hal/default/service-settings/igor.yml + subPath: igor.yml + - name: halconfig + mountPath: /home/spinnaker/staging/.hal/default/service-settings/fiat.yml + subPath: fiat.yml + - name: halconfig + mountPath: /home/spinnaker/staging/.hal/default/profiles/front50-local.yml + subPath: front50-local.yml + volumes: + - name: halconfig + configMap: + name: halconfig +--- + +apiVersion: v1 +kind: Service +metadata: + name: spin-halyard + namespace: spinnaker +spec: + ports: + - port: 8064 + targetPort: 8064 + protocol: TCP + selector: + app: spin + stack: halyard + +--- + +apiVersion: v1 +kind: ConfigMap +metadata: + name: halconfig + namespace: spinnaker +data: + igor.yml: | + enabled: true + fiat.yml: | + enabled: false + skipLifeCycleManagement: true + front50-local.yml: | + spinnaker.s3.versioning: false + gate.yml: | + host: 0.0.0.0 + deck.yml: | + host: 0.0.0.0 + env: + API_HOST: http://spin-gate.spinnaker:8084/ + config: | + currentDeployment: default + deploymentConfigurations: + - name: default + version: 1.7.0 + providers: + appengine: + enabled: false + accounts: [] + aws: + enabled: false + accounts: [] + defaultKeyPairTemplate: '{{name}}-keypair' + defaultRegions: + - name: us-west-2 + defaults: + iamRole: BaseIAMRole + azure: + enabled: false + accounts: [] + bakeryDefaults: + templateFile: azure-linux.json + baseImages: [] + dcos: + enabled: false + accounts: [] + clusters: [] + dockerRegistry: + enabled: true + accounts: + - name: dockerhub + address: https://index.docker.io + repositories: + - opnfv/clover + google: + enabled: false + accounts: [] + bakeryDefaults: + templateFile: gce.json + baseImages: [] + zone: us-central1-f + network: default + useInternalIp: false + kubernetes: + enabled: true + accounts: + - name: my-kubernetes-account + requiredGroupMembership: [] + providerVersion: V2 + dockerRegistries: [] + configureImagePullSecrets: true + serviceAccount: true + namespaces: [] + omitNamespaces: [] + kinds: [] + omitKinds: [] + customResources: [] + oauthScopes: [] + oAuthScopes: [] + primaryAccount: my-kubernetes-account + openstack: + enabled: false + accounts: [] + bakeryDefaults: + baseImages: [] + oraclebmcs: + enabled: false + accounts: [] + deploymentEnvironment: + size: SMALL + type: Distributed + accountName: my-kubernetes-account + updateVersions: true + consul: + enabled: false + vault: + enabled: false + customSizing: {} + gitConfig: + upstreamUser: spinnaker + persistentStorage: + persistentStoreType: s3 + azs: {} + gcs: + rootFolder: front50 + redis: {} + s3: + bucket: spinnaker-artifacts + rootFolder: front50 + endpoint: http://minio-service.spinnaker:9000 + accessKeyId: dont-use-this + secretAccessKey: for-production + oraclebmcs: {} + features: + auth: false + fiat: false + chaos: false + entityTags: false + jobs: false + metricStores: + datadog: + enabled: false + prometheus: + enabled: false + add_source_metalabels: true + stackdriver: + enabled: false + period: 30 + enabled: false + notifications: + slack: + enabled: false + timezone: America/Los_Angeles + ci: + jenkins: + enabled: true + masters: [] + travis: + enabled: false + masters: [] + security: + apiSecurity: + ssl: + enabled: false + overrideBaseUrl: /gate + uiSecurity: + ssl: + enabled: false + authn: + oauth2: + enabled: false + client: {} + resource: {} + userInfoMapping: {} + saml: + enabled: false + ldap: + enabled: false + x509: + enabled: false + enabled: false + authz: + groupMembership: + service: EXTERNAL + google: + roleProviderType: GOOGLE + github: + roleProviderType: GITHUB + file: + roleProviderType: FILE + enabled: false + artifacts: + gcs: + enabled: false + accounts: [] + github: + enabled: false + accounts: [] + http: + enabled: false + accounts: [] + pubsub: + google: + enabled: false + subscriptions: [] + +--- + +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: minio-pv-claim + namespace: spinnaker + labels: + app: minio-storage-claim +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi + storageClassName: standard + +--- + +apiVersion: apps/v1beta1 +kind: Deployment +metadata: + # This name uniquely identifies the Deployment + name: minio-deployment + namespace: spinnaker +spec: + strategy: + type: Recreate + template: + metadata: + labels: + app: minio + spec: + volumes: + - name: storage + persistentVolumeClaim: + claimName: minio-pv-claim + containers: + - name: minio + image: minio/minio + args: + - server + - /storage + env: + - name: MINIO_ACCESS_KEY + value: "dont-use-this" + - name: MINIO_SECRET_KEY + value: "for-production" + ports: + - containerPort: 9000 + volumeMounts: + - name: storage + mountPath: /storage + +--- + +apiVersion: v1 +kind: Service +metadata: + name: minio-service + namespace: spinnaker +spec: + ports: + - port: 9000 + targetPort: 9000 + protocol: TCP + selector: + app: minio + +--- + +apiVersion: batch/v1 +kind: Job +metadata: + name: hal-deploy-apply + namespace: spinnaker + labels: + app: job + stack: hal-deploy +spec: + template: + metadata: + labels: + app: job + stack: hal-deploy + spec: + restartPolicy: OnFailure + containers: + - name: hal-deploy-apply + # todo use a custom image + image: gcr.io/spinnaker-marketplace/halyard:stable + command: + - /bin/sh + args: + - -c + - "hal deploy apply --daemon-endpoint http://spin-halyard.spinnaker:8064" diff --git a/clover/spinnaker/lib/__init__.py b/clover/spinnaker/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/clover/spinnaker/lib/halyard_base.py b/clover/spinnaker/lib/halyard_base.py new file mode 100755 index 0000000..7245d6a --- /dev/null +++ b/clover/spinnaker/lib/halyard_base.py @@ -0,0 +1,157 @@ +#!/bin/python + + +import requests +import json +import time + +base_url="http://spin-halyard.spinnaker:8064/v1" +namespace= 'spinnaker' +headers = {'content-type': 'application/json; charset=UTF-8'} +debug = False + +def get(url): + result = requests.get(url) + return result.json() + +def post(url, data = None, headers = None): + result = requests.post(url, data=data, headers=headers) + return result.json() + +def put(url, data = None, headers = None): + result = requests.put(url, data=data, headers=headers) + return result.json() + +def delete(url): + result = requests.delete(url) + return result.json() + +def print_dict_info(dict_info, debug = False): + if dict_info == None: + return None + + if debug == True: + for v,k in dict_info.items(): + print('{v}:{k}'.format(v = v, k = k)) + else: + print dict_info.get('name') + print dict_info.get('state') + +def is_account_exist(provider, accountname): + exist_accounts = list_accounts(provider) + if accountname in exist_accounts: + print "account exists" + return True + print "account doesn't exist" + return False + +def get_task_info(uuid): + if uuid == None: + return None + url = base_url + "/tasks/" + uuid + "/" + result = get(url) + return result + +def wait_task_successful(uuid): + flag = "" + while True: + resp = get_task_info(uuid) + state = resp.get('state') + if flag != state: + print_dict_info(resp, debug) + flag = state + if state == "SUCCEEDED": + return "SUCCEEDED", resp + if state == "FAILED": + return "FAILED", resp + if resp.get('timedOut'): + return "TimeOut", resp + +def get_current_deployment(): + '''get the current deployment and check the state''' + + url = base_url + "/config/currentDeployment" + result = get(url) + uuid = result.get('uuid') + task_info = get_task_info(uuid) + print_dict_info(task_info, debug) + + return task_info + +def apply_deploy(): + """ + after using api to config halyard, it need ruan apply deploy. + """ + prep_url = base_url + "/config/deployments/default/prep/?validate=true" + deploy_url = base_url + "/config/deployments/default/deploy/?validate=false" + data='""' + result = post(prep_url, data=data, headers=headers) + uuid = result.get('uuid') + result, task_info = wait_task_successful(uuid) + if result != "SUCCEEDED": + return result + result = post(deploy_url, data=data, headers=headers) + uuid = result.get('uuid') + result, task_info = wait_task_successful(uuid) + return result + +def list_accounts(provider): + """ + According to the provider, list all accounts + """ + url = base_url + "/config/deployments/default/providers/" + \ + provider + "/?validate=true" + resp = get(url) + uuid = resp.get('uuid') + result, task_info = wait_task_successful(uuid) + if result != "SUCCEEDED": + print "Get account failed" + return None + accounts = task_info.get('response').get('responseBody').get('accounts') + account_list = [] + for account in accounts: + account_name = account.get('name') + account_list.append(account_name) + return account_list + +def enable_provider(provider, data='true'): + """ + if needs to add a provider, it is necessary to enable the provider + """ + url = base_url + "/config/deployments/default/providers/" + \ + provider + "/enabled/?validate=true" + resp = put(url,data=data,headers=headers) + uuid = resp.get('uuid') + result, task_info = wait_task_successful(uuid) + return result + +def add_account(provider, data): + url = base_url + "/config/deployments/default/providers/" + \ + provider + "/accounts/?validate=true" + + enable_provider(provider) + + resp = post(url, data=json.dumps(data), headers=headers) + uuid = resp.get('uuid') + result, task_info = wait_task_successful(uuid) + if result != "SUCCEEDED": + print "Add account failed" + return result + apply_result = apply_deploy() + if apply_result == "SUCCEEDED": + print "Deployment successful" + else: + print "Deployment failed" + return apply_result + +def delete_account(provider, accountname): + if not is_account_exist(provider, accountname): + return "FAILED" + url = base_url + "/config/deployments/default/providers/" + \ + provider + "/accounts/account/" + accountname + "/?validate=true" + resp = delete(url) + uuid = resp.get('uuid') + result, task_info = wait_task_successful(uuid) + + return result + diff --git a/clover/tools/yaml/cassandra.yaml b/clover/tools/yaml/cassandra.yaml index 83ed20f..0206d75 100644 --- a/clover/tools/yaml/cassandra.yaml +++ b/clover/tools/yaml/cassandra.yaml @@ -20,7 +20,7 @@ # note that this value rely on a functioning DNS on kubernetes to resolve the IP # # This static method can be made dynamic by using Helm -# or bash script to replace the namespace on the fly. +# or bash script to replace the namespace on the fly. # # - Cassandra launch only 1 replica in the cluster # to dynamically scale up or down, you can use: -- cgit 1.2.3-korg