From 7efe385feb2afbb5e6d2918ff005d56e17ad00e9 Mon Sep 17 00:00:00 2001 From: Alejandro Pedraza Date: Tue, 26 Mar 2019 11:53:56 -0500 Subject: [PATCH] Have the Webhook react to pod creation/update only (#2472) Have the Webhook react to pod creation/update only This was already working almost out-of-the-box, just had to: - Change the webhook config so it watches pods instead of deployments - Grant some extra ClusterRole permissions - Add the piece that figures what's the OwnerReference and add the label for it - Manually inject service account mount paths - Readd volumes tests Fixes #2342 and #1751 Signed-off-by: Alejandro Pedraza --- chart/templates/proxy_injector.yaml | 8 +- cli/cmd/inject.go | 6 +- ...stall_no_init_container_auto_inject.golden | 8 +- cli/cmd/testdata/install_output.golden | 8 +- controller/cmd/proxy-injector/main.go | 10 +- .../inject-linkerd-secrets-volume-spec.yaml | 4 + controller/proxy-injector/server.go | 6 +- controller/proxy-injector/server_test.go | 21 ++-- .../tmpl/mutating_webhook_configuration.go | 6 +- controller/proxy-injector/webhook.go | 39 ++++---- controller/proxy-injector/webhook_config.go | 19 ++-- .../proxy-injector/webhook_config_test.go | 8 +- controller/proxy-injector/webhook_test.go | 4 +- pkg/inject/inject.go | 96 ++++++++++++------- pkg/inject/inject_test.go | 2 +- pkg/inject/patch.go | 48 +++++----- pkg/inject/patch_test.go | 21 ++-- pkg/inject/report.go | 8 +- pkg/k8s/labels.go | 4 + 19 files changed, 205 insertions(+), 121 deletions(-) create mode 100644 controller/proxy-injector/fake/data/inject-linkerd-secrets-volume-spec.yaml diff --git a/chart/templates/proxy_injector.yaml b/chart/templates/proxy_injector.yaml index 105c1c4b3..f3fc1ab6a 100644 --- a/chart/templates/proxy_injector.yaml +++ b/chart/templates/proxy_injector.yaml @@ -80,7 +80,13 @@ rules: verbs: ["create", "get", "delete"] - apiGroups: [""] resources: ["namespaces"] - verbs: ["get"] + verbs: ["list", "get", "watch"] +- apiGroups: [""] + resources: ["pods"] + verbs: ["list"] +- apiGroups: ["apps"] + resources: ["replicasets"] + verbs: ["list", "get", "watch"] --- kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 diff --git a/cli/cmd/inject.go b/cli/cmd/inject.go index 634d887cd..bdb4b5519 100644 --- a/cli/cmd/inject.go +++ b/cli/cmd/inject.go @@ -142,7 +142,11 @@ func (rt resourceTransformerInject) transform(bytes []byte) ([]byte, []inject.Re if patchJSON == nil { return bytes, reports, nil } - log.Infof("patch generated for: %s", conf) + // TODO: refactor GetPatch() for it to return just one report item + if len(reports) > 0 { + r := reports[0] + log.Infof("patch generated for: %s", r.ResName()) + } log.Debugf("patch: %s", patchJSON) patch, err := jsonpatch.DecodePatch(patchJSON) if err != nil { 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 c1d1711aa..4d429a9a1 100644 --- a/cli/cmd/testdata/install_no_init_container_auto_inject.golden +++ b/cli/cmd/testdata/install_no_init_container_auto_inject.golden @@ -1491,7 +1491,13 @@ rules: verbs: ["create", "get", "delete"] - apiGroups: [""] resources: ["namespaces"] - verbs: ["get"] + verbs: ["list", "get", "watch"] +- apiGroups: [""] + resources: ["pods"] + verbs: ["list"] +- apiGroups: ["apps"] + resources: ["replicasets"] + verbs: ["list", "get", "watch"] --- kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 diff --git a/cli/cmd/testdata/install_output.golden b/cli/cmd/testdata/install_output.golden index 35ca56bf1..9b78849ad 100644 --- a/cli/cmd/testdata/install_output.golden +++ b/cli/cmd/testdata/install_output.golden @@ -1424,7 +1424,13 @@ rules: verbs: ["create", "get", "delete"] - apiGroups: [""] resources: ["namespaces"] - verbs: ["get"] + verbs: ["list", "get", "watch"] +- apiGroups: [""] + resources: ["pods"] + verbs: ["list"] +- apiGroups: ["apps"] + resources: ["replicasets"] + verbs: ["list", "get", "watch"] --- kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 diff --git a/controller/cmd/proxy-injector/main.go b/controller/cmd/proxy-injector/main.go index bb86c51e5..1e8073dcd 100644 --- a/controller/cmd/proxy-injector/main.go +++ b/controller/cmd/proxy-injector/main.go @@ -27,9 +27,9 @@ func main() { defer close(stop) signal.Notify(stop, os.Interrupt, os.Kill) - k8sClient, err := k8s.NewClientSet(*kubeconfig) + k8sAPI, err := k8s.InitializeAPI(*kubeconfig, k8s.NS, k8s.RS) if err != nil { - log.Fatalf("failed to initialize Kubernetes client: %s", err) + log.Fatalf("failed to initialize Kubernetes API: %s", err) } rootCA, err := tls.GenerateRootCAWithDefaults("Proxy Injector Mutating Webhook Admission Controller CA") @@ -37,7 +37,7 @@ func main() { log.Fatalf("failed to create root CA: %s", err) } - webhookConfig, err := injector.NewWebhookConfig(k8sClient, *controllerNamespace, *webhookServiceName, rootCA) + webhookConfig, err := injector.NewWebhookConfig(k8sAPI, *controllerNamespace, *webhookServiceName, rootCA) if err != nil { log.Fatalf("failed to read the trust anchor file: %s", err) } @@ -48,11 +48,13 @@ func main() { } log.Infof("created mutating webhook configuration: %s", mwc.ObjectMeta.SelfLink) - s, err := injector.NewWebhookServer(k8sClient, *addr, *controllerNamespace, *noInitContainer, rootCA) + 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 { diff --git a/controller/proxy-injector/fake/data/inject-linkerd-secrets-volume-spec.yaml b/controller/proxy-injector/fake/data/inject-linkerd-secrets-volume-spec.yaml new file mode 100644 index 000000000..6144b7364 --- /dev/null +++ b/controller/proxy-injector/fake/data/inject-linkerd-secrets-volume-spec.yaml @@ -0,0 +1,4 @@ +name: linkerd-secrets +secret: + secretName: nginx-deployment-tls-linkerd-io + optional: true \ No newline at end of file diff --git a/controller/proxy-injector/server.go b/controller/proxy-injector/server.go index 37ad5c9e4..f085fa7d5 100644 --- a/controller/proxy-injector/server.go +++ b/controller/proxy-injector/server.go @@ -8,9 +8,9 @@ import ( "io/ioutil" "net/http" + "github.com/linkerd/linkerd2/controller/k8s" pkgTls "github.com/linkerd/linkerd2/pkg/tls" log "github.com/sirupsen/logrus" - "k8s.io/client-go/kubernetes" ) // WebhookServer is the webhook's HTTP server. It has an embedded webhook which @@ -21,7 +21,7 @@ type WebhookServer struct { } // NewWebhookServer returns a new instance of the WebhookServer. -func NewWebhookServer(client kubernetes.Interface, addr, controllerNamespace string, noInitContainer bool, rootCA *pkgTls.CA) (*WebhookServer, error) { +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 @@ -32,7 +32,7 @@ func NewWebhookServer(client kubernetes.Interface, addr, controllerNamespace str TLSConfig: c, } - webhook, err := NewWebhook(client, controllerNamespace, noInitContainer) + webhook, err := NewWebhook(api, controllerNamespace, noInitContainer) if err != nil { return nil, err } diff --git a/controller/proxy-injector/server_test.go b/controller/proxy-injector/server_test.go index 81b5e5924..93507d527 100644 --- a/controller/proxy-injector/server_test.go +++ b/controller/proxy-injector/server_test.go @@ -9,6 +9,7 @@ import ( "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" @@ -20,9 +21,12 @@ var ( func init() { // create a webhook which uses its fake client to seed the sidecar configmap - fakeClient := fake.NewClient("") + k8sAPI, err := k8s.NewFakeAPI() + if err != nil { + panic(err) + } - webhook, err := NewWebhook(fakeClient, fake.DefaultControllerNamespace, false) + webhook, err := NewWebhook(k8sAPI, fake.DefaultControllerNamespace, false) if err != nil { panic(err) } @@ -73,13 +77,12 @@ func TestNewWebhookServer(t *testing.T) { log.Fatalf("failed to create root CA: %s", err) } - var ( - addr = ":7070" - kubeconfig = "" - ) - fakeClient := fake.NewClient(kubeconfig) - - server, err := NewWebhookServer(fakeClient, addr, fake.DefaultControllerNamespace, false, rootCA) + 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) } diff --git a/controller/proxy-injector/tmpl/mutating_webhook_configuration.go b/controller/proxy-injector/tmpl/mutating_webhook_configuration.go index f08245663..ab99297b0 100644 --- a/controller/proxy-injector/tmpl/mutating_webhook_configuration.go +++ b/controller/proxy-injector/tmpl/mutating_webhook_configuration.go @@ -17,6 +17,6 @@ webhooks: caBundle: {{ .CABundle }} rules: - operations: [ "CREATE" , "UPDATE" ] - apiGroups: ["apps", "extensions"] - apiVersions: ["v1", "v1beta1", "v1beta2"] - resources: ["deployments"]` + apiGroups: [""] + apiVersions: ["v1"] + resources: ["pods"]` diff --git a/controller/proxy-injector/webhook.go b/controller/proxy-injector/webhook.go index 878e5730e..b493e4eb3 100644 --- a/controller/proxy-injector/webhook.go +++ b/controller/proxy-injector/webhook.go @@ -4,16 +4,17 @@ import ( "fmt" pb "github.com/linkerd/linkerd2/controller/gen/config" + "github.com/linkerd/linkerd2/controller/k8s" "github.com/linkerd/linkerd2/pkg/config" "github.com/linkerd/linkerd2/pkg/inject" - "github.com/linkerd/linkerd2/pkg/k8s" + pkgK8s "github.com/linkerd/linkerd2/pkg/k8s" "github.com/linkerd/linkerd2/pkg/version" 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" - "k8s.io/client-go/kubernetes" "sigs.k8s.io/yaml" ) @@ -21,21 +22,21 @@ import ( // requests by injecting sidecar container spec into the pod spec during pod // creation. type Webhook struct { - client kubernetes.Interface + k8sAPI *k8s.API deserializer runtime.Decoder controllerNamespace string noInitContainer bool } // NewWebhook returns a new instance of Webhook. -func NewWebhook(client kubernetes.Interface, controllerNamespace string, noInitContainer bool) (*Webhook, error) { +func NewWebhook(api *k8s.API, controllerNamespace string, noInitContainer bool) (*Webhook, error) { var ( scheme = runtime.NewScheme() codecs = serializer.NewCodecFactory(scheme) ) return &Webhook{ - client: client, + k8sAPI: api, deserializer: codecs.UniversalDeserializer(), controllerNamespace: controllerNamespace, noInitContainer: noInitContainer, @@ -87,17 +88,17 @@ func (w *Webhook) decode(data []byte) (*admissionv1beta1.AdmissionReview, error) func (w *Webhook) inject(request *admissionv1beta1.AdmissionRequest) (*admissionv1beta1.AdmissionResponse, error) { log.Debugf("request object bytes: %s", request.Object.Raw) - globalConfig, err := config.Global(k8s.MountPathGlobalConfig) + globalConfig, err := config.Global(pkgK8s.MountPathGlobalConfig) if err != nil { return nil, err } - proxyConfig, err := config.Proxy(k8s.MountPathProxyConfig) + proxyConfig, err := config.Proxy(pkgK8s.MountPathProxyConfig) if err != nil { return nil, err } - namespace, err := w.client.CoreV1().Namespaces().Get(request.Namespace, metav1.GetOptions{}) + namespace, err := w.k8sAPI.NS().Lister().Get(request.Namespace) if err != nil { return nil, err } @@ -105,6 +106,7 @@ func (w *Webhook) inject(request *admissionv1beta1.AdmissionRequest) (*admission configs := &pb.All{Global: globalConfig, Proxy: proxyConfig} conf := inject.NewResourceConfig(configs). + WithOwnerRetriever(w.ownerRetriever(request.Namespace)). WithNsAnnotations(nsAnnotations). WithKind(request.Kind.Kind) nonEmpty, err := conf.ParseMeta(request.Object.Raw) @@ -120,7 +122,7 @@ func (w *Webhook) inject(request *admissionv1beta1.AdmissionRequest) (*admission return admissionResponse, nil } - p, _, err := conf.GetPatch(request.Object.Raw, inject.ShouldInjectWebhook) + p, reports, err := conf.GetPatch(request.Object.Raw, inject.ShouldInjectWebhook) if err != nil { return nil, err } @@ -131,17 +133,15 @@ func (w *Webhook) inject(request *admissionv1beta1.AdmissionRequest) (*admission p.AddCreatedByPodAnnotation(fmt.Sprintf("linkerd/proxy-injector %s", version.Version)) - // When adding workloads through `kubectl apply` the spec template labels are - // automatically copied to the workload's main metadata section. - // This doesn't happen when adding labels through the webhook. So we manually - // add them to remain consistent. - conf.AddRootLabels(p) - patchJSON, err := p.Marshal() if err != nil { return nil, err } - log.Infof("patch generated for: %s", conf) + // TODO: refactor GetPatch() so it only returns one report item + if len(reports) > 0 { + r := reports[0] + log.Infof("patch generated for: %s", r.ResName()) + } log.Debugf("patch: %s", patchJSON) patchType := admissionv1beta1.PatchTypeJSONPatch @@ -150,3 +150,10 @@ func (w *Webhook) inject(request *admissionv1beta1.AdmissionRequest) (*admission return admissionResponse, nil } + +func (w *Webhook) ownerRetriever(ns string) inject.OwnerRetrieverFunc { + return func(p *v1.Pod) (string, string) { + p.SetNamespace(ns) + return w.k8sAPI.GetOwnerKindAndName(p) + } +} diff --git a/controller/proxy-injector/webhook_config.go b/controller/proxy-injector/webhook_config.go index 7a1bb40db..bcaa02c7e 100644 --- a/controller/proxy-injector/webhook_config.go +++ b/controller/proxy-injector/webhook_config.go @@ -5,6 +5,7 @@ import ( "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" @@ -12,7 +13,6 @@ import ( arv1beta1 "k8s.io/api/admissionregistration/v1beta1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" "sigs.k8s.io/yaml" ) @@ -22,11 +22,11 @@ type WebhookConfig struct { webhookServiceName string trustAnchor []byte configTemplate *template.Template - k8sAPI kubernetes.Interface + k8sAPI *k8s.API } // NewWebhookConfig returns a new instance of initiator. -func NewWebhookConfig(client kubernetes.Interface, controllerNamespace, webhookServiceName string, rootCA *tls.CA) (*WebhookConfig, error) { +func NewWebhookConfig(api *k8s.API, controllerNamespace, webhookServiceName string, rootCA *tls.CA) (*WebhookConfig, error) { trustAnchor := rootCA.Cred.EncodeCertificatePEM() t := template.New(k8sPkg.ProxyInjectorWebhookConfig) @@ -36,7 +36,7 @@ func NewWebhookConfig(client kubernetes.Interface, controllerNamespace, webhookS webhookServiceName: webhookServiceName, trustAnchor: []byte(trustAnchor), configTemplate: template.Must(t.Parse(tmpl.MutatingWebhookConfigurationSpec)), - k8sAPI: client, + k8sAPI: api, }, nil } @@ -97,15 +97,18 @@ func (w *WebhookConfig) create() (*arv1beta1.MutatingWebhookConfiguration, error return nil, err } - return w.k8sAPI.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Create(&config) + return w.k8sAPI.Client. + AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Create(&config) } func (w *WebhookConfig) get() (*arv1beta1.MutatingWebhookConfiguration, error) { - return w.k8sAPI.AdmissionregistrationV1beta1().MutatingWebhookConfigurations(). + return w.k8sAPI.Client. + AdmissionregistrationV1beta1().MutatingWebhookConfigurations(). Get(k8sPkg.ProxyInjectorWebhookConfig, metav1.GetOptions{}) } func (w *WebhookConfig) delete() error { - return w.k8sAPI.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Delete( - k8sPkg.ProxyInjectorWebhookConfig, &metav1.DeleteOptions{}) + 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 index c7b5cec65..b38f9af49 100644 --- a/controller/proxy-injector/webhook_config_test.go +++ b/controller/proxy-injector/webhook_config_test.go @@ -5,6 +5,7 @@ import ( "log" "testing" + "github.com/linkerd/linkerd2/controller/k8s" "github.com/linkerd/linkerd2/controller/proxy-injector/fake" "github.com/linkerd/linkerd2/pkg/tls" ) @@ -16,14 +17,17 @@ func TestCreate(t *testing.T) { ) log.SetOutput(ioutil.Discard) - client := fake.NewClient("") + 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(client, namespace, webhookServiceName, rootCA) + webhookConfig, err := NewWebhookConfig(k8sAPI, namespace, webhookServiceName, rootCA) if err != nil { t.Fatal("Unexpected error: ", err) } diff --git a/controller/proxy-injector/webhook_test.go b/controller/proxy-injector/webhook_test.go index cb94cabb1..0c1810ac1 100644 --- a/controller/proxy-injector/webhook_test.go +++ b/controller/proxy-injector/webhook_test.go @@ -7,7 +7,7 @@ import ( "github.com/linkerd/linkerd2/controller/gen/config" "github.com/linkerd/linkerd2/controller/proxy-injector/fake" "github.com/linkerd/linkerd2/pkg/inject" - "github.com/linkerd/linkerd2/pkg/k8s" + pkgK8s "github.com/linkerd/linkerd2/pkg/k8s" admissionv1beta1 "k8s.io/api/admission/v1beta1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -42,7 +42,7 @@ var ( func confNsEnabled() *inject.ResourceConfig { return inject. NewResourceConfig(configs). - WithNsAnnotations(map[string]string{k8s.ProxyInjectAnnotation: k8s.ProxyInjectEnabled}) + WithNsAnnotations(map[string]string{pkgK8s.ProxyInjectAnnotation: pkgK8s.ProxyInjectEnabled}) } func confNsDisabled() *inject.ResourceConfig { diff --git a/pkg/inject/inject.go b/pkg/inject/inject.go index aa931802a..5f1a6a9d1 100644 --- a/pkg/inject/inject.go +++ b/pkg/inject/inject.go @@ -73,6 +73,10 @@ var injectableKinds = []string{ k8s.StatefulSet, } +// OwnerRetrieverFunc is a function that returns a pod's owner reference +// kind and name +type OwnerRetrieverFunc func(*v1.Pod) (string, string) + // ResourceConfig contains the parsed information for a given workload type ResourceConfig struct { configs *config.All @@ -80,6 +84,7 @@ type ResourceConfig struct { destinationDNSOverride string identityDNSOverride string proxyOutboundCapacity map[string]uint + ownerRetriever OwnerRetrieverFunc workload struct { obj runtime.Object @@ -108,20 +113,6 @@ func NewResourceConfig(configs *config.All) *ResourceConfig { return config } -// String satisfies the Stringer interface -func (conf *ResourceConfig) String() string { - l := []string{} - - if conf.workload.metaType.Kind != "" { - l = append(l, conf.workload.metaType.Kind) - } - if conf.workload.meta != nil { - l = append(l, fmt.Sprintf("%s.%s", conf.workload.meta.GetName(), conf.workload.meta.GetNamespace())) - } - - return strings.Join(l, "/") -} - // WithKind enriches ResourceConfig with the workload kind func (conf *ResourceConfig) WithKind(kind string) *ResourceConfig { conf.workload.metaType = metav1.TypeMeta{Kind: kind} @@ -143,6 +134,13 @@ func (conf *ResourceConfig) WithProxyOutboundCapacity(m map[string]uint) *Resour return conf } +// WithOwnerRetriever enriches ResourceConfig with a function that allows to retrieve +// the kind and name of the workload's owner reference +func (conf *ResourceConfig) WithOwnerRetriever(f OwnerRetrieverFunc) *ResourceConfig { + conf.ownerRetriever = f + return conf +} + // YamlMarshalObj returns the yaml for the workload in conf func (conf *ResourceConfig) YamlMarshalObj() ([]byte, error) { return yaml.Marshal(conf.workload.obj) @@ -158,7 +156,7 @@ func (conf *ResourceConfig) ParseMetaAndYaml(bytes []byte) (*Report, error) { } // ParseMeta extracts metadata from bytes. -// It returns false if the workload's payload is empty +// It returns false if the workload's payload is empty. func (conf *ResourceConfig) ParseMeta(bytes []byte) (bool, error) { if err := yaml.Unmarshal(bytes, &conf.workload.metaType); err != nil { return false, err @@ -181,12 +179,7 @@ func (conf *ResourceConfig) GetPatch( return nil, nil, err } - var patch *Patch - if strings.ToLower(conf.workload.metaType.Kind) == k8s.Pod { - patch = NewPatchPod() - } else { - patch = NewPatchDeployment() - } + patch := NewPatch(conf.workload.metaType.Kind) // If we don't inject anything into the pod template then output the // original serialization of the original object. Otherwise, output the @@ -246,6 +239,8 @@ func (conf *ResourceConfig) JSONToYAML(bytes []byte) ([]byte, error) { return yaml.Marshal(obj) } +// parse parses the bytes payload, filling the gaps in ResourceConfig +// depending on the workload kind func (conf *ResourceConfig) parse(bytes []byte) error { // The Kubernetes API is versioned and each version has an API modeled // with its own distinct Go types. If we tell `yaml.Unmarshal()` which @@ -352,6 +347,24 @@ func (conf *ResourceConfig) parse(bytes []byte) error { conf.workload.obj = v conf.pod.spec = &v.Spec conf.pod.Meta = &v.ObjectMeta + + if conf.ownerRetriever != nil { + kind, name := conf.ownerRetriever(v) + switch kind { + case k8s.Deployment: + conf.pod.labels[k8s.ProxyDeploymentLabel] = name + case k8s.ReplicationController: + conf.pod.labels[k8s.ProxyReplicationControllerLabel] = name + case k8s.ReplicaSet: + conf.pod.labels[k8s.ProxyReplicaSetLabel] = name + case k8s.Job: + conf.pod.labels[k8s.ProxyJobLabel] = name + case k8s.DaemonSet: + conf.pod.labels[k8s.ProxyDaemonSetLabel] = name + case k8s.StatefulSet: + conf.pod.labels[k8s.ProxyStatefulSetLabel] = name + } + } } return nil @@ -364,8 +377,9 @@ func (conf *ResourceConfig) complete(template *v1.PodTemplateSpec) { // injectPodSpec adds linkerd sidecars to the provided PodSpec. func (conf *ResourceConfig) injectPodSpec(patch *Patch) { + saVolumeMount := conf.serviceAccountVolumeMount() if !conf.configs.GetGlobal().GetCniEnabled() { - conf.injectProxyInit(patch) + conf.injectProxyInit(patch, saVolumeMount) } proxyUID := conf.proxyUID() @@ -455,6 +469,10 @@ func (conf *ResourceConfig) injectPodSpec(patch *Patch) { } } + if saVolumeMount != nil { + sidecar.VolumeMounts = []v1.VolumeMount{*saVolumeMount} + } + idctx := conf.configs.GetGlobal().GetIdentityContext() if idctx == nil { sidecar.Env = append(sidecar.Env, v1.EnvVar{ @@ -527,7 +545,7 @@ func (conf *ResourceConfig) injectPodSpec(patch *Patch) { patch.addContainer(&sidecar) } -func (conf *ResourceConfig) injectProxyInit(patch *Patch) { +func (conf *ResourceConfig) injectProxyInit(patch *Patch, saVolumeMount *v1.VolumeMount) { nonRoot := false runAsUser := int64(0) initContainer := &v1.Container{ @@ -545,12 +563,28 @@ func (conf *ResourceConfig) injectProxyInit(patch *Patch) { RunAsUser: &runAsUser, }, } + if saVolumeMount != nil { + initContainer.VolumeMounts = []v1.VolumeMount{*saVolumeMount} + } if len(conf.pod.spec.InitContainers) == 0 { patch.addInitContainerRoot() } patch.addInitContainer(initContainer) } +func (conf *ResourceConfig) serviceAccountVolumeMount() *v1.VolumeMount { + // Probably always true, but wanna be super-safe + if containers := conf.pod.spec.Containers; len(containers) > 0 { + for _, vm := range containers[0].VolumeMounts { + if vm.MountPath == k8s.MountPathServiceAccount { + vm := vm // pin + return &vm + } + } + } + return nil +} + // Given a ObjectMeta, update ObjectMeta in place with the new labels and // annotations. func (conf *ResourceConfig) injectObjectMeta(patch *Patch) { @@ -565,15 +599,13 @@ func (conf *ResourceConfig) injectObjectMeta(patch *Patch) { patch.addPodAnnotation(k8s.IdentityModeAnnotation, k8s.IdentityModeDisabled) } - for k, v := range conf.pod.labels { - patch.addPodLabel(k, v) - } -} - -// AddRootLabels adds all the pod labels into the root workload (e.g. Deployment) -func (conf *ResourceConfig) AddRootLabels(patch *Patch) { - for k, v := range conf.pod.labels { - patch.addRootLabel(k, v) + if len(conf.pod.labels) > 0 { + if len(conf.pod.Meta.Labels) == 0 { + patch.addPodLabelsRoot() + } + for k, v := range conf.pod.labels { + patch.addPodLabel(k, v) + } } } diff --git a/pkg/inject/inject_test.go b/pkg/inject/inject_test.go index f41796fde..c7f416102 100644 --- a/pkg/inject/inject_test.go +++ b/pkg/inject/inject_test.go @@ -227,7 +227,7 @@ func TestConfigAccessors(t *testing.T) { t.Fatal(err) } - if err := resourceConfig.parse(data); err != nil { + if _, err := resourceConfig.ParseMetaAndYaml(data); err != nil { t.Fatal(err) } diff --git a/pkg/inject/patch.go b/pkg/inject/patch.go index e84d1d669..99f26512c 100644 --- a/pkg/inject/patch.go +++ b/pkg/inject/patch.go @@ -24,8 +24,21 @@ type Patch struct { patchPathPodAnnotations string } -// NewPatchDeployment returns a new instance of Patch for Deployment-like workloads -func NewPatchDeployment() *Patch { +// NewPatch returns a new instance of Patch for any kind of workload kind +func NewPatch(kind string) *Patch { + if strings.ToLower(kind) == k8s.Pod { + return &Patch{ + patchOps: []*patchOp{}, + patchPathContainer: "/spec/containers/-", + patchPathInitContainerRoot: "/spec/initContainers", + patchPathInitContainer: "/spec/initContainers/-", + patchPathVolumeRoot: "/spec/volumes", + patchPathVolume: "/spec/volumes/-", + patchPathPodLabels: patchPathRootLabels, + patchPathPodAnnotations: "/metadata/annotations", + } + } + return &Patch{ patchOps: []*patchOp{}, patchPathContainer: "/spec/template/spec/containers/-", @@ -38,20 +51,6 @@ func NewPatchDeployment() *Patch { } } -// NewPatchPod returns a new instance of Patch for Pod workloads -func NewPatchPod() *Patch { - return &Patch{ - patchOps: []*patchOp{}, - patchPathContainer: "/spec/containers/-", - patchPathInitContainerRoot: "/spec/initContainers", - patchPathInitContainer: "/spec/initContainers/-", - patchPathVolumeRoot: "/spec/volumes", - patchPathVolume: "/spec/volumes/-", - patchPathPodLabels: patchPathRootLabels, - patchPathPodAnnotations: "/metadata/annotations", - } -} - // Marshal returns the patch as JSON func (p *Patch) Marshal() ([]byte, error) { return json.Marshal(p.patchOps) @@ -97,6 +96,15 @@ func (p *Patch) addVolume(volume *corev1.Volume) { }) } +func (p *Patch) addPodLabelsRoot() { + p.patchOps = append(p.patchOps, &patchOp{ + Op: "add", + Path: p.patchPathPodLabels, + Value: map[string]string{}, + }) +} + +// AddPodLabel appends the label with the provided key and value func (p *Patch) addPodLabel(key, value string) { p.patchOps = append(p.patchOps, &patchOp{ Op: "add", @@ -105,14 +113,6 @@ func (p *Patch) addPodLabel(key, value string) { }) } -func (p *Patch) addRootLabel(key, value string) { - p.patchOps = append(p.patchOps, &patchOp{ - Op: "add", - Path: patchPathRootLabels + "/" + escapeKey(key), - Value: value, - }) -} - func (p *Patch) addPodAnnotationsRoot() { p.patchOps = append(p.patchOps, &patchOp{ Op: "add", diff --git a/pkg/inject/patch_test.go b/pkg/inject/patch_test.go index 13381bf24..0de199214 100644 --- a/pkg/inject/patch_test.go +++ b/pkg/inject/patch_test.go @@ -13,11 +13,10 @@ import ( func TestPatch(t *testing.T) { fixture := fake.NewFactory(filepath.Join("..", "..", "controller", "proxy-injector", "fake", "data")) - // TODO end entity volume - // secrets, err := fixture.Volume("inject-linkerd-secrets-volume-spec.yaml") - // if err != nil { - // t.Fatal("Unexpected error: ", err) - // } + secrets, err := fixture.Volume("inject-linkerd-secrets-volume-spec.yaml") + if err != nil { + t.Fatal("Unexpected error: ", err) + } sidecar, err := fixture.Container("inject-sidecar-container-spec.yaml") if err != nil { @@ -34,22 +33,22 @@ func TestPatch(t *testing.T) { createdBy = "linkerd/cli v18.8.4" ) - actual := NewPatchDeployment() + actual := NewPatch(k8sPkg.Deployment) actual.addContainer(sidecar) actual.addInitContainerRoot() actual.addInitContainer(init) - //actual.addVolumeRoot() - //actual.addVolume(secrets) + actual.addVolumeRoot() + actual.addVolume(secrets) actual.addPodLabel(k8sPkg.ControllerNSLabel, controllerNamespace) actual.addPodAnnotation(k8sPkg.CreatedByAnnotation, createdBy) - expected := NewPatchDeployment() + expected := NewPatch(k8sPkg.Deployment) expected.patchOps = []*patchOp{ {Op: "add", Path: expected.patchPathContainer, Value: sidecar}, {Op: "add", Path: expected.patchPathInitContainerRoot, Value: []*v1.Container{}}, {Op: "add", Path: expected.patchPathInitContainer, Value: init}, - //{Op: "add", Path: expected.patchPathVolumeRoot, Value: []*v1.Volume{}}, - //{Op: "add", Path: expected.patchPathVolume, Value: secrets}, + {Op: "add", Path: expected.patchPathVolumeRoot, Value: []*v1.Volume{}}, + {Op: "add", Path: expected.patchPathVolume, Value: secrets}, {Op: "add", Path: expected.patchPathPodLabels + "/" + escapeKey(k8sPkg.ControllerNSLabel), Value: controllerNamespace}, {Op: "add", Path: expected.patchPathPodAnnotations + "/" + escapeKey(k8sPkg.CreatedByAnnotation), Value: createdBy}, } diff --git a/pkg/inject/report.go b/pkg/inject/report.go index 7a0a88cc4..935265797 100644 --- a/pkg/inject/report.go +++ b/pkg/inject/report.go @@ -27,7 +27,11 @@ func newReport(conf *ResourceConfig) Report { var name string if m := conf.pod.Meta; m != nil { name = m.Name + if name == "" { + name = m.GenerateName + } } + return Report{ Kind: strings.ToLower(conf.workload.metaType.Kind), Name: name, @@ -35,13 +39,13 @@ func newReport(conf *ResourceConfig) Report { } // ResName returns a string "Kind/Name" for the workload referred in the report r -func (r Report) ResName() string { +func (r *Report) ResName() string { return fmt.Sprintf("%s/%s", r.Kind, r.Name) } // Injectable returns false if the report flags indicate that the workload is on a host network // or there is already a sidecar or the resource is not supported or inject is explicitly disabled -func (r Report) Injectable() bool { +func (r *Report) Injectable() bool { return !r.HostNetwork && !r.Sidecar && !r.UnsupportedResource && !r.InjectDisabled } diff --git a/pkg/k8s/labels.go b/pkg/k8s/labels.go index 5828b5ac8..1ee92c20b 100644 --- a/pkg/k8s/labels.go +++ b/pkg/k8s/labels.go @@ -190,6 +190,10 @@ const ( // MountPathBase is the base directory of the mount path. MountPathBase = "/var/run/linkerd" + // MountPathServiceAccount is the default path where Kuberenetes stores + // the service account token + MountPathServiceAccount = "/var/run/secrets/kubernetes.io/serviceaccount" + // MountPathGlobalConfig is the path at which the global config file is mounted. MountPathGlobalConfig = MountPathBase + "/config/global"