diff --git a/chart/templates/controller.yaml b/chart/templates/controller.yaml index 5fd6ff335f1c1..7360538fac7c9 100644 --- a/chart/templates/controller.yaml +++ b/chart/templates/controller.yaml @@ -24,6 +24,9 @@ rules: - apiGroups: [""] resources: ["pods", "endpoints", "services", "replicationcontrollers", "namespaces"] verbs: ["list", "get", "watch"] +- apiGroups: ["admissionregistration.k8s.io"] + resources: ["validatingwebhookconfigurations"] + verbs: ["create", "get", "delete"] - apiGroups: ["linkerd.io"] resources: ["serviceprofiles"] verbs: ["list", "get", "watch"] @@ -77,6 +80,24 @@ spec: port: 8086 targetPort: 8086 --- +kind: Service +apiVersion: v1 +metadata: + name: linkerd-sp-validator + namespace: {{.Namespace}} + labels: + {{.ControllerComponentLabel}}: controller + annotations: + {{.CreatedByAnnotation}}: {{.CliVersion}} +spec: + type: ClusterIP + selector: + {{.ControllerComponentLabel}}: controller + ports: + - name: sp-validator + port: 443 + targetPort: sp-validator +--- kind: Deployment apiVersion: extensions/v1beta1 metadata: @@ -187,6 +208,31 @@ spec: {{ end -}} securityContext: runAsUser: {{.ControllerUID}} + - name: sp-validator + image: {{.ControllerImage}} + imagePullPolicy: {{.ImagePullPolicy}} + args: + - "sp-validator" + - "-controller-namespace={{.Namespace}}" + - "-log-level={{.ControllerLogLevel}}" + ports: + - name: sp-validator + containerPort: 8443 + livenessProbe: + httpGet: + path: /ping + port: 9997 + initialDelaySeconds: 10 + readinessProbe: + httpGet: + path: /ready + port: 9997 + failureThreshold: 7 + {{ with .SPValidatorResources -}} + {{- template "resources" . }} + {{ end -}} + securityContext: + runAsUser: {{.ControllerUID}} volumes: - name: config configMap: diff --git a/chart/templates/proxy_injector.yaml b/chart/templates/proxy_injector.yaml index 2c401d8e0f609..47e2c5e8910dd 100644 --- a/chart/templates/proxy_injector.yaml +++ b/chart/templates/proxy_injector.yaml @@ -35,7 +35,6 @@ spec: - "proxy-injector" - "-controller-namespace={{.Namespace}}" - "-log-level={{.ControllerLogLevel}}" - - "-no-init-container={{.NoInitContainer}}" ports: - name: proxy-injector containerPort: 8443 diff --git a/cli/cmd/install.go b/cli/cmd/install.go index 3a2189aaabc4d..54b75f763365d 100644 --- a/cli/cmd/install.go +++ b/cli/cmd/install.go @@ -62,6 +62,7 @@ type ( IdentityResources, PrometheusResources, ProxyInjectorResources, + SPValidatorResources, PublicAPIResources, TapResources, WebResources *resources @@ -431,6 +432,7 @@ func (options *installOptions) buildValuesWithoutIdentity(configs *pb.All) (*ins values.DestinationResources = &*defaultConstraints values.GrafanaResources = &*defaultConstraints values.ProxyInjectorResources = &*defaultConstraints + values.SPValidatorResources = &*defaultConstraints values.PublicAPIResources = &*defaultConstraints values.TapResources = &*defaultConstraints values.WebResources = &*defaultConstraints diff --git a/cli/cmd/testdata/install_default.golden b/cli/cmd/testdata/install_default.golden index b00ea262305b6..3f398e847c06f 100644 --- a/cli/cmd/testdata/install_default.golden +++ b/cli/cmd/testdata/install_default.golden @@ -283,6 +283,9 @@ rules: - apiGroups: [""] resources: ["pods", "endpoints", "services", "replicationcontrollers", "namespaces"] verbs: ["list", "get", "watch"] +- apiGroups: ["admissionregistration.k8s.io"] + resources: ["validatingwebhookconfigurations"] + verbs: ["create", "get", "delete"] - apiGroups: ["linkerd.io"] resources: ["serviceprofiles"] verbs: ["list", "get", "watch"] @@ -336,6 +339,24 @@ spec: port: 8086 targetPort: 8086 --- +kind: Service +apiVersion: v1 +metadata: + name: linkerd-sp-validator + namespace: linkerd + labels: + linkerd.io/control-plane-component: controller + annotations: + linkerd.io/created-by: linkerd/cli dev-undefined +spec: + type: ClusterIP + selector: + linkerd.io/control-plane-component: controller + ports: + - name: sp-validator + port: 443 + targetPort: sp-validator +--- apiVersion: extensions/v1beta1 kind: Deployment metadata: @@ -446,6 +467,29 @@ spec: resources: {} securityContext: runAsUser: 2103 + - args: + - sp-validator + - -controller-namespace=linkerd + - -log-level=info + image: gcr.io/linkerd-io/controller:dev-undefined + imagePullPolicy: IfNotPresent + livenessProbe: + httpGet: + path: /ping + port: 9997 + initialDelaySeconds: 10 + name: sp-validator + ports: + - containerPort: 8443 + name: sp-validator + readinessProbe: + failureThreshold: 7 + httpGet: + path: /ready + port: 9997 + resources: {} + securityContext: + runAsUser: 2103 - env: - name: LINKERD2_PROXY_LOG value: warn,linkerd2_proxy=info diff --git a/cli/cmd/testdata/install_ha_output.golden b/cli/cmd/testdata/install_ha_output.golden index de3b7cbb58de1..88bebecb16f79 100644 --- a/cli/cmd/testdata/install_ha_output.golden +++ b/cli/cmd/testdata/install_ha_output.golden @@ -289,6 +289,9 @@ rules: - apiGroups: [""] resources: ["pods", "endpoints", "services", "replicationcontrollers", "namespaces"] verbs: ["list", "get", "watch"] +- apiGroups: ["admissionregistration.k8s.io"] + resources: ["validatingwebhookconfigurations"] + verbs: ["create", "get", "delete"] - apiGroups: ["linkerd.io"] resources: ["serviceprofiles"] verbs: ["list", "get", "watch"] @@ -342,6 +345,24 @@ spec: port: 8086 targetPort: 8086 --- +kind: Service +apiVersion: v1 +metadata: + name: linkerd-sp-validator + namespace: linkerd + labels: + linkerd.io/control-plane-component: controller + annotations: + linkerd.io/created-by: linkerd/cli dev-undefined +spec: + type: ClusterIP + selector: + linkerd.io/control-plane-component: controller + ports: + - name: sp-validator + port: 443 + targetPort: sp-validator +--- apiVersion: extensions/v1beta1 kind: Deployment metadata: @@ -461,6 +482,32 @@ spec: memory: 50Mi securityContext: runAsUser: 2103 + - args: + - sp-validator + - -controller-namespace=linkerd + - -log-level=info + image: gcr.io/linkerd-io/controller:dev-undefined + imagePullPolicy: IfNotPresent + livenessProbe: + httpGet: + path: /ping + port: 9997 + initialDelaySeconds: 10 + name: sp-validator + ports: + - containerPort: 8443 + name: sp-validator + readinessProbe: + failureThreshold: 7 + httpGet: + path: /ready + port: 9997 + resources: + requests: + cpu: 20m + memory: 50Mi + securityContext: + runAsUser: 2103 - env: - name: LINKERD2_PROXY_LOG value: warn,linkerd2_proxy=info diff --git a/cli/cmd/testdata/install_ha_with_overrides_output.golden b/cli/cmd/testdata/install_ha_with_overrides_output.golden index 4a0739815f7e8..5f1a1f8acf75a 100644 --- a/cli/cmd/testdata/install_ha_with_overrides_output.golden +++ b/cli/cmd/testdata/install_ha_with_overrides_output.golden @@ -289,6 +289,9 @@ rules: - apiGroups: [""] resources: ["pods", "endpoints", "services", "replicationcontrollers", "namespaces"] verbs: ["list", "get", "watch"] +- apiGroups: ["admissionregistration.k8s.io"] + resources: ["validatingwebhookconfigurations"] + verbs: ["create", "get", "delete"] - apiGroups: ["linkerd.io"] resources: ["serviceprofiles"] verbs: ["list", "get", "watch"] @@ -342,6 +345,24 @@ spec: port: 8086 targetPort: 8086 --- +kind: Service +apiVersion: v1 +metadata: + name: linkerd-sp-validator + namespace: linkerd + labels: + linkerd.io/control-plane-component: controller + annotations: + linkerd.io/created-by: linkerd/cli dev-undefined +spec: + type: ClusterIP + selector: + linkerd.io/control-plane-component: controller + ports: + - name: sp-validator + port: 443 + targetPort: sp-validator +--- apiVersion: extensions/v1beta1 kind: Deployment metadata: @@ -461,6 +482,32 @@ spec: memory: 50Mi securityContext: runAsUser: 2103 + - args: + - sp-validator + - -controller-namespace=linkerd + - -log-level=info + image: gcr.io/linkerd-io/controller:dev-undefined + imagePullPolicy: IfNotPresent + livenessProbe: + httpGet: + path: /ping + port: 9997 + initialDelaySeconds: 10 + name: sp-validator + ports: + - containerPort: 8443 + name: sp-validator + readinessProbe: + failureThreshold: 7 + httpGet: + path: /ready + port: 9997 + resources: + requests: + cpu: 20m + memory: 50Mi + securityContext: + runAsUser: 2103 - env: - name: LINKERD2_PROXY_LOG value: warn,linkerd2_proxy=info diff --git a/cli/cmd/testdata/install_no_init_container.golden b/cli/cmd/testdata/install_no_init_container.golden index 14006ce93ca07..e652a33cde9dc 100644 --- a/cli/cmd/testdata/install_no_init_container.golden +++ b/cli/cmd/testdata/install_no_init_container.golden @@ -259,6 +259,9 @@ rules: - apiGroups: [""] resources: ["pods", "endpoints", "services", "replicationcontrollers", "namespaces"] verbs: ["list", "get", "watch"] +- apiGroups: ["admissionregistration.k8s.io"] + resources: ["validatingwebhookconfigurations"] + verbs: ["create", "get", "delete"] - apiGroups: ["linkerd.io"] resources: ["serviceprofiles"] verbs: ["list", "get", "watch"] @@ -312,6 +315,24 @@ spec: port: 8086 targetPort: 8086 --- +kind: Service +apiVersion: v1 +metadata: + name: linkerd-sp-validator + namespace: linkerd + labels: + linkerd.io/control-plane-component: controller + annotations: + linkerd.io/created-by: linkerd/cli dev-undefined +spec: + type: ClusterIP + selector: + linkerd.io/control-plane-component: controller + ports: + - name: sp-validator + port: 443 + targetPort: sp-validator +--- apiVersion: extensions/v1beta1 kind: Deployment metadata: @@ -422,6 +443,29 @@ spec: resources: {} securityContext: runAsUser: 2103 + - args: + - sp-validator + - -controller-namespace=linkerd + - -log-level=info + image: gcr.io/linkerd-io/controller:dev-undefined + imagePullPolicy: IfNotPresent + livenessProbe: + httpGet: + path: /ping + port: 9997 + initialDelaySeconds: 10 + name: sp-validator + ports: + - containerPort: 8443 + name: sp-validator + readinessProbe: + failureThreshold: 7 + httpGet: + path: /ready + port: 9997 + resources: {} + securityContext: + runAsUser: 2103 - env: - name: LINKERD2_PROXY_LOG value: warn,linkerd2_proxy=info diff --git a/cli/cmd/testdata/install_no_init_container_auto_inject.golden b/cli/cmd/testdata/install_no_init_container_auto_inject.golden index be0272faccf86..934cbecd93e03 100644 --- a/cli/cmd/testdata/install_no_init_container_auto_inject.golden +++ b/cli/cmd/testdata/install_no_init_container_auto_inject.golden @@ -261,6 +261,9 @@ rules: - apiGroups: [""] resources: ["pods", "endpoints", "services", "replicationcontrollers", "namespaces"] verbs: ["list", "get", "watch"] +- apiGroups: ["admissionregistration.k8s.io"] + resources: ["validatingwebhookconfigurations"] + verbs: ["create", "get", "delete"] - apiGroups: ["linkerd.io"] resources: ["serviceprofiles"] verbs: ["list", "get", "watch"] @@ -314,6 +317,24 @@ spec: port: 8086 targetPort: 8086 --- +kind: Service +apiVersion: v1 +metadata: + name: linkerd-sp-validator + namespace: linkerd + labels: + linkerd.io/control-plane-component: controller + annotations: + linkerd.io/created-by: linkerd/cli dev-undefined +spec: + type: ClusterIP + selector: + linkerd.io/control-plane-component: controller + ports: + - name: sp-validator + port: 443 + targetPort: sp-validator +--- apiVersion: extensions/v1beta1 kind: Deployment metadata: @@ -424,6 +445,29 @@ spec: resources: {} securityContext: runAsUser: 2103 + - args: + - sp-validator + - -controller-namespace=linkerd + - -log-level=info + image: gcr.io/linkerd-io/controller:dev-undefined + imagePullPolicy: IfNotPresent + livenessProbe: + httpGet: + path: /ping + port: 9997 + initialDelaySeconds: 10 + name: sp-validator + ports: + - containerPort: 8443 + name: sp-validator + readinessProbe: + failureThreshold: 7 + httpGet: + path: /ready + port: 9997 + resources: {} + securityContext: + runAsUser: 2103 - env: - name: LINKERD2_PROXY_LOG value: warn,linkerd2_proxy=info @@ -1361,7 +1405,6 @@ spec: - proxy-injector - -controller-namespace=linkerd - -log-level=info - - -no-init-container=true image: gcr.io/linkerd-io/controller:dev-undefined imagePullPolicy: IfNotPresent livenessProbe: diff --git a/cli/cmd/testdata/install_output.golden b/cli/cmd/testdata/install_output.golden index ad8194f3f0b24..537242aac6e89 100644 --- a/cli/cmd/testdata/install_output.golden +++ b/cli/cmd/testdata/install_output.golden @@ -250,6 +250,9 @@ rules: - apiGroups: [""] resources: ["pods", "endpoints", "services", "replicationcontrollers", "namespaces"] verbs: ["list", "get", "watch"] +- apiGroups: ["admissionregistration.k8s.io"] + resources: ["validatingwebhookconfigurations"] + verbs: ["create", "get", "delete"] - apiGroups: ["linkerd.io"] resources: ["serviceprofiles"] verbs: ["list", "get", "watch"] @@ -303,6 +306,24 @@ spec: port: 8086 targetPort: 8086 --- +kind: Service +apiVersion: v1 +metadata: + name: linkerd-sp-validator + namespace: Namespace + labels: + ControllerComponentLabel: controller + annotations: + CreatedByAnnotation: CliVersion +spec: + type: ClusterIP + selector: + ControllerComponentLabel: controller + ports: + - name: sp-validator + port: 443 + targetPort: sp-validator +--- apiVersion: extensions/v1beta1 kind: Deployment metadata: @@ -414,6 +435,29 @@ spec: resources: {} securityContext: runAsUser: 2103 + - args: + - sp-validator + - -controller-namespace=Namespace + - -log-level=ControllerLogLevel + image: ControllerImage + imagePullPolicy: ImagePullPolicy + livenessProbe: + httpGet: + path: /ping + port: 9997 + initialDelaySeconds: 10 + name: sp-validator + ports: + - containerPort: 8443 + name: sp-validator + readinessProbe: + failureThreshold: 7 + httpGet: + path: /ready + port: 9997 + resources: {} + securityContext: + runAsUser: 2103 - env: - name: LINKERD2_PROXY_LOG value: warn,linkerd2_proxy=info @@ -1306,7 +1350,6 @@ spec: - proxy-injector - -controller-namespace=Namespace - -log-level=ControllerLogLevel - - -no-init-container=false image: ControllerImage imagePullPolicy: ImagePullPolicy livenessProbe: diff --git a/controller/cmd/proxy-injector/main.go b/controller/cmd/proxy-injector/main.go index 1e8073dcd0002..673848e5aaf55 100644 --- a/controller/cmd/proxy-injector/main.go +++ b/controller/cmd/proxy-injector/main.go @@ -1,74 +1,19 @@ package main import ( - "flag" - "net/http" - "os" - "os/signal" - - "github.com/linkerd/linkerd2/controller/k8s" injector "github.com/linkerd/linkerd2/controller/proxy-injector" - "github.com/linkerd/linkerd2/pkg/admin" - "github.com/linkerd/linkerd2/pkg/flags" - "github.com/linkerd/linkerd2/pkg/tls" - log "github.com/sirupsen/logrus" + "github.com/linkerd/linkerd2/controller/proxy-injector/tmpl" + "github.com/linkerd/linkerd2/controller/webhook" + "github.com/linkerd/linkerd2/pkg/k8s" ) func main() { - metricsAddr := flag.String("metrics-addr", ":9995", "address to serve scrapable metrics on") - addr := flag.String("addr", ":8443", "address to serve on") - kubeconfig := flag.String("kubeconfig", "", "path to kubeconfig") - controllerNamespace := flag.String("controller-namespace", "linkerd", "namespace in which Linkerd is installed") - webhookServiceName := flag.String("webhook-service", "linkerd-proxy-injector.linkerd.io", "name of the admission webhook") - noInitContainer := flag.Bool("no-init-container", false, "whether to use an init container or the linkerd-cni plugin") - flags.ConfigureAndParse() - - stop := make(chan os.Signal, 1) - defer close(stop) - signal.Notify(stop, os.Interrupt, os.Kill) - - k8sAPI, err := k8s.InitializeAPI(*kubeconfig, k8s.NS, k8s.RS) - if err != nil { - log.Fatalf("failed to initialize Kubernetes API: %s", err) - } - - rootCA, err := tls.GenerateRootCAWithDefaults("Proxy Injector Mutating Webhook Admission Controller CA") - if err != nil { - log.Fatalf("failed to create root CA: %s", err) - } - - webhookConfig, err := injector.NewWebhookConfig(k8sAPI, *controllerNamespace, *webhookServiceName, rootCA) - if err != nil { - log.Fatalf("failed to read the trust anchor file: %s", err) - } - - mwc, err := webhookConfig.Create() - if err != nil { - log.Fatalf("failed to create the mutating webhook configurations resource: %s", err) - } - log.Infof("created mutating webhook configuration: %s", mwc.ObjectMeta.SelfLink) - - s, err := injector.NewWebhookServer(k8sAPI, *addr, *controllerNamespace, *noInitContainer, rootCA) - if err != nil { - log.Fatalf("failed to initialize the webhook server: %s", err) - } - - k8sAPI.Sync() - - go func() { - log.Infof("listening at %s", *addr) - if err := s.ListenAndServeTLS("", ""); err != nil { - if err == http.ErrServerClosed { - return - } - log.Fatal(err) - } - }() - go admin.StartServer(*metricsAddr) - - <-stop - log.Info("shutting down webhook server") - if err := s.Shutdown(); err != nil { - log.Error(err) - } + webhook.Launch(&webhook.Config{ + MetricsPort: 9995, + WebhookConfigName: k8s.ProxyInjectorWebhookConfigName, + WebhookServiceName: k8s.ProxyInjectorWebhookServiceName, + TemplateStr: tmpl.MutatingWebhookConfigurationSpec, + Ops: &injector.Ops{}, + Handler: injector.Inject, + }) } diff --git a/controller/cmd/sp-validator/main.go b/controller/cmd/sp-validator/main.go new file mode 100644 index 0000000000000..e7608a0ad2cd6 --- /dev/null +++ b/controller/cmd/sp-validator/main.go @@ -0,0 +1,19 @@ +package main + +import ( + validator "github.com/linkerd/linkerd2/controller/sp-validator" + "github.com/linkerd/linkerd2/controller/sp-validator/tmpl" + "github.com/linkerd/linkerd2/controller/webhook" + "github.com/linkerd/linkerd2/pkg/k8s" +) + +func main() { + webhook.Launch(&webhook.Config{ + MetricsPort: 9997, + WebhookConfigName: k8s.SPValidatorWebhookConfigName, + WebhookServiceName: k8s.SPValidatorWebhookServiceName, + TemplateStr: tmpl.ValidatingWebhookConfigurationSpec, + Ops: &validator.Ops{}, + Handler: validator.AdmitSP, + }) +} diff --git a/controller/proxy-injector/server.go b/controller/proxy-injector/server.go deleted file mode 100644 index f085fa7d5fa63..0000000000000 --- a/controller/proxy-injector/server.go +++ /dev/null @@ -1,102 +0,0 @@ -package injector - -import ( - "context" - "crypto/tls" - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - - "github.com/linkerd/linkerd2/controller/k8s" - pkgTls "github.com/linkerd/linkerd2/pkg/tls" - log "github.com/sirupsen/logrus" -) - -// WebhookServer is the webhook's HTTP server. It has an embedded webhook which -// mutate all the requests. -type WebhookServer struct { - *http.Server - *Webhook -} - -// NewWebhookServer returns a new instance of the WebhookServer. -func NewWebhookServer(api *k8s.API, addr, controllerNamespace string, noInitContainer bool, rootCA *pkgTls.CA) (*WebhookServer, error) { - c, err := tlsConfig(rootCA, controllerNamespace) - if err != nil { - return nil, err - } - - server := &http.Server{ - Addr: addr, - TLSConfig: c, - } - - webhook, err := NewWebhook(api, controllerNamespace, noInitContainer) - if err != nil { - return nil, err - } - - ws := &WebhookServer{server, webhook} - ws.Handler = http.HandlerFunc(ws.serve) - return ws, nil -} - -func (w *WebhookServer) serve(res http.ResponseWriter, req *http.Request) { - var ( - data []byte - err error - ) - if req.Body != nil { - data, err = ioutil.ReadAll(req.Body) - if err != nil { - http.Error(res, err.Error(), http.StatusInternalServerError) - return - } - } - - if len(data) == 0 { - return - } - - response := w.Mutate(data) - responseJSON, err := json.Marshal(response) - if err != nil { - http.Error(res, err.Error(), http.StatusInternalServerError) - return - } - - if _, err := res.Write(responseJSON); err != nil { - http.Error(res, err.Error(), http.StatusInternalServerError) - return - } -} - -// Shutdown initiates a graceful shutdown of the underlying HTTP server. -func (w *WebhookServer) Shutdown() error { - return w.Server.Shutdown(context.Background()) -} - -func tlsConfig(rootCA *pkgTls.CA, controllerNamespace string) (*tls.Config, error) { - // must use the service short name in this TLS identity as the k8s api server - // looks for the webhook at ..svc, without the cluster - // domain. - dnsName := fmt.Sprintf("linkerd-proxy-injector.%s.svc", controllerNamespace) - cred, err := rootCA.GenerateEndEntityCred(dnsName) - if err != nil { - return nil, err - } - - certPEM := cred.EncodePEM() - log.Debugf("PEM-encoded certificate: %s\n", certPEM) - - keyPEM := cred.EncodePrivateKeyPEM() - cert, err := tls.X509KeyPair([]byte(certPEM), []byte(keyPEM)) - if err != nil { - return nil, err - } - - return &tls.Config{ - Certificates: []tls.Certificate{cert}, - }, nil -} diff --git a/controller/proxy-injector/server_test.go b/controller/proxy-injector/server_test.go deleted file mode 100644 index 93507d527e13b..0000000000000 --- a/controller/proxy-injector/server_test.go +++ /dev/null @@ -1,93 +0,0 @@ -package injector - -import ( - "bytes" - "io/ioutil" - "net/http" - "net/http/httptest" - "path/filepath" - "reflect" - "testing" - - "github.com/linkerd/linkerd2/controller/k8s" - "github.com/linkerd/linkerd2/controller/proxy-injector/fake" - "github.com/linkerd/linkerd2/pkg/tls" - log "github.com/sirupsen/logrus" -) - -var ( - testServer *WebhookServer -) - -func init() { - // create a webhook which uses its fake client to seed the sidecar configmap - k8sAPI, err := k8s.NewFakeAPI() - if err != nil { - panic(err) - } - - webhook, err := NewWebhook(k8sAPI, fake.DefaultControllerNamespace, false) - if err != nil { - panic(err) - } - log.SetOutput(ioutil.Discard) - factory = fake.NewFactory(filepath.Join("fake", "data")) - - testServer = &WebhookServer{nil, webhook} -} - -func TestServe(t *testing.T) { - t.Run("with empty http request body", func(t *testing.T) { - in := bytes.NewReader(nil) - request := httptest.NewRequest(http.MethodGet, "/", in) - - recorder := httptest.NewRecorder() - testServer.serve(recorder, request) - - if recorder.Code != http.StatusOK { - t.Errorf("HTTP response status mismatch. Expected: %d. Actual: %d", http.StatusOK, recorder.Code) - } - - if reflect.DeepEqual(recorder.Body.Bytes(), []byte("")) { - t.Errorf("Content mismatch. Expected HTTP response body to be empty %v", recorder.Body.Bytes()) - } - }) -} - -func TestShutdown(t *testing.T) { - server := &http.Server{Addr: ":0"} - testServer := WebhookServer{server, nil} - - go func() { - if err := testServer.ListenAndServe(); err != nil { - if err != http.ErrServerClosed { - t.Errorf("Expected server to be gracefully shutdown with error: %q", http.ErrServerClosed) - } - } - }() - - if err := testServer.Shutdown(); err != nil { - t.Fatal("Unexpected error: ", err) - } -} - -func TestNewWebhookServer(t *testing.T) { - rootCA, err := tls.GenerateRootCAWithDefaults("Test CA") - if err != nil { - log.Fatalf("failed to create root CA: %s", err) - } - - addr := ":7070" - k8sAPI, err := k8s.NewFakeAPI() - if err != nil { - t.Fatalf("NewFakeAPI returned an error: %s", err) - } - server, err := NewWebhookServer(k8sAPI, addr, fake.DefaultControllerNamespace, false, rootCA) - if err != nil { - t.Fatal("Unexpected error: ", err) - } - - if server.Addr != addr { - t.Errorf("Expected server address to be :%q", addr) - } -} diff --git a/controller/proxy-injector/tmpl/mutating_webhook_configuration.go b/controller/proxy-injector/tmpl/mutating_webhook_configuration.go index ab99297b04c85..ba36f2b375580 100644 --- a/controller/proxy-injector/tmpl/mutating_webhook_configuration.go +++ b/controller/proxy-injector/tmpl/mutating_webhook_configuration.go @@ -8,7 +8,7 @@ kind: MutatingWebhookConfiguration metadata: name: {{ .WebhookConfigName }} webhooks: -- name: {{ .WebhookServiceName }} +- name: linkerd-proxy-injector.linkerd.io clientConfig: service: name: linkerd-proxy-injector diff --git a/controller/proxy-injector/webhook.go b/controller/proxy-injector/webhook.go index f636992f353c0..ecad4e5a9abaa 100644 --- a/controller/proxy-injector/webhook.go +++ b/controller/proxy-injector/webhook.go @@ -12,80 +12,13 @@ import ( log "github.com/sirupsen/logrus" admissionv1beta1 "k8s.io/api/admission/v1beta1" v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/serializer" - "sigs.k8s.io/yaml" ) -// Webhook is a Kubernetes mutating admission webhook that mutates pods admission -// requests by injecting sidecar container spec into the pod spec during pod -// creation. -type Webhook struct { - k8sAPI *k8s.API - deserializer runtime.Decoder - controllerNamespace string - noInitContainer bool -} - -// NewWebhook returns a new instance of Webhook. -func NewWebhook(api *k8s.API, controllerNamespace string, noInitContainer bool) (*Webhook, error) { - var ( - scheme = runtime.NewScheme() - codecs = serializer.NewCodecFactory(scheme) - ) - - return &Webhook{ - k8sAPI: api, - deserializer: codecs.UniversalDeserializer(), - controllerNamespace: controllerNamespace, - noInitContainer: noInitContainer, - }, nil -} - -// Mutate changes the given pod spec by injecting the proxy sidecar container -// into the spec. The admission review object returns contains the original -// request and the response with the mutated pod spec. -func (w *Webhook) Mutate(data []byte) *admissionv1beta1.AdmissionReview { - admissionReview, err := w.decode(data) - if err != nil { - log.Error("failed to decode data. Reason: ", err) - admissionReview.Response = &admissionv1beta1.AdmissionResponse{ - UID: admissionReview.Request.UID, - Allowed: false, - Result: &metav1.Status{ - Message: err.Error(), - }, - } - return admissionReview - } - log.Infof("received admission review request %s", admissionReview.Request.UID) - log.Debugf("admission request: %+v", admissionReview.Request) - - admissionResponse, err := w.inject(admissionReview.Request) - if err != nil { - log.Error("failed to inject sidecar. Reason: ", err) - admissionReview.Response = &admissionv1beta1.AdmissionResponse{ - UID: admissionReview.Request.UID, - Allowed: false, - Result: &metav1.Status{ - Message: err.Error(), - }, - } - return admissionReview - } - admissionReview.Response = admissionResponse - - return admissionReview -} - -func (w *Webhook) decode(data []byte) (*admissionv1beta1.AdmissionReview, error) { - var admissionReview admissionv1beta1.AdmissionReview - err := yaml.Unmarshal(data, &admissionReview) - return &admissionReview, err -} - -func (w *Webhook) inject(request *admissionv1beta1.AdmissionRequest) (*admissionv1beta1.AdmissionResponse, error) { +// Inject returns an AdmissionResponse containing the patch, if any, to apply +// to the pod (proxy sidecar and eventually the init container to set it up) +func Inject(api *k8s.API, + request *admissionv1beta1.AdmissionRequest, +) (*admissionv1beta1.AdmissionResponse, error) { log.Debugf("request object bytes: %s", request.Object.Raw) globalConfig, err := config.Global(pkgK8s.MountPathGlobalConfig) @@ -98,7 +31,7 @@ func (w *Webhook) inject(request *admissionv1beta1.AdmissionRequest) (*admission return nil, err } - namespace, err := w.k8sAPI.NS().Lister().Get(request.Namespace) + namespace, err := api.NS().Lister().Get(request.Namespace) if err != nil { return nil, err } @@ -106,7 +39,7 @@ func (w *Webhook) inject(request *admissionv1beta1.AdmissionRequest) (*admission configs := &pb.All{Global: globalConfig, Proxy: proxyConfig} resourceConfig := inject.NewResourceConfig(configs, inject.OriginWebhook). - WithOwnerRetriever(w.ownerRetriever(request.Namespace)). + WithOwnerRetriever(ownerRetriever(api, request.Namespace)). WithNsAnnotations(nsAnnotations). WithKind(request.Kind.Kind) report, err := resourceConfig.ParseMetaAndYAML(request.Object.Raw) @@ -151,9 +84,9 @@ func (w *Webhook) inject(request *admissionv1beta1.AdmissionRequest) (*admission return admissionResponse, nil } -func (w *Webhook) ownerRetriever(ns string) inject.OwnerRetrieverFunc { +func ownerRetriever(api *k8s.API, ns string) inject.OwnerRetrieverFunc { return func(p *v1.Pod) (string, string) { p.SetNamespace(ns) - return w.k8sAPI.GetOwnerKindAndName(p) + return api.GetOwnerKindAndName(p) } } diff --git a/controller/proxy-injector/webhook_config.go b/controller/proxy-injector/webhook_config.go deleted file mode 100644 index bcaa02c7eabbe..0000000000000 --- a/controller/proxy-injector/webhook_config.go +++ /dev/null @@ -1,114 +0,0 @@ -package injector - -import ( - "bytes" - "encoding/base64" - "text/template" - - "github.com/linkerd/linkerd2/controller/k8s" - "github.com/linkerd/linkerd2/controller/proxy-injector/tmpl" - k8sPkg "github.com/linkerd/linkerd2/pkg/k8s" - "github.com/linkerd/linkerd2/pkg/tls" - log "github.com/sirupsen/logrus" - arv1beta1 "k8s.io/api/admissionregistration/v1beta1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/yaml" -) - -// WebhookConfig creates the MutatingWebhookConfiguration of the webhook. -type WebhookConfig struct { - controllerNamespace string - webhookServiceName string - trustAnchor []byte - configTemplate *template.Template - k8sAPI *k8s.API -} - -// NewWebhookConfig returns a new instance of initiator. -func NewWebhookConfig(api *k8s.API, controllerNamespace, webhookServiceName string, rootCA *tls.CA) (*WebhookConfig, error) { - trustAnchor := rootCA.Cred.EncodeCertificatePEM() - - t := template.New(k8sPkg.ProxyInjectorWebhookConfig) - - return &WebhookConfig{ - controllerNamespace: controllerNamespace, - webhookServiceName: webhookServiceName, - trustAnchor: []byte(trustAnchor), - configTemplate: template.Must(t.Parse(tmpl.MutatingWebhookConfigurationSpec)), - k8sAPI: api, - }, nil -} - -// Create sends the request to create the MutatingWebhookConfiguration resource. -func (w *WebhookConfig) Create() (*arv1beta1.MutatingWebhookConfiguration, error) { - exists, err := w.exists() - if err != nil { - return nil, err - } - - if exists { - log.Info("deleting existing mutating webhook configuration") - if err := w.delete(); err != nil { - return nil, err - } - } - - return w.create() -} - -// exists returns true if the mutating webhook configuration exists. Otherwise, -// it returns false. -func (w *WebhookConfig) exists() (bool, error) { - _, err := w.get() - if err != nil { - if apierrors.IsNotFound(err) { - return false, nil - } - - return false, err - } - - return true, nil -} - -func (w *WebhookConfig) create() (*arv1beta1.MutatingWebhookConfiguration, error) { - var ( - buf = &bytes.Buffer{} - spec = struct { - WebhookConfigName string - WebhookServiceName string - ControllerNamespace string - CABundle string - }{ - WebhookConfigName: k8sPkg.ProxyInjectorWebhookConfig, - WebhookServiceName: w.webhookServiceName, - ControllerNamespace: w.controllerNamespace, - CABundle: base64.StdEncoding.EncodeToString(w.trustAnchor), - } - ) - if err := w.configTemplate.Execute(buf, spec); err != nil { - return nil, err - } - - var config arv1beta1.MutatingWebhookConfiguration - if err := yaml.Unmarshal(buf.Bytes(), &config); err != nil { - log.Infof("failed to unmarshal mutating webhook configuration: %s\n%s\n", err, buf.String()) - return nil, err - } - - return w.k8sAPI.Client. - AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Create(&config) -} - -func (w *WebhookConfig) get() (*arv1beta1.MutatingWebhookConfiguration, error) { - return w.k8sAPI.Client. - AdmissionregistrationV1beta1().MutatingWebhookConfigurations(). - Get(k8sPkg.ProxyInjectorWebhookConfig, metav1.GetOptions{}) -} - -func (w *WebhookConfig) delete() error { - return w.k8sAPI.Client. - AdmissionregistrationV1beta1().MutatingWebhookConfigurations(). - Delete(k8sPkg.ProxyInjectorWebhookConfig, &metav1.DeleteOptions{}) -} diff --git a/controller/proxy-injector/webhook_config_test.go b/controller/proxy-injector/webhook_config_test.go deleted file mode 100644 index b38f9af49c03b..0000000000000 --- a/controller/proxy-injector/webhook_config_test.go +++ /dev/null @@ -1,62 +0,0 @@ -package injector - -import ( - "io/ioutil" - "log" - "testing" - - "github.com/linkerd/linkerd2/controller/k8s" - "github.com/linkerd/linkerd2/controller/proxy-injector/fake" - "github.com/linkerd/linkerd2/pkg/tls" -) - -func TestCreate(t *testing.T) { - var ( - namespace = fake.DefaultControllerNamespace - webhookServiceName = "test.linkerd.io" - ) - log.SetOutput(ioutil.Discard) - - k8sAPI, err := k8s.NewFakeAPI() - if err != nil { - t.Fatalf("NewFakeAPI returned an error: %s", err) - } - - rootCA, err := tls.GenerateRootCAWithDefaults("Test CA") - if err != nil { - t.Fatalf("failed to create root CA: %s", err) - } - - webhookConfig, err := NewWebhookConfig(k8sAPI, namespace, webhookServiceName, rootCA) - if err != nil { - t.Fatal("Unexpected error: ", err) - } - - // expect mutating webhook configuration to not exist - exists, err := webhookConfig.exists() - if err != nil { - t.Fatal("Unexpected error: ", err) - } - if exists { - t.Error("Unexpected mutating webhook configuration. Expect resources to not exist") - } - - // create the mutating webhook configuration - if _, err := webhookConfig.Create(); err != nil { - t.Fatal("Unexpected error: ", err) - } - - // expect mutating webhook configuration to exist - exists, err = webhookConfig.exists() - if err != nil { - t.Fatal("Unexpected error: ", err) - } - if !exists { - t.Error("Expected mutating webhook configuration to exist") - } - - // expect the mutating webhook configuration to be created without errors - if _, err := webhookConfig.Create(); err != nil { - t.Fatal("Unexpected error: ", err) - } -} diff --git a/controller/proxy-injector/webhook_ops.go b/controller/proxy-injector/webhook_ops.go new file mode 100644 index 0000000000000..d85cd1ff15efc --- /dev/null +++ b/controller/proxy-injector/webhook_ops.go @@ -0,0 +1,50 @@ +package injector + +import ( + "bytes" + + "github.com/linkerd/linkerd2/controller/k8s" + k8sPkg "github.com/linkerd/linkerd2/pkg/k8s" + log "github.com/sirupsen/logrus" + arv1beta1 "k8s.io/api/admissionregistration/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/yaml" +) + +// Ops satisfies the ConfigOps interface for managing MutatingWebhook configs +type Ops struct{} + +// Create persists the Mutating webhook config and returns its SelfLink +func (*Ops) Create(api *k8s.API, buf *bytes.Buffer) (string, error) { + var config arv1beta1.MutatingWebhookConfiguration + if err := yaml.Unmarshal(buf.Bytes(), &config); err != nil { + log.Infof("failed to unmarshal mutating webhook configuration: %s\n%s\n", err, buf.String()) + return "", err + } + + obj, err := api.Client. + AdmissionregistrationV1beta1(). + MutatingWebhookConfigurations(). + Create(&config) + if err != nil { + return "", err + } + return obj.ObjectMeta.SelfLink, nil +} + +// Get returns an error if the Mutating webhook doesn't exist +func (*Ops) Get(api *k8s.API) error { + _, err := api.Client. + AdmissionregistrationV1beta1(). + MutatingWebhookConfigurations(). + Get(k8sPkg.ProxyInjectorWebhookConfigName, metav1.GetOptions{}) + return err +} + +// Delete removes the Mutating webhook from the cluster +func (*Ops) Delete(api *k8s.API) error { + return api.Client. + AdmissionregistrationV1beta1(). + MutatingWebhookConfigurations(). + Delete(k8sPkg.ProxyInjectorWebhookConfigName, &metav1.DeleteOptions{}) +} diff --git a/controller/proxy-injector/webhook_test.go b/controller/proxy-injector/webhook_test.go index 3c2583777e7a0..9b923eb0d6aef 100644 --- a/controller/proxy-injector/webhook_test.go +++ b/controller/proxy-injector/webhook_test.go @@ -2,6 +2,7 @@ package injector import ( "fmt" + "path/filepath" "testing" "github.com/linkerd/linkerd2/controller/gen/config" @@ -15,7 +16,6 @@ import ( ) var ( - factory *fake.Factory configs = &config.All{ Global: &config.Global{ LinkerdNamespace: "linkerd", @@ -50,6 +50,7 @@ func confNsDisabled() *inject.ResourceConfig { } func TestGetPatch(t *testing.T) { + factory := fake.NewFactory(filepath.Join("fake", "data")) nsEnabled, err := factory.Namespace("namespace-inject-enabled.yaml") if err != nil { t.Fatalf("Unexpected error: %s", err) diff --git a/controller/sp-validator/tmpl/validating_webhook_configuration.go b/controller/sp-validator/tmpl/validating_webhook_configuration.go new file mode 100644 index 0000000000000..86b8cbf6fab79 --- /dev/null +++ b/controller/sp-validator/tmpl/validating_webhook_configuration.go @@ -0,0 +1,22 @@ +package tmpl + +// ValidatingWebhookConfigurationSpec provides a template for a +// ValidatingWebhookConfiguration. +var ValidatingWebhookConfigurationSpec = ` +apiVersion: admissionregistration.k8s.io/v1beta1 +kind: ValidatingWebhookConfiguration +metadata: + name: {{ .WebhookConfigName }} +webhooks: +- name: linkerd-sp-validator.linkerd.io + clientConfig: + service: + name: linkerd-sp-validator + namespace: {{ .ControllerNamespace }} + path: "/" + caBundle: {{ .CABundle }} + rules: + - operations: [ "CREATE" , "UPDATE" ] + apiGroups: ["linkerd.io"] + apiVersions: ["v1alpha1"] + resources: ["serviceprofiles"]` diff --git a/controller/sp-validator/webhook.go b/controller/sp-validator/webhook.go new file mode 100644 index 0000000000000..18eb399adb4eb --- /dev/null +++ b/controller/sp-validator/webhook.go @@ -0,0 +1,21 @@ +package validator + +import ( + "github.com/linkerd/linkerd2/controller/k8s" + "github.com/linkerd/linkerd2/pkg/profiles" + admissionv1beta1 "k8s.io/api/admission/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// AdmitSP verifies that the received Admission Request contains a valid +// Service Profile definition +func AdmitSP( + _ *k8s.API, request *admissionv1beta1.AdmissionRequest, +) (*admissionv1beta1.AdmissionResponse, error) { + admissionResponse := &admissionv1beta1.AdmissionResponse{Allowed: true} + if err := profiles.Validate(request.Object.Raw); err != nil { + admissionResponse.Allowed = false + admissionResponse.Result = &metav1.Status{Message: err.Error()} + } + return admissionResponse, nil +} diff --git a/controller/sp-validator/webhook_ops.go b/controller/sp-validator/webhook_ops.go new file mode 100644 index 0000000000000..b74e4c276b3e6 --- /dev/null +++ b/controller/sp-validator/webhook_ops.go @@ -0,0 +1,50 @@ +package validator + +import ( + "bytes" + + "github.com/linkerd/linkerd2/controller/k8s" + k8sPkg "github.com/linkerd/linkerd2/pkg/k8s" + log "github.com/sirupsen/logrus" + arv1beta1 "k8s.io/api/admissionregistration/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/yaml" +) + +// Ops satisfies the ConfigOps interface for managing ValidatingWebhook configs +type Ops struct{} + +// Create persists the Validating webhook config and returns its SelfLink +func (*Ops) Create(api *k8s.API, buf *bytes.Buffer) (string, error) { + var config arv1beta1.ValidatingWebhookConfiguration + if err := yaml.Unmarshal(buf.Bytes(), &config); err != nil { + log.Infof("failed to unmarshal validating webhook configuration: %s\n%s\n", err, buf.String()) + return "", err + } + + obj, err := api.Client. + AdmissionregistrationV1beta1(). + ValidatingWebhookConfigurations(). + Create(&config) + if err != nil { + return "", err + } + return obj.ObjectMeta.SelfLink, nil +} + +// Get returns an error if the Validating webhook doesn't exist +func (*Ops) Get(api *k8s.API) error { + _, err := api.Client. + AdmissionregistrationV1beta1(). + ValidatingWebhookConfigurations(). + Get(k8sPkg.SPValidatorWebhookConfigName, metav1.GetOptions{}) + return err +} + +// Delete removes the Validating webhook from the cluster +func (*Ops) Delete(api *k8s.API) error { + return api.Client. + AdmissionregistrationV1beta1(). + ValidatingWebhookConfigurations(). + Delete(k8sPkg.SPValidatorWebhookConfigName, &metav1.DeleteOptions{}) +} diff --git a/controller/webhook/config.go b/controller/webhook/config.go new file mode 100644 index 0000000000000..fe3249b75cb28 --- /dev/null +++ b/controller/webhook/config.go @@ -0,0 +1,81 @@ +package webhook + +import ( + "bytes" + "encoding/base64" + "html/template" + + "github.com/linkerd/linkerd2/controller/k8s" + "github.com/linkerd/linkerd2/pkg/tls" + log "github.com/sirupsen/logrus" + apierrors "k8s.io/apimachinery/pkg/api/errors" +) + +// ConfigOps declares the methods used to manage the webhook configs in the cluster +type ConfigOps interface { + Create(*k8s.API, *bytes.Buffer) (string, error) + Get(*k8s.API) error + Delete(*k8s.API) error +} + +// Config contains all the necessary data to build and persist the webhook resource +type Config struct { + MetricsPort uint32 + WebhookConfigName string + WebhookServiceName string + TemplateStr string + Ops ConfigOps + Handler handlerFunc + api *k8s.API + controllerNamespace string + rootCA *tls.CA +} + +// Create deletes the webhook config if it already exists and then creates +// a new one +func (c *Config) Create() (string, error) { + exists, err := c.Exists() + if err != nil { + return "", err + } + + if exists { + log.Info("deleting existing webhook configuration") + if err := c.Ops.Delete(c.api); err != nil { + return "", err + } + } + + var ( + buf = &bytes.Buffer{} + trustAnchor = []byte(c.rootCA.Cred.EncodeCertificatePEM()) + spec = struct { + WebhookConfigName string + ControllerNamespace string + CABundle string + }{ + WebhookConfigName: c.WebhookConfigName, + ControllerNamespace: c.controllerNamespace, + CABundle: base64.StdEncoding.EncodeToString(trustAnchor), + } + ) + t := template.Must(template.New("webhook").Parse(c.TemplateStr)) + if err := t.Execute(buf, spec); err != nil { + return "", err + } + + return c.Ops.Create(c.api, buf) +} + +// Exists returns true if the webhook already exists +func (c *Config) Exists() (bool, error) { + if err := c.Ops.Get(c.api); err != nil { + if apierrors.IsNotFound(err) { + return false, nil + } + + return false, err + } + + return true, nil +} diff --git a/controller/webhook/config_test.go b/controller/webhook/config_test.go new file mode 100644 index 0000000000000..553e2239cf303 --- /dev/null +++ b/controller/webhook/config_test.go @@ -0,0 +1,87 @@ +package webhook + +import ( + "fmt" + "testing" + + "github.com/linkerd/linkerd2/controller/k8s" + injector "github.com/linkerd/linkerd2/controller/proxy-injector" + injectorTmpl "github.com/linkerd/linkerd2/controller/proxy-injector/tmpl" + validator "github.com/linkerd/linkerd2/controller/sp-validator" + validatorTmpl "github.com/linkerd/linkerd2/controller/sp-validator/tmpl" + k8sPkg "github.com/linkerd/linkerd2/pkg/k8s" + "github.com/linkerd/linkerd2/pkg/tls" +) + +func TestCreate(t *testing.T) { + k8sAPI, err := k8s.NewFakeAPI() + if err != nil { + panic(err) + } + + rootCA, err := tls.GenerateRootCAWithDefaults("Test CA") + if err != nil { + t.Fatalf("failed to create root CA: %s", err) + } + + testCases := []struct { + testName string + configName string + serviceName string + templateStr string + ops ConfigOps + }{ + { + testName: "Mutating webhook", + configName: k8sPkg.ProxyInjectorWebhookConfigName, + serviceName: "mutatingwebhook.linkerd.io", + templateStr: injectorTmpl.MutatingWebhookConfigurationSpec, + ops: &injector.Ops{}, + }, + { + testName: "Validating webhook", + configName: k8sPkg.SPValidatorWebhookConfigName, + serviceName: "validatingwebhook.linkerd.io", + templateStr: validatorTmpl.ValidatingWebhookConfigurationSpec, + ops: &validator.Ops{}, + }, + } + + for _, tc := range testCases { + tc := tc // pin + t.Run(fmt.Sprintf(tc.testName), func(t *testing.T) { + webhookConfig := &Config{ + WebhookConfigName: tc.configName, + WebhookServiceName: tc.serviceName, + TemplateStr: tc.templateStr, + Ops: tc.ops, + api: k8sAPI, + controllerNamespace: "linkerd", + rootCA: rootCA, + } + + // expect configuration to not exist + exists, err := webhookConfig.Exists() + if err != nil { + t.Fatal("Unexpected error: ", err) + } + if exists { + t.Error("Unexpected webhook configuration. Expect resource to not exist") + } + + // create the webhook configuration + if _, err := webhookConfig.Create(); err != nil { + t.Fatal("Unexpected error: ", err) + } + + // expect webhook configuration to exist + exists, err = webhookConfig.Exists() + if err != nil { + t.Fatal("Unexpected error: ", err) + } + if !exists { + t.Error("Expected webhook configuration to exist") + } + }) + } +} diff --git a/controller/webhook/launcher.go b/controller/webhook/launcher.go new file mode 100644 index 0000000000000..a9ac38d98a5fb --- /dev/null +++ b/controller/webhook/launcher.go @@ -0,0 +1,64 @@ +package webhook + +import ( + "flag" + "os" + "os/signal" + "strconv" + + "github.com/linkerd/linkerd2/controller/k8s" + "github.com/linkerd/linkerd2/pkg/admin" + "github.com/linkerd/linkerd2/pkg/flags" + "github.com/linkerd/linkerd2/pkg/tls" + log "github.com/sirupsen/logrus" +) + +// Launch sets up and starts the webhook and metrics servers +func Launch(config *Config) { + p := strconv.FormatUint(uint64(config.MetricsPort), 10) + metricsAddr := flag.String("metrics-addr", ":"+p, "address to serve scrapable metrics on") + addr := flag.String("addr", ":8443", "address to serve on") + kubeconfig := flag.String("kubeconfig", "", "path to kubeconfig") + controllerNamespace := flag.String("controller-namespace", "linkerd", "namespace in which Linkerd is installed") + flags.ConfigureAndParse() + + stop := make(chan os.Signal, 1) + defer close(stop) + signal.Notify(stop, os.Interrupt, os.Kill) + + k8sAPI, err := k8s.InitializeAPI(*kubeconfig, k8s.NS, k8s.RS) + if err != nil { + log.Fatalf("failed to initialize Kubernetes API: %s", err) + } + + rootCA, err := tls.GenerateRootCAWithDefaults(config.WebhookServiceName) + if err != nil { + log.Fatalf("failed to create root CA: %s", err) + } + + config.api = k8sAPI + config.controllerNamespace = *controllerNamespace + config.rootCA = rootCA + + selfLink, err := config.Create() + if err != nil { + log.Fatalf("failed to create the webhook configurations resource: %s", err) + } + log.Infof("created webhook configuration: %s", selfLink) + + s, err := NewServer(k8sAPI, *addr, config.WebhookServiceName, *controllerNamespace, rootCA, config.Handler) + if err != nil { + log.Fatalf("failed to initialize the webhook server: %s", err) + } + + k8sAPI.Sync() + + go s.Start() + go admin.StartServer(*metricsAddr) + + <-stop + log.Info("shutting down webhook server") + if err := s.Shutdown(); err != nil { + log.Error(err) + } +} diff --git a/controller/webhook/server.go b/controller/webhook/server.go new file mode 100644 index 0000000000000..a2b9c6edd4c3a --- /dev/null +++ b/controller/webhook/server.go @@ -0,0 +1,152 @@ +package webhook + +import ( + "context" + "crypto/tls" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + + "github.com/linkerd/linkerd2/controller/k8s" + pkgTls "github.com/linkerd/linkerd2/pkg/tls" + log "github.com/sirupsen/logrus" + admissionv1beta1 "k8s.io/api/admission/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/yaml" +) + +type handlerFunc func(*k8s.API, *admissionv1beta1.AdmissionRequest) (*admissionv1beta1.AdmissionResponse, error) + +// Server describes the https server implementing the webhook +type Server struct { + *http.Server + api *k8s.API + handler handlerFunc + controllerNamespace string +} + +// NewServer returns a new instance of Server +func NewServer(api *k8s.API, addr, name, controllerNamespace string, rootCA *pkgTls.CA, handler handlerFunc) (*Server, error) { + c, err := tlsConfig(rootCA, name, controllerNamespace) + if err != nil { + return nil, err + } + + server := &http.Server{ + Addr: addr, + TLSConfig: c, + } + + s := &Server{server, api, handler, controllerNamespace} + s.Handler = http.HandlerFunc(s.serve) + return s, nil +} + +// Start starts the https server +func (s *Server) Start() { + log.Infof("listening at %s", s.Server.Addr) + if err := s.ListenAndServeTLS("", ""); err != nil { + if err == http.ErrServerClosed { + return + } + log.Fatal(err) + } +} + +func (s *Server) serve(res http.ResponseWriter, req *http.Request) { + var ( + data []byte + err error + ) + if req.Body != nil { + data, err = ioutil.ReadAll(req.Body) + if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } + } + + if len(data) == 0 { + return + } + + response := s.processReq(data) + responseJSON, err := json.Marshal(response) + if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } + + if _, err := res.Write(responseJSON); err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } +} + +func (s *Server) processReq(data []byte) *admissionv1beta1.AdmissionReview { + admissionReview, err := decode(data) + if err != nil { + log.Error("failed to decode data. Reason: ", err) + admissionReview.Response = &admissionv1beta1.AdmissionResponse{ + UID: admissionReview.Request.UID, + Allowed: false, + Result: &metav1.Status{ + Message: err.Error(), + }, + } + return admissionReview + } + log.Infof("received admission review request %s", admissionReview.Request.UID) + log.Debugf("admission request: %+v", admissionReview.Request) + + admissionResponse, err := s.handler(s.api, admissionReview.Request) + if err != nil { + log.Error("failed to inject sidecar. Reason: ", err) + admissionReview.Response = &admissionv1beta1.AdmissionResponse{ + UID: admissionReview.Request.UID, + Allowed: false, + Result: &metav1.Status{ + Message: err.Error(), + }, + } + return admissionReview + } + admissionReview.Response = admissionResponse + + return admissionReview +} + +// Shutdown initiates a graceful shutdown of the underlying HTTP server. +func (s *Server) Shutdown() error { + return s.Server.Shutdown(context.Background()) +} + +func tlsConfig(rootCA *pkgTls.CA, name, controllerNamespace string) (*tls.Config, error) { + // must use the service short name in this TLS identity as the k8s api server + // looks for the webhook at ..svc, without the cluster + // domain. + dnsName := fmt.Sprintf("%s.%s.svc", name, controllerNamespace) + + cred, err := rootCA.GenerateEndEntityCred(dnsName) + if err != nil { + return nil, err + } + + certPEM := cred.EncodePEM() + keyPEM := cred.EncodePrivateKeyPEM() + cert, err := tls.X509KeyPair([]byte(certPEM), []byte(keyPEM)) + if err != nil { + return nil, err + } + + return &tls.Config{ + Certificates: []tls.Certificate{cert}, + }, nil +} + +func decode(data []byte) (*admissionv1beta1.AdmissionReview, error) { + var admissionReview admissionv1beta1.AdmissionReview + err := yaml.Unmarshal(data, &admissionReview) + return &admissionReview, err +} diff --git a/controller/webhook/server_test.go b/controller/webhook/server_test.go new file mode 100644 index 0000000000000..cd8d063f40fdc --- /dev/null +++ b/controller/webhook/server_test.go @@ -0,0 +1,52 @@ +package webhook + +import ( + "bytes" + "net/http" + "net/http/httptest" + "reflect" + "testing" + + "github.com/linkerd/linkerd2/controller/k8s" +) + +func TestServe(t *testing.T) { + t.Run("with empty http request body", func(t *testing.T) { + k8sAPI, err := k8s.NewFakeAPI() + if err != nil { + panic(err) + } + testServer := &Server{nil, k8sAPI, nil, "linkerd"} + + in := bytes.NewReader(nil) + request := httptest.NewRequest(http.MethodGet, "/", in) + + recorder := httptest.NewRecorder() + testServer.serve(recorder, request) + + if recorder.Code != http.StatusOK { + t.Errorf("HTTP response status mismatch. Expected: %d. Actual: %d", http.StatusOK, recorder.Code) + } + + if reflect.DeepEqual(recorder.Body.Bytes(), []byte("")) { + t.Errorf("Content mismatch. Expected HTTP response body to be empty %v", recorder.Body.Bytes()) + } + }) +} + +func TestShutdown(t *testing.T) { + server := &http.Server{Addr: ":0"} + testServer := &Server{server, nil, nil, "linkerd"} + + go func() { + if err := testServer.ListenAndServe(); err != nil { + if err != http.ErrServerClosed { + t.Errorf("Expected server to be gracefully shutdown with error: %q", http.ErrServerClosed) + } + } + }() + + if err := testServer.Shutdown(); err != nil { + t.Fatal("Unexpected error: ", err) + } +} diff --git a/pkg/k8s/labels.go b/pkg/k8s/labels.go index 1a407b0fec746..8e38aaf24679c 100644 --- a/pkg/k8s/labels.go +++ b/pkg/k8s/labels.go @@ -191,9 +191,17 @@ const ( // ProxyAdminPortName is the name of the Linkerd Proxy's metrics port. ProxyAdminPortName = "linkerd-admin" - // ProxyInjectorWebhookConfig is the name of the mutating webhook - // configuration resource of the proxy-injector webhook. - ProxyInjectorWebhookConfig = "linkerd-proxy-injector-webhook-config" + // ProxyInjectorWebhookServiceName is the name of the mutating webhook service + ProxyInjectorWebhookServiceName = "linkerd-proxy-injector" + + // ProxyInjectorWebhookConfigName is the name of the mutating webhook configuration + ProxyInjectorWebhookConfigName = ProxyInjectorWebhookServiceName + "-webhook-config" + + // SPValidatorWebhookServiceName is the name of the validating webhook service + SPValidatorWebhookServiceName = "linkerd-sp-validator" + + // SPValidatorWebhookConfigName is the name of the validating webhook configuration + SPValidatorWebhookConfigName = SPValidatorWebhookServiceName + "-webhook-config" /* * Mount paths