diff --git a/cmd/injector/injector.go b/cmd/injector/injector.go new file mode 100644 index 0000000..79151a9 --- /dev/null +++ b/cmd/injector/injector.go @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 ( + "os" + + "github.com/apache/servicecomb-mesher/injection/cmd" + "github.com/go-mesh/openlogging" + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + app.HideVersion = true + app.Name = "injector" + app.Usage = "Kubernetes webhook for automatic ServiceComb mesher injection." + app.Commands = []cli.Command{ + cmd.GetCmdStart(), + cmd.GetCmdVersion(), + } + + err := app.Run(os.Args) + if err != nil { + openlogging.Error("run app command line failed: " + err.Error()) + } + return +} diff --git a/deployments/kubernetes/injector/mesher-injector.yaml b/deployments/kubernetes/injector/mesher-injector.yaml new file mode 100644 index 0000000..7f7a61d --- /dev/null +++ b/deployments/kubernetes/injector/mesher-injector.yaml @@ -0,0 +1,153 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +--- +# svccomb-mesher-injector configmap +apiVersion: v1 +kind: ConfigMap +metadata: + namespace: svccomb-system + name: svccomb-mesher-injector-configmap +data: + injectorconfig.yaml: | + mesher: + image: servicecomb/mesher-sidecar + tag: 1.6.3 + serviceCenter: + name: servicecenter + namespace: servicecomb + injectortemplate.yaml: | + apiVersion: v1 + kind: Pod + spec: + containers: + - env: + - name: http_proxy + value: http://127.0.0.1:{{.Mesher.HTTPPort}} + name: {{.App.Name}} + - env: + - name: SPECIFIC_ADDR + value: 127.0.0.1:{{.App.Port}} + - name: SERVICE_NAME + value: {{.App.Name}} + - name: VERSION + value: {{.App.Version}} + - name: CSE_REGISTRY_ADDR + value: {{.ServiceCenter.Address}} + image: {{.Mesher.Image}}:{{.Mesher.Tag}} + imagePullPolicy: IfNotPresent + name: {{.Mesher.Name}} + ports: + - containerport: {{.Mesher.GRPCPort}} + name: grpc + protocol: TCP + - containerport: {{.Mesher.HTTPPort}} + name: http + protocol: TCP + - containerport: {{.Mesher.AdminPort}} + name: rest-admin + protocol: TCP +--- +# svccomb-mesher-injector webhook +apiVersion: admissionregistration.k8s.io/v1beta1 +kind: MutatingWebhookConfiguration +metadata: + name: svccomb-mesher-injector + labels: + app: mesherInjectorWebhook + chart: mesherInjectorWebhook +webhooks: + - name: servicecomb.apache.org + clientConfig: + service: + name: svccomb-mesher-injector + namespace: svccomb-system + path: "/v1/mesher/inject" + caBundle: ${CA_BUNDLE} + rules: + - operations: [ "CREATE" ] + apiGroups: [""] + apiVersions: ["v1"] + resources: ["pods"] + failurePolicy: Fail + namespaceSelector: + matchLabels: + svccomb-injection: enabled +--- +# svccomb-mesher-injector service +apiVersion: v1 +kind: Service +metadata: + name: svccomb-mesher-injector + namespace: svccomb-system + labels: + app: mesherInjectorWebhook + chart: mesherInjectorWebhook + svccomb: mesher-injector +spec: + ports: + - port: 443 + name: https + selector: + svccomb: mesher-injector +--- +# svccomb-mesher-injector deployment +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: svccomb-mesher-injector + namespace: svccomb-system + labels: + app: mesherInjectorWebhook + chart: mesherInjectorWebhook + svccomb: mesher-injector +spec: + replicas: 1 + selector: + matchLabels: + svccomb: mesher-injector + template: + metadata: + labels: + app: mesherInjectorWebhook + chart: mesherInjectorWebhook + svccomb: mesher-injector + spec: + containers: + - name: mesher-injector-webhook + image: servicecomb/mesher-injector + args: + - --tlsCertPath=/etc/certs/cert.pem + - --tlsKeyPath=/etc/certs/key.pem + - --configPath=/etc/configs/injectorconfig.yaml + - --templatePath=/etc/configs/injectortemplate.yaml + imagePullPolicy: IfNotPresent #Always + ports: + - containerPort: 443 + volumeMounts: + - name: certs + mountPath: /etc/certs + readOnly: true + - name: configs + mountPath: /etc/configs + readOnly: true + volumes: + - name: certs + secret: + secretName: svccomb-mesher-injector-service-account + - name: configs + configMap: + name: svccomb-mesher-injector-configmap + diff --git a/deployments/kubernetes/injector/svccomb-system.yaml b/deployments/kubernetes/injector/svccomb-system.yaml new file mode 100644 index 0000000..ab49d59 --- /dev/null +++ b/deployments/kubernetes/injector/svccomb-system.yaml @@ -0,0 +1,21 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: v1 +kind: Namespace +metadata: + name: svccomb-system + labels: + svccomb-injection: disabled diff --git a/docker/injector/Dockerfile b/docker/injector/Dockerfile new file mode 100644 index 0000000..140bc02 --- /dev/null +++ b/docker/injector/Dockerfile @@ -0,0 +1,27 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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.12.10 as builder + +COPY . /go/src/github.com/apache/servicecomb-mesher/ +WORKDIR /go/src/github.com/apache/servicecomb-mesher/ +ENV GOPROXY=https://goproxy.io \ + GO111MODULE=on +RUN go build -a github.com/apache/servicecomb-mesher/cmd/injector + +FROM frolvlad/alpine-glibc:latest +WORKDIR /home +COPY --from=builder /go/src/github.com/apache/servicecomb-mesher/injector /home +ENTRYPOINT ["./injector", "start"] diff --git a/docs/injection/injection.md b/docs/injection/injection.md new file mode 100644 index 0000000..e0e4ea7 --- /dev/null +++ b/docs/injection/injection.md @@ -0,0 +1,70 @@ +# Masher injection + +Mesher can be used with any application language on any infrastructure. On Kubernetes, we inject Mesher into Pods, and applications can take advantage of all its features. Below we introduce the use of Mesher injector by example. + +## Before +1. Install Servcicecomb [service-center](http://servicecomb.apache.org/docs/service-center/install/#deployment-with-kubernetes) + + +2. Download the example of quick_start +```bash +$ git clone https://github.com/apache/servicecomb-mesher.git +$ cd ./servicecomb-mesher/examples/quick_start +``` + +3. Build docker images +```bash +$ cd ./mesher_injection +$ bash build_images.sh +``` + +## Automatic Mesher injection +Meshers can be automatically added to applicable Kubernetes pods using a mutating webhook admission controller provided by Mesher Injector. + +1. Create namespace "svccomb-system" + ```bash + cp ../../../deployments/kubernetes/injector/*.yaml . + $ kubectl apply -f svccomb-system.yaml + namespace/svccomb-system created + ``` +2. Generate Injector's certificatesigningrequest and secret + ```bash + $ wget https://raw.githubusercontent.com/morvencao/kube-mutating-webhook-tutorial/master/deployment/webhook-create-signed-cert.sh + + $ bash webhook-create-signed-cert.sh --service svccomb-mesher-injector --namespace svccomb-system --secret svccomb-mesher-injector-service-account + ``` +3. Query caBundle and fill it into "mesher-injector.yaml" + ```bash + $ CA_BUNDLE=$(kubectl config view --raw --minify --flatten -o jsonpath='{.clusters[].cluster.certificate-authority-data}') + + $ sed -i "s|\${CA_BUNDLE}|${CA_BUNDLE}|g" mesher-injector.yaml + ``` +4. Deploy mesher injecter + ```bash + $ kubectl apply -f mesher-injector.yaml + mutatingwebhookconfiguration.admissionregistration.k8s.io/svccomb-mesher-injector configured + service/svccomb-mesher-injector created + deployment.extensions/svccomb-mesher-injector created + ``` +5. Deploy examples to Kubernetes + ```bash + $ kubectl apply -f svccomb-test.yaml + namespace/svccomb-test created + + $ kubectl -n svccomb-test apply -f calculator.yaml + service/calculator created + deployment.extensions/calculator-python created + + $ kubectl -n svccomb-test apply -f webapp.yaml + service/webapp created + deployment.extensions/webapp-node created + ``` +6. Validated results + ```bash + $ kubectl get svc + NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE + calculator ClusterIP 10.104.2.143 5000/TCP 3m43s + kubernetes ClusterIP 10.96.0.1 443/TCP 42d + webapp NodePort 10.104.134.148 5001:30062/TCP 3m35s + ``` + Open the page "http://127.0.0.1:30062" in your browser, enter your height and weight in the input boxes, and click the submit button, you can see the BMI results about you. \ No newline at end of file diff --git a/examples/quick_start/mesher_injection/Dockerfile_calculator b/examples/quick_start/mesher_injection/Dockerfile_calculator new file mode 100644 index 0000000..b0919b3 --- /dev/null +++ b/examples/quick_start/mesher_injection/Dockerfile_calculator @@ -0,0 +1,22 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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 python:2 + +WORKDIR /usr/src/app + +COPY httpserver_calculator/ ./ + +CMD [ "python", "httpserver_calculator.py" ] \ No newline at end of file diff --git a/examples/quick_start/mesher_injection/Dockerfile_webapp b/examples/quick_start/mesher_injection/Dockerfile_webapp new file mode 100644 index 0000000..fc935cd --- /dev/null +++ b/examples/quick_start/mesher_injection/Dockerfile_webapp @@ -0,0 +1,24 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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 node:10.16.2 + +WORKDIR /home + +COPY httpserver_webapp/ ./ + +RUN npm install + +CMD [ "node", "httpserver_webapp.js" ] \ No newline at end of file diff --git a/examples/quick_start/mesher_injection/build_images.sh b/examples/quick_start/mesher_injection/build_images.sh new file mode 100755 index 0000000..3cb4113 --- /dev/null +++ b/examples/quick_start/mesher_injection/build_images.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +echo "copy resource" +cp -r ../httpserver_* . + +echo "build calculator image" +cp -f Dockerfile_calculator Dockerfile +docker build -t servicecomb/calculator-python:latest . + +echo "build webapp image" +cp -f Dockerfile_webapp Dockerfile +docker build -t servicecomb/webapp-node:latest . + +echo "clean" +rm -rf httpserver_* Dockerfile \ No newline at end of file diff --git a/examples/quick_start/mesher_injection/mesher-bmi.yaml b/examples/quick_start/mesher_injection/mesher-bmi.yaml new file mode 100644 index 0000000..49e8855 --- /dev/null +++ b/examples/quick_start/mesher_injection/mesher-bmi.yaml @@ -0,0 +1,96 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +# svccomb-test namespace +apiVersion: v1 +kind: Namespace +metadata: + name: svccomb-test + labels: + svccomb-injection: enabled +--- +# calculator service +apiVersion: v1 +kind: Service +metadata: + name: calculator + namespace: svccomb-test + labels: + app: calculator +spec: + ports: + - port: 5000 + name: http + targetPort: 4540 + selector: + app: calculator +--- +# calculator deployment +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: calculator-python + namespace: svccomb-test +spec: + replicas: 1 + template: + metadata: + labels: + app: calculator + spec: + containers: + - name: calculator + image: servicecomb/calculator-python:latest + imagePullPolicy: IfNotPresent #Always + ports: + - containerPort: 4540 +--- +# webapp service +apiVersion: v1 +kind: Service +metadata: + name: webapp + namespace: svccomb-test + labels: + app: webapp +spec: + type: NodePort + ports: + - port: 5001 + name: http + targetPort: 4597 + nodePort: 30062 + selector: + app: webapp +--- +# webapp deployment +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: webapp-node + namespace: svccomb-test +spec: + replicas: 1 + template: + metadata: + labels: + app: webapp + spec: + containers: + - name: webapp + image: servicecomb/webapp-node:latest + imagePullPolicy: IfNotPresent #Always + ports: + - containerPort: 4597 \ No newline at end of file diff --git a/go.mod b/go.mod index bf3ce08..1753e88 100644 --- a/go.mod +++ b/go.mod @@ -24,6 +24,7 @@ require ( golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 google.golang.org/grpc v1.19.1 gopkg.in/yaml.v2 v2.2.4 + k8s.io/api v0.17.0 k8s.io/apimachinery v0.17.0 k8s.io/client-go v0.17.0 ) diff --git a/injection/cmd/start.go b/injection/cmd/start.go new file mode 100644 index 0000000..588a32e --- /dev/null +++ b/injection/cmd/start.go @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 cmd + +import ( + "github.com/apache/servicecomb-mesher/injection/config" + "github.com/apache/servicecomb-mesher/injection/server" + "github.com/go-mesh/openlogging" + "github.com/urfave/cli" +) + +var conf = config.DefaultConfig() + +// GetCmdStart return service stated configure +func GetCmdStart() cli.Command { + return cli.Command{ + Name: "start", + Usage: "start sidecar injection server", + Action: start, + Flags: []cli.Flag{ + cli.IntFlag{ + Name: "port", + Usage: "webhook server port", + Value: conf.Port, + Destination: &conf.Port, + }, + cli.StringFlag{ + Name: "tlsCertPath", + Usage: "path of tls cert", + Value: conf.CertFile, + Destination: &conf.CertFile, + }, + cli.StringFlag{ + Name: "tlsKeyPath", + Usage: "path of tls key", + Value: conf.KeyFile, + Destination: &conf.KeyFile, + }, + cli.StringFlag{ + Name: "templateName", + Usage: "template name of sidecar", + Value: conf.TemplateName, + Destination: &conf.TemplateName, + }, + cli.StringFlag{ + Name: "configPath", + Usage: "path of sidecar config", + Destination: &conf.SidecarConfig, + }, + cli.StringFlag{ + Name: "templatePath", + Usage: "path of sidecar template", + Destination: &conf.SidecarTemplate, + }, + }, + } +} + +func start(c *cli.Context) { + if err := server.Run(conf); err != nil { + openlogging.Error("start webhook server failed:" + err.Error()) + panic(err) + } +} diff --git a/injection/cmd/start_test.go b/injection/cmd/start_test.go new file mode 100644 index 0000000..4f8a38a --- /dev/null +++ b/injection/cmd/start_test.go @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 cmd + +import ( + "flag" + "io/ioutil" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/urfave/cli" +) + +func getCliContext(args []string) (*cli.Context, error) { + app := cli.NewApp() + app.HideVersion = true + app.Name = "injector" + app.Usage = "Kubernetes webhook for automatic ServiceComb mesher injection." + app.Commands = []cli.Command{ + GetCmdStart(), + GetCmdVersion(), + } + + flagSet := flag.NewFlagSet(app.Name, flag.ContinueOnError) + flagSet.SetOutput(ioutil.Discard) + err := flagSet.Parse(args[1:]) + if err != nil { + return nil, err + } + return cli.NewContext(app, flagSet, nil), nil +} + +func TestStart(t *testing.T) { + defer func() { + if info := recover(); info != nil { + t.Log("has panic") + } + }() + ctx, err := getCliContext([]string{"test", "start"}) + assert.Nil(t, err) + start(ctx) +} diff --git a/injection/cmd/version.go b/injection/cmd/version.go new file mode 100644 index 0000000..d46d133 --- /dev/null +++ b/injection/cmd/version.go @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 cmd + +import ( + "github.com/apache/servicecomb-mesher/proxy/resource/v1/version" + "github.com/go-mesh/openlogging" + "github.com/urfave/cli" +) + +// GetCmdVersion return injector server version +func GetCmdVersion() cli.Command { + return cli.Command{ + Name: "version", + Usage: "print injection server version", + Action: printVersion, + } +} + +// output injector version. +func printVersion(c *cli.Context) { + versionID := version.Ver().Version + if versionID == "" { + versionID = version.DefaultVersion + } + openlogging.Info("injector server version: " + versionID) +} diff --git a/injection/cmd/version_test.go b/injection/cmd/version_test.go new file mode 100644 index 0000000..517145d --- /dev/null +++ b/injection/cmd/version_test.go @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 cmd + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPrintVersion(t *testing.T) { + ctx, err := getCliContext([]string{"test", "version"}) + assert.Nil(t, err) + printVersion(ctx) +} diff --git a/injection/config/config.go b/injection/config/config.go new file mode 100644 index 0000000..f4e6391 --- /dev/null +++ b/injection/config/config.go @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 config + +import "github.com/apache/servicecomb-mesher/injection/templates/servicecomb" + +// Config is a pointer of struct Config +type Config struct { + Port int + CertFile string + KeyFile string + SidecarConfig string + SidecarTemplate string + TemplateName string +} + +// DefaultConfig return default configure for stated +func DefaultConfig() *Config { + return &Config{ + Port: 443, + CertFile: "/etc/certs/cert.pem", + KeyFile: "/etc/certs/key.pem", + TemplateName: servicecomb.TemplateName, + } +} diff --git a/injection/config/config_test.go b/injection/config/config_test.go new file mode 100644 index 0000000..1110f4b --- /dev/null +++ b/injection/config/config_test.go @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 config + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDefaultConfig(t *testing.T) { + conf := DefaultConfig() + assert.NotNil(t, conf) +} diff --git a/injection/server/server.go b/injection/server/server.go new file mode 100644 index 0000000..b376291 --- /dev/null +++ b/injection/server/server.go @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 server + +import ( + "github.com/apache/servicecomb-mesher/injection/config" + "github.com/apache/servicecomb-mesher/injection/webhook" + "github.com/go-mesh/openlogging" +) + +// Run Webhhook server +func Run(conf *config.Config) error { + webHook, err := webhook.NewWebhook( + webhook.WithListenPort(conf.Port), + webhook.WithCertFile(conf.CertFile), + webhook.WithKeyFile(conf.KeyFile), + webhook.WithTemplateName(conf.TemplateName), + webhook.WithSidecarConfig(conf.SidecarConfig), + webhook.WithSidecarTemplate(conf.SidecarTemplate), + ) + if err != nil { + openlogging.Error("new webhook failed: " + err.Error()) + return err + } + return webHook.Run() +} diff --git a/injection/server/server_test.go b/injection/server/server_test.go new file mode 100644 index 0000000..0569ba5 --- /dev/null +++ b/injection/server/server_test.go @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 server + +import ( + "testing" + + "github.com/apache/servicecomb-mesher/injection/config" + "github.com/stretchr/testify/assert" +) + +func TestRun(t *testing.T) { + conf := config.DefaultConfig() + err := Run(conf) + assert.NotNil(t, err) + + conf.TemplateName = "test-notfound" + err = Run(conf) + assert.NotNil(t, err) +} diff --git a/injection/templates/servicecomb/config.go b/injection/templates/servicecomb/config.go new file mode 100644 index 0000000..119ba5c --- /dev/null +++ b/injection/templates/servicecomb/config.go @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 servicecomb + +// Config of ServiceComb sidecar template +type Config struct { + App App `yaml:"-"` + Mesher Mesher `yaml:"mesher"` + ServiceCenter ServiceCenter `yaml:"serviceCenter"` +} + +// App metadata, need to read from the k8s pod +type App struct { + Name string + Host string + Port int32 + Version string +} + +// Mesher startup configuration +type Mesher struct { + Name string `yaml:"name"` + Image string `yaml:"image"` + Tag string `yaml:"tag"` + GRPCPort int `yaml:"grpcPort"` + HTTPPort int `yaml:"httpPort"` + AdminPort int `yaml:"adminPort"` +} + +// ServiceCenter registry configuration +type ServiceCenter struct { + Name string `yaml:"name"` + Namespace string `yaml:"namespace"` + Address string `yaml:"address"` + TlsConfig *TLSConfig `yaml:"tlsConfig"` +} + +// TLSConfig tls configuration +type TLSConfig struct { + CaFile string `yaml:"caFile"` + CertFile string `yaml:"certFile"` + KeyFile string `yaml:"keyFile"` +} + +// DefaultConfig return default configuration +func DefaultConfig() *Config { + return &Config{ + App: App{}, + Mesher: Mesher{ + Name: "svccomb-mesher", + Image: "servicecomb/mesher-sidecar", + Tag: "latest", + GRPCPort: 40101, + HTTPPort: 30101, + AdminPort: 30102, + }, + ServiceCenter: ServiceCenter{ + Name: "servicecenter", + Namespace: "svccomb-system", + }, + } +} diff --git a/injection/templates/servicecomb/config_test.go b/injection/templates/servicecomb/config_test.go new file mode 100644 index 0000000..58dabbc --- /dev/null +++ b/injection/templates/servicecomb/config_test.go @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 servicecomb + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDefaultConfig(t *testing.T) { + conf := DefaultConfig() + assert.NotNil(t, conf) +} diff --git a/injection/templates/servicecomb/servicecomb.go b/injection/templates/servicecomb/servicecomb.go new file mode 100644 index 0000000..674e0c8 --- /dev/null +++ b/injection/templates/servicecomb/servicecomb.go @@ -0,0 +1,248 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 servicecomb + +import ( + "bytes" + "fmt" + "io/ioutil" + "path/filepath" + "strings" + "sync" + "text/template" + + "github.com/apache/servicecomb-mesher/injection/templates" + "github.com/go-mesh/openlogging" + "gopkg.in/yaml.v2" + corev1 "k8s.io/api/core/v1" +) + +const ( + TemplateName = "servicecomb-mesher" + envVersion = "VERSION" + envListenPortName = "LISTEN_PORT_NAME" + defaultAppVersion = "1.0.0" + defaultListenPortName = "http" +) + +// ServiceComb template +type ServiceComb struct { + mu sync.RWMutex + conf *Config + tmpl *template.Template +} + +func init() { + templates.Register(TemplateName, NewServiceComb) +} + +// NewServiceComb returns ServiceComb Templater +func NewServiceComb(configPath, templatePath string) (templates.Templater, error) { + sc := &ServiceComb{ + conf: DefaultConfig(), + tmpl: DefaultTmpl(), + } + + if configPath != "" { + err := sc.UpdateConfig(configPath) + if err != nil { + return nil, err + } + } + + if templatePath != "" { + err := sc.UpdateTemplate(templatePath) + if err != nil { + return nil, err + } + } + return sc, nil +} + +// UpdateConfig update ServiceComb configuration +func (s *ServiceComb) UpdateConfig(configPath string) error { + conf, err := loadConfig(configPath) + if err != nil { + openlogging.Error(fmt.Sprintf("load sidecar config failed: filename = %s, error = %s", configPath, err.Error())) + return err + } + s.mu.Lock() + defer s.mu.Unlock() + s.conf = s.mergeConfig(conf, s.conf) + return nil +} + +func (s *ServiceComb) mergeConfig(dst, src *Config) *Config { + if dst.Mesher.Name == "" { + dst.Mesher.Name = src.Mesher.Name + } + + if dst.Mesher.Image == "" { + dst.Mesher.Image = src.Mesher.Image + } + + if dst.Mesher.Tag == "" { + dst.Mesher.Tag = src.Mesher.Tag + } + + if dst.Mesher.GRPCPort <= 0 { + dst.Mesher.GRPCPort = src.Mesher.GRPCPort + } + + if dst.Mesher.HTTPPort <= 0 { + dst.Mesher.HTTPPort = src.Mesher.HTTPPort + } + + if dst.Mesher.AdminPort <= 0 { + dst.Mesher.AdminPort = src.Mesher.AdminPort + } + + if dst.ServiceCenter.Name == "" { + dst.ServiceCenter.Name = src.ServiceCenter.Name + } + + if dst.ServiceCenter.Namespace == "" { + dst.ServiceCenter.Namespace = src.ServiceCenter.Namespace + } + + protocol := "http" + if dst.ServiceCenter.TlsConfig != nil { + protocol = "https" + } + + if dst.ServiceCenter.Address == "" { + dst.ServiceCenter.Address = src.ServiceCenter.Address + if dst.ServiceCenter.Address == "" { + dst.ServiceCenter.Address = fmt.Sprintf("%s://%s.%s.svc.cluster.local:30100", + protocol, dst.ServiceCenter.Name, dst.ServiceCenter.Namespace) + } + } + return dst +} + +// UpdateTemplate update ServiceComb template +func (s *ServiceComb) UpdateTemplate(tmplConfig string) error { + tmpl, err := template.ParseFiles(tmplConfig) + if err != nil { + openlogging.Error(fmt.Sprintf("parse sidecar template failed: filename = %s, error = %s", tmplConfig, err.Error())) + return err + } + s.mu.Lock() + defer s.mu.Unlock() + s.tmpl = tmpl + return nil +} + +// PodSpecFromTemplate returns PodSpec by template +func (s *ServiceComb) PodSpecFromTemplate(origin *corev1.Pod) (*corev1.PodSpec, error) { + if len(origin.Spec.Containers) == 0 { + err := fmt.Errorf("can not find containers in origin pod") + openlogging.Error(err.Error()) + return nil, err + } + + s.parseAppFromPod(origin) + + newPod, err := s.podFromTemplate() + if err != nil { + openlogging.Error("get pod from template failed: err = " + err.Error()) + return nil, err + } + + return &newPod.Spec, nil +} + +func (s *ServiceComb) parseAppFromPod(pod *corev1.Pod) { + var port int32 + var version string + name := pod.Name + if name == "" { + name = pod.GenerateName + } + + least := len(name) + for _, container := range pod.Spec.Containers { + if strings.HasPrefix(name, container.Name) { + if least > len(container.Name) { + least = len(container.Name) + name = container.Name + version = getEnv(container.Env, envVersion, defaultAppVersion) + listenPortMatch := getEnv(container.Env, envListenPortName, defaultListenPortName) + port = getPort(container.Ports, listenPortMatch) + } + } + } + + s.conf.App.Name = name + s.conf.App.Host = "127.0.0.1" + s.conf.App.Port = port + s.conf.App.Version = version +} + +func (s *ServiceComb) podFromTemplate() (*corev1.Pod, error) { + buffer := &bytes.Buffer{} + err := s.tmpl.Execute(buffer, s.conf) + if err != nil { + openlogging.Error("execute template failed: err = " + err.Error()) + return nil, err + } + + pod := &corev1.Pod{} + if err := yaml.Unmarshal(buffer.Bytes(), pod); err != nil { + openlogging.Error("unmarshal template failed: err = " + err.Error()) + return nil, err + } + return pod, nil +} + +func loadConfig(configPath string) (*Config, error) { + content, err := ioutil.ReadFile(filepath.Clean(configPath)) + if err != nil { + openlogging.Error(fmt.Sprintf("read config file failed: filename = %s, err = %s", configPath, err.Error())) + return nil, err + } + + conf := &Config{} + err = yaml.Unmarshal(content, conf) + if err != nil { + openlogging.Error(fmt.Sprintf("unmarshal config file failed: filename = %s, err = %s", configPath, err.Error())) + return nil, err + } + return conf, nil +} + +func getPort(ports []corev1.ContainerPort, matchName string) (port int32) { + for _, item := range ports { + if item.Protocol == corev1.ProtocolTCP { + port = item.ContainerPort + if item.Name == matchName { + return + } + } + } + return +} + +func getEnv(envs []corev1.EnvVar, matchName string, noMatched string) string { + for _, env := range envs { + if strings.ToUpper(env.Name) == strings.ToUpper(matchName) { + return env.Value + } + } + return noMatched +} diff --git a/injection/templates/servicecomb/servicecomb_test.go b/injection/templates/servicecomb/servicecomb_test.go new file mode 100644 index 0000000..6cc5032 --- /dev/null +++ b/injection/templates/servicecomb/servicecomb_test.go @@ -0,0 +1,144 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 servicecomb + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" +) + +var ( + testConfigFile = "injectorconfig.yaml" + testTemplateFile = "injectortempalet.yaml" + + configContent = []byte(`serviceCenter: + tlsConfig: + certFile: aaa`) + + templateContent = []byte(`apiVersion: v1 +kind: Pod +spec: + containers: + - env: + - name: http_proxy + value: http://127.0.0.1:{{.AMesher.HTTPPort}} + name: {{.App.AName}}`) +) + +func init() { + os.Remove(testConfigFile) + os.Remove(testTemplateFile) +} + +func TestNewServiceComb(t *testing.T) { + t.Log("========New ServiceComb Template no configPath and no templatePath=") + tmpl, err := NewServiceComb("", "") + assert.Nil(t, err) + assert.NotNil(t, tmpl) + + t.Log("========New ServiceComb Template wrong configPath and no templatePath=") + tmpl, err = NewServiceComb(testConfigFile, "") + assert.NotNil(t, err) + + t.Log("========New ServiceComb Template no configPath and wrong templatePath=") + tmpl, err = NewServiceComb("", testTemplateFile) + assert.NotNil(t, err) +} + +func TestServiceComb_UpdateConfig(t *testing.T) { + t.Log("========ServiceComb Template UpdateConfig with wrong configPath=") + tmpl, err := NewServiceComb("", "") + assert.Nil(t, err) + + err = tmpl.UpdateConfig(testConfigFile) + assert.NotNil(t, err) + + t.Log("========ServiceComb Template UpdateConfig with configPath=") + err = ioutil.WriteFile(testConfigFile, configContent, 0640) + assert.Nil(t, err) + defer os.Remove(testConfigFile) + + err = tmpl.UpdateConfig(testConfigFile) + assert.Nil(t, err) + +} + +func TestServiceComb_UpdateTemplate(t *testing.T) { + t.Log("========ServiceComb Template UpdateTemplate with wrong templatePath=") + tmpl, err := NewServiceComb("", "") + assert.Nil(t, err) + + err = tmpl.UpdateTemplate(testTemplateFile) + assert.NotNil(t, err) + + t.Log("========ServiceComb Template UpdateTemplate with templatePath=") + err = ioutil.WriteFile(testTemplateFile, templateContent, 0640) + assert.Nil(t, err) + defer os.Remove(testTemplateFile) + + err = tmpl.UpdateTemplate(testTemplateFile) + assert.Nil(t, err) +} + +func TestServiceComb_PodSpecFromTemplate(t *testing.T) { + tmpl, err := NewServiceComb("", "") + assert.Nil(t, err) + + t.Log("========ServiceComb Template PodSpecFromTemplate with empty pod=") + pod := &corev1.Pod{} + _, err = tmpl.PodSpecFromTemplate(pod) + assert.NotNil(t, err) + + t.Log("========ServiceComb Template PodSpecFromTemplate with pod=") + pod.GenerateName = "testPod-1" + pod.Spec.Containers = append(pod.Spec.Containers, corev1.Container{ + Name: "testPod", + Ports: []corev1.ContainerPort{}, + Env: []corev1.EnvVar{ + { + Name: "VERSION", + Value: "1.0.1", + }, + }, + }) + _, err = tmpl.PodSpecFromTemplate(pod) + assert.Nil(t, err) + + pod.Spec.Containers[0].Ports = append(pod.Spec.Containers[0].Ports, corev1.ContainerPort{ + Protocol: corev1.ProtocolTCP, + ContainerPort: 8080, + Name: "http", + }) + _, err = tmpl.PodSpecFromTemplate(pod) + assert.Nil(t, err) + + t.Log("========ServiceComb Template PodSpecFromTemplate with wrong template=") + err = ioutil.WriteFile(testTemplateFile, templateContent, 0640) + assert.Nil(t, err) + defer os.Remove(testTemplateFile) + + err = tmpl.UpdateTemplate(testTemplateFile) + assert.Nil(t, err) + + _, err = tmpl.PodSpecFromTemplate(pod) + assert.NotNil(t, err) +} diff --git a/injection/templates/servicecomb/template.go b/injection/templates/servicecomb/template.go new file mode 100644 index 0000000..7508c6a --- /dev/null +++ b/injection/templates/servicecomb/template.go @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 servicecomb + +import ( + "text/template" + + "github.com/go-mesh/openlogging" +) + +var sidecarContainer = `apiVersion: v1 +kind: Pod +spec: + containers: + - env: + - name: http_proxy + value: http://127.0.0.1:{{.Mesher.HTTPPort}} + name: {{.App.Name}} + - env: + - name: SPECIFIC_ADDR + value: 127.0.0.1:{{.App.Port}} + - name: SERVICE_NAME + value: {{.App.Name}} + - name: VERSION + value: {{.App.Version}} + - name: CSE_REGISTRY_ADDR + value: {{.ServiceCenter.Address}} + image: {{.Mesher.Image}}:{{.Mesher.Tag}} + imagePullPolicy: IfNotPresent + name: {{.Mesher.Name}} + ports: + - containerport: {{.Mesher.GRPCPort}} + name: grpc + protocol: TCP + - containerport: {{.Mesher.HTTPPort}} + name: http + protocol: TCP + - containerport: {{.Mesher.AdminPort}} + name: rest-admin + protocol: TCP` + +// DefaultTmpl returns default template +func DefaultTmpl() *template.Template { + tmpl, err := template.New("sidecar").Parse(sidecarContainer) + if err != nil { + openlogging.Error("get default template failed: " + err.Error()) + } + return tmpl +} diff --git a/injection/templates/servicecomb/template_test.go b/injection/templates/servicecomb/template_test.go new file mode 100644 index 0000000..29ac40e --- /dev/null +++ b/injection/templates/servicecomb/template_test.go @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 servicecomb + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDefaultTmpl(t *testing.T) { + tmpl := DefaultTmpl() + assert.NotNil(t, tmpl) +} diff --git a/injection/templates/templates.go b/injection/templates/templates.go new file mode 100644 index 0000000..1927983 --- /dev/null +++ b/injection/templates/templates.go @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 templates + +import ( + "fmt" + + "github.com/go-mesh/openlogging" + corev1 "k8s.io/api/core/v1" +) + +var templateMap = map[string]func(configPath, templatePath string) (Templater, error){} + +// Templater sidecar's configured template parser +type Templater interface { + // PodSpecFromTemplate get PodSpec from template + PodSpecFromTemplate(*corev1.Pod) (*corev1.PodSpec, error) + + // UpdateConfig update sidecar config + UpdateConfig(configPath string) error + + // UpdateTemplate update sidecar template + UpdateTemplate(tmplConfig string) error +} + +// Register templater to manage +func Register(name string, fn func(configPath, templatePath string) (Templater, error)) { + _, ok := templateMap[name] + if ok { + openlogging.Warn(fmt.Sprintf("sidecar already exists, name = %s", name)) + return + } + templateMap[name] = fn +} + +// NewTemplater templater by name +func NewTemplater(name, configPath, templatePath string) (Templater, error) { + fn, ok := templateMap[name] + if !ok { + err := fmt.Errorf("sidecar not found, , name = %s", name) + openlogging.Error(err.Error()) + return nil, err + } + return fn(configPath, templatePath) +} diff --git a/injection/templates/templates_test.go b/injection/templates/templates_test.go new file mode 100644 index 0000000..273de16 --- /dev/null +++ b/injection/templates/templates_test.go @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 templates + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func newTestTemplate(configPath, templatePath string) (Templater, error) { + return nil, nil +} + +func TestNewTemplater(t *testing.T) { + name := "testTemplate" + _, err := NewTemplater(name, "", "") + assert.NotNil(t, err) + + Register(name, newTestTemplate) + _, err = NewTemplater(name, "", "") + assert.Nil(t, err) + + Register(name, newTestTemplate) +} diff --git a/injection/webhook/inject.go b/injection/webhook/inject.go new file mode 100644 index 0000000..c7e6194 --- /dev/null +++ b/injection/webhook/inject.go @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 webhook + +import ( + "encoding/json" + "fmt" + + "github.com/go-mesh/openlogging" + "k8s.io/api/admission/v1beta1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func (wh *Webhook) inject(review *v1beta1.AdmissionReview) *v1beta1.AdmissionResponse { + reqBytes := review.Request.Object.Raw + pod := &corev1.Pod{} + if err := json.Unmarshal(reqBytes, pod); err != nil { + openlogging.Error(fmt.Sprintf("json marshal pod: err = %s, bytes = %s", err.Error(), string(reqBytes))) + return responseFromErrorMessage(err) + } + + tmplSpec, err := wh.templater.PodSpecFromTemplate(pod) + if err != nil { + openlogging.Error("get pod spec from template failed, error = " + err.Error()) + return responseFromErrorMessage(err) + } + + tmplSpec.Containers = addK8sServiceAccount(pod.Spec.Containers, tmplSpec.Containers) + + pathBytes, err := createJSONPatch(&pod.Spec, tmplSpec) + if err != nil { + openlogging.Error("create patch failed: " + err.Error()) + return responseFromErrorMessage(err) + } + + return &v1beta1.AdmissionResponse{ + Allowed: true, + Patch: pathBytes, + PatchType: func() *v1beta1.PatchType { + pt := v1beta1.PatchTypeJSONPatch + return &pt + }(), + } +} + +func addK8sServiceAccount(dst, src []corev1.Container) []corev1.Container { + var serviceAccount *corev1.VolumeMount + for _, dc := range dst { + serviceAccount = getServiceAccount(dc.VolumeMounts) + if serviceAccount != nil { + break + } + } + + for index, sc := range src { + if matchVolumeMountByName(sc.VolumeMounts, serviceAccount.Name) { + continue + } + src[index].VolumeMounts = append(src[index].VolumeMounts, *serviceAccount) + } + return src +} + +func getServiceAccount(volumeMounts []corev1.VolumeMount) *corev1.VolumeMount { + for _, vMount := range volumeMounts { + if vMount.MountPath == "/var/run/secrets/kubernetes.io/serviceaccount" { + return &vMount + } + } + return nil +} + +func matchVolumeMountByName(volumeMounts []corev1.VolumeMount, name string) bool { + for _, vMount := range volumeMounts { + if vMount.Name == name { + return true + } + } + return false +} + +func responseFromErrorMessage(err error) *v1beta1.AdmissionResponse { + return &v1beta1.AdmissionResponse{Result: &metav1.Status{Message: err.Error()}} +} diff --git a/injection/webhook/options.go b/injection/webhook/options.go new file mode 100644 index 0000000..b89826c --- /dev/null +++ b/injection/webhook/options.go @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 webhook + +import ( + "crypto/tls" + "sync" +) + +// ConfigOption Config option +type ConfigOption func(*config) + +// WithListenPort listen port to ConfigOption +func WithListenPort(port int) ConfigOption { + return func(c *config) { c.port = port } +} + +// WithCertFile tls cert file to ConfigOption +func WithCertFile(certFile string) ConfigOption { + return func(c *config) { c.certFile = certFile } +} + +// WithKeyFile tls key file to ConfigOption +func WithKeyFile(keyFile string) ConfigOption { + return func(c *config) { c.keyFile = keyFile } +} + +// WithSidecarConfig sidecar config to ConfigOption +func WithSidecarConfig(configPath string) ConfigOption { + return func(c *config) { c.sidecarConfig = configPath } +} + +// WithTemplateName template name to ConfigOption +func WithTemplateName(templateName string) ConfigOption { + return func(c *config) { c.templateName = templateName } +} + +// WithSidecarTemplate sidecar template to ConfigOption +func WithSidecarTemplate(templatePath string) ConfigOption { + return func(c *config) { c.sidecarTemplate = templatePath } +} + +func toConfig(opts ...ConfigOption) *config { + conf := &config{} + for _, opt := range opts { + opt(conf) + } + return conf +} + +type config struct { + port int + certFile string + keyFile string + sidecarConfig string + sidecarTemplate string + templateName string + + mu sync.RWMutex + cert *tls.Certificate +} + +func (c *config) loadTLSConfigFiles() error { + pair, err := tls.LoadX509KeyPair(c.certFile, c.keyFile) + if err != nil { + return err + } + c.mu.Lock() + defer c.mu.Unlock() + c.cert = &pair + return nil +} + +func (c *config) getCertificate(*tls.ClientHelloInfo) (*tls.Certificate, error) { + c.mu.RLock() + defer c.mu.RUnlock() + return c.cert, nil +} diff --git a/injection/webhook/options_test.go b/injection/webhook/options_test.go new file mode 100644 index 0000000..49c98fc --- /dev/null +++ b/injection/webhook/options_test.go @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 webhook + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestOptions(t *testing.T) { + opts := []ConfigOption{ + WithListenPort(808), + WithCertFile(""), + WithKeyFile(""), + WithSidecarConfig(""), + WithTemplateName(""), + WithSidecarTemplate(""), + } + + conf := toConfig(opts...) + assert.NotNil(t, conf) + + _, err := conf.getCertificate(nil) + assert.Nil(t, err) + + err = conf.loadTLSConfigFiles() + assert.NotNil(t, err) +} diff --git a/injection/webhook/patch.go b/injection/webhook/patch.go new file mode 100644 index 0000000..194f642 --- /dev/null +++ b/injection/webhook/patch.go @@ -0,0 +1,130 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 webhook + +import ( + "encoding/json" + "fmt" + "reflect" + + corev1 "k8s.io/api/core/v1" +) + +type operation string + +const ( + OperationAdd operation = "add" + OperationReplace operation = "replace" + OperationRemove operation = "remove" +) + +type jsonPatch struct { + Op operation `json:"op"` + Path string `json:"path"` + Value interface{} `json:"value,omitempty"` +} + +func addPatch(src interface{}, basePath string) (patch []jsonPatch) { + return append(patch, jsonPatch{Op: OperationAdd, Path: basePath, Value: src}) +} + +func removePatch(basePath string) (patch []jsonPatch) { + return append(patch, jsonPatch{Op: OperationRemove, Path: basePath}) +} + +func createJSONPatch(dst *corev1.PodSpec, src *corev1.PodSpec) ([]byte, error) { + patch := make([]jsonPatch, 0, 10) + patch = append(patch, containersToPatch(dst.InitContainers, src.InitContainers, "Name", "/spec/initContainers")...) + patch = append(patch, containersToPatch(dst.Containers, src.Containers, "Name", "/spec/containers")...) + patch = append(patch, sliceToPatch(dst.Volumes, src.Volumes, "Name", "/spec/volumes")...) + patch = append(patch, sliceToPatch(dst.ImagePullSecrets, src.ImagePullSecrets, "Name", "/spec/imagePullSecrets")...) + + if src.DNSConfig != nil { + patch = append(patch, addPatch(src.DNSConfig, "/spec/dnsConfig")...) + } + + if dst.SecurityContext != nil { + patch = append(patch, addPatch(dst.SecurityContext, "/spec/securityContext")...) + } + + return json.Marshal(patch) +} + +func containersToPatch(dst []corev1.Container, src []corev1.Container, field string, basePath string) (patch []jsonPatch) { + patch = append(patch, sliceToPatch(dst, src, field, basePath)...) + for i, dic := range dst { + for _, sic := range src { + if dic.Name == sic.Name { + patch = append(patch, sliceToPatch(dic.Env, sic.Env, "Name", fmt.Sprintf("%v/%v/%v", basePath, i, "env"))...) + patch = append(patch, sliceToPatch(dic.VolumeMounts, sic.VolumeMounts, "Name", fmt.Sprintf("%v/%v/%v", basePath, i, "volumeMounts"))...) + } + } + } + return +} + +func sliceToPatch(dst interface{}, src interface{}, field string, basePath string) (patch []jsonPatch) { + dVal := reflect.ValueOf(dst) + sVal := reflect.ValueOf(src) + if dVal.Kind() != sVal.Kind() || dVal.IsNil() && sVal.IsNil() { + return + } + return addSlicePatch(dVal, sVal, field, basePath) +} + +func removeSlicePatch(dst reflect.Value, src reflect.Value, field string, basePath string) (patch []jsonPatch) { + for i := dst.Len() - 1; i >= 0; i-- { + dv := dst.Index(i) + if matchSliceByField(src, dv, field) { + continue + } + patch = append(patch, removePatch(fmt.Sprintf("%v/%v", basePath, i))...) + } + return patch +} + +func addSlicePatch(dst reflect.Value, src reflect.Value, field string, basePath string) (patch []jsonPatch) { + first := dst.Len() == 0 + for i := 0; i < src.Len(); i++ { + sv := src.Index(i) + if matchSliceByField(dst, sv, field) { + continue + } + + value := sv.Interface() + path := basePath + if first { + first = false + value = []interface{}{value} + } else { + path += "/-" + } + patch = append(patch, addPatch(value, path)...) + } + return patch +} + +func matchSliceByField(set reflect.Value, match reflect.Value, field string) bool { + for i := 0; i < set.Len(); i++ { + item := set.Index(i) + if item.FieldByName(field).Interface() == match.FieldByName(field).Interface() { + return true + } + } + return false +} diff --git a/injection/webhook/webhook.go b/injection/webhook/webhook.go new file mode 100644 index 0000000..9f5c08b --- /dev/null +++ b/injection/webhook/webhook.go @@ -0,0 +1,127 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 webhook + +import ( + "crypto/tls" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + + "github.com/apache/servicecomb-mesher/injection/templates" + "github.com/go-mesh/openlogging" + "k8s.io/api/admission/v1beta1" +) + +// Webhook struct +type Webhook struct { + server *http.Server + config *config + templater templates.Templater +} + +// NewWebhook new webhook server +func NewWebhook(ops ...ConfigOption) (*Webhook, error) { + conf := toConfig(ops...) + templater, err := templates.NewTemplater(conf.templateName, conf.sidecarConfig, conf.sidecarTemplate) + if err != nil { + openlogging.Error(fmt.Sprintf("new templater failed: error = %v, template = %s", err.Error(), conf.templateName)) + return nil, err + } + + return &Webhook{ + server: &http.Server{ + Addr: fmt.Sprintf(":%d", conf.port), + TLSConfig: &tls.Config{GetCertificate: conf.getCertificate}, + }, + config: conf, + templater: templater, + }, nil +} + +// Run webhook server +func (wh *Webhook) Run() error { + err := wh.config.loadTLSConfigFiles() + if err != nil { + openlogging.Error("load tls configure files failed: " + err.Error()) + return err + } + + http.HandleFunc("/v1/mesher/inject", wh.injectHandler) + + if err := wh.server.ListenAndServeTLS("", ""); err != nil { + openlogging.Error("Webhook ListenAndServeTLS failed: " + err.Error()) + return err + } + return nil +} + +func (wh *Webhook) injectHandler(rw http.ResponseWriter, r *http.Request) { + // Webhooks are sent a POST request, with Content-Type: application/json + if r.Method != http.MethodPost { + openlogging.Error("request method not allowed: " + r.Method) + http.Error(rw, "request method not allowed", http.StatusMethodNotAllowed) + return + } + + if r.Header.Get("Content-Type") != "application/json" { + http.Error(rw, "invalid Content-Type, want `application/json`", http.StatusUnsupportedMediaType) + return + } + + body, err := ioutil.ReadAll(r.Body) + if err != nil || len(body) == 0 { + openlogging.Error("request body is not found") + http.Error(rw, "request body is not found", http.StatusBadRequest) + return + } + + var response *v1beta1.AdmissionResponse + + reviewReq := &v1beta1.AdmissionReview{} + err = json.Unmarshal(body, reviewReq) + if err != nil { + openlogging.Error(fmt.Sprintf("json unmarshal review request: error = %v, bytes = %s", err, string(body))) + http.Error(rw, fmt.Sprintf("json marshal review response failed: %s", err), http.StatusBadRequest) + return + } else { + response = wh.inject(reviewReq) + } + + reviewResp := v1beta1.AdmissionReview{} + if response != nil { + reviewResp.Response = response + if reviewReq.Request != nil { + reviewResp.Response.UID = reviewReq.Request.UID + } + } + + resp, err := json.Marshal(reviewResp) + if err != nil { + openlogging.Error("json marshal review response failed: " + err.Error()) + http.Error(rw, fmt.Sprintf("json marshal review response failed: %s", err), http.StatusBadRequest) + return + } + + _, err = rw.Write(resp) + if err != nil { + openlogging.Error("write response failed: " + err.Error()) + http.Error(rw, fmt.Sprintf("write response failed: %s", err), http.StatusInternalServerError) + } +} diff --git a/injection/webhook/webhook_test.go b/injection/webhook/webhook_test.go new file mode 100644 index 0000000..e27a238 --- /dev/null +++ b/injection/webhook/webhook_test.go @@ -0,0 +1,238 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 webhook + +import ( + "bytes" + "net/http" + "net/http/httptest" + "testing" + + "github.com/apache/servicecomb-mesher/injection/templates" + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v2" + corev1 "k8s.io/api/core/v1" +) + +var ( + mockTemplateName = "test-mock" +) + +type mockTemplate struct{} + +func init() { + templates.Register(mockTemplateName, NewMockTemplate) +} + +func NewMockTemplate(configPath, templatePath string) (templates.Templater, error) { + return &mockTemplate{}, nil +} + +func (t *mockTemplate) PodSpecFromTemplate(*corev1.Pod) (*corev1.PodSpec, error) { + pod := &corev1.Pod{} + err := yaml.Unmarshal(getTmplPodBytes(), pod) + if err != nil { + return nil, err + } + return &(pod.Spec), nil +} + +func (t *mockTemplate) UpdateConfig(configPath string) error { + return nil +} + +func (t *mockTemplate) UpdateTemplate(tmplConfig string) error { + return nil +} + +func TestNewWebhook(t *testing.T) { + t.Log("========Webhook Mock Template=") + _, err := NewWebhook(WithTemplateName(mockTemplateName)) + assert.Nil(t, err) + + t.Log("========Webhook Template Notfound=") + _, err = NewWebhook(WithTemplateName("test-notfound")) + assert.NotNil(t, err) +} + +func TestInjectHandler(t *testing.T) { + t.Log("========Webhook InjectHandler Method Not Allowed=") + wh, err := NewWebhook(WithTemplateName(mockTemplateName)) + assert.Nil(t, err) + svr := httptest.NewServer(http.HandlerFunc(wh.injectHandler)) + defer svr.Close() + + resp, err := http.Get(svr.URL) + assert.Nil(t, err) + assert.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode) + + t.Log("========Webhook InjectHandler Unsupported Media Type=") + resp, err = http.Post(svr.URL, "", nil) + assert.Nil(t, err) + assert.Equal(t, http.StatusUnsupportedMediaType, resp.StatusCode) + + t.Log("========Webhook InjectHandler Body is nil=") + resp, err = http.Post(svr.URL, "application/json", nil) + assert.Nil(t, err) + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + + t.Log("========Webhook InjectHandler Body is wrong=") + resp, err = http.Post(svr.URL, "application/json", bytes.NewBuffer([]byte("abc"))) + assert.Nil(t, err) + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + + t.Log("========Webhook InjectHandler Success=") + resp, err = http.Post(svr.URL, "application/json", bytes.NewBuffer(getMockBytes())) + assert.Nil(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) +} + +func getMockBytes() []byte { + return []byte(`{ + "kind": "AdmissionReview", + "apiVersion": "admission.k8s.io/v1beta1", + "request": { + "uid": "1caf247a-2076-11ea-bcbc-fa163eca30e0", + "kind": { + "group": "", + "version": "v1", + "kind": "Pod" + }, + "resource": { + "group": "", + "version": "v1", + "resource": "pods" + }, + "namespace": "svccomb-test", + "operation": "CREATE", + "userInfo": { + "username": "system:serviceaccount:kube-system:replicaset-controller", + "uid": "57c75718-fa18-11e9-8271-fa163eca30e0", + "groups": ["system:serviceaccounts", "system:serviceaccounts:kube-system", "system:authenticated"] + }, + "object": { + "metadata": { + "generateName": "calculator-python-8dd449c6b-", + "creationTimestamp": null, + "labels": { + "app": "calculator", + "pod-template-hash": "8dd449c6b" + }, + "ownerReferences": [{ + "apiVersion": "apps/v1", + "kind": "ReplicaSet", + "name": "calculator-python-8dd449c6b", + "uid": "1cac4646-2076-11ea-bcbc-fa163eca30e0", + "controller": true, + "blockOwnerDeletion": true + }] + }, + "spec": { + "volumes": [{ + "name": "default-token-9pvt7", + "secret": { + "secretName": "default-token-9pvt7" + } + }], + "containers": [{ + "name": "calculator", + "image": "servicecomb/calculator-python:latest", + "ports": [{ + "containerPort": 4540, + "protocol": "TCP" + }], + "resources": {}, + "volumeMounts": [{ + "name": "default-token-9pvt7", + "readOnly": true, + "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount" + }], + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "imagePullPolicy": "IfNotPresent" + }], + "restartPolicy": "Always", + "terminationGracePeriodSeconds": 30, + "dnsPolicy": "ClusterFirst", + "serviceAccountName": "default", + "serviceAccount": "default", + "securityContext": {}, + "schedulerName": "default-scheduler", + "tolerations": [{ + "key": "node.kubernetes.io/not-ready", + "operator": "Exists", + "effect": "NoExecute", + "tolerationSeconds": 300 + }, { + "key": "node.kubernetes.io/unreachable", + "operator": "Exists", + "effect": "NoExecute", + "tolerationSeconds": 300 + }], + "priority": 0, + "enableServiceLinks": true + }, + "status": {} + }, + "oldObject": null, + "dryRun": false + } +}`) +} + +func getTmplPodBytes() []byte { + return []byte(`apiVersion: v1 +kind: Pod +spec: + containers: + - env: + - name: http_proxy + value: http://127.0.0.1:30101 + name: calculator + resources: {} + volumeMounts: + - mountPath: /var/run/secrets/kubernetes.io/serviceaccount + name: default-token-9pvt7 + readOnly: true + - env: + - name: SPECIFIC_ADDR + value: 127.0.0.1:4540 + - name: SERVICE_NAME + value: calculator + - name: VERSION + value: 1.0.0 + - name: CSE_REGISTRY_ADDR + value: http://servicecenter.servicecomb.svc.cluster.local:30100 + image: servicecomb/mesher-sidecar:1.6.3 + name: svccomb-mesher + ports: + - containerPort: 40101 + name: grpc + protocol: TCP + - containerPort: 30101 + name: http + protocol: TCP + - containerPort: 30102 + name: rest-admin + protocol: TCP + resources: {} + volumeMounts: + - mountPath: /var/run/secrets/kubernetes.io/serviceaccount + name: default-token-9pvt7 + readOnly: true`) +}