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 <alejandro@buoyant.io>
This commit is contained in:
Alejandro Pedraza 2019-03-26 11:53:56 -05:00 committed by GitHub
parent 9c442f6885
commit 7efe385feb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 205 additions and 121 deletions

View File

@ -80,7 +80,13 @@ rules:
verbs: ["create", "get", "delete"] verbs: ["create", "get", "delete"]
- apiGroups: [""] - apiGroups: [""]
resources: ["namespaces"] resources: ["namespaces"]
verbs: ["get"] verbs: ["list", "get", "watch"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["list"]
- apiGroups: ["apps"]
resources: ["replicasets"]
verbs: ["list", "get", "watch"]
--- ---
kind: ClusterRoleBinding kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1 apiVersion: rbac.authorization.k8s.io/v1

View File

@ -142,7 +142,11 @@ func (rt resourceTransformerInject) transform(bytes []byte) ([]byte, []inject.Re
if patchJSON == nil { if patchJSON == nil {
return bytes, reports, 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) log.Debugf("patch: %s", patchJSON)
patch, err := jsonpatch.DecodePatch(patchJSON) patch, err := jsonpatch.DecodePatch(patchJSON)
if err != nil { if err != nil {

View File

@ -1491,7 +1491,13 @@ rules:
verbs: ["create", "get", "delete"] verbs: ["create", "get", "delete"]
- apiGroups: [""] - apiGroups: [""]
resources: ["namespaces"] resources: ["namespaces"]
verbs: ["get"] verbs: ["list", "get", "watch"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["list"]
- apiGroups: ["apps"]
resources: ["replicasets"]
verbs: ["list", "get", "watch"]
--- ---
kind: ClusterRoleBinding kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1 apiVersion: rbac.authorization.k8s.io/v1

View File

@ -1424,7 +1424,13 @@ rules:
verbs: ["create", "get", "delete"] verbs: ["create", "get", "delete"]
- apiGroups: [""] - apiGroups: [""]
resources: ["namespaces"] resources: ["namespaces"]
verbs: ["get"] verbs: ["list", "get", "watch"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["list"]
- apiGroups: ["apps"]
resources: ["replicasets"]
verbs: ["list", "get", "watch"]
--- ---
kind: ClusterRoleBinding kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1 apiVersion: rbac.authorization.k8s.io/v1

View File

@ -27,9 +27,9 @@ func main() {
defer close(stop) defer close(stop)
signal.Notify(stop, os.Interrupt, os.Kill) signal.Notify(stop, os.Interrupt, os.Kill)
k8sClient, err := k8s.NewClientSet(*kubeconfig) k8sAPI, err := k8s.InitializeAPI(*kubeconfig, k8s.NS, k8s.RS)
if err != nil { 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") 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) 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 { if err != nil {
log.Fatalf("failed to read the trust anchor file: %s", err) 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) 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 { if err != nil {
log.Fatalf("failed to initialize the webhook server: %s", err) log.Fatalf("failed to initialize the webhook server: %s", err)
} }
k8sAPI.Sync()
go func() { go func() {
log.Infof("listening at %s", *addr) log.Infof("listening at %s", *addr)
if err := s.ListenAndServeTLS("", ""); err != nil { if err := s.ListenAndServeTLS("", ""); err != nil {

View File

@ -0,0 +1,4 @@
name: linkerd-secrets
secret:
secretName: nginx-deployment-tls-linkerd-io
optional: true

View File

@ -8,9 +8,9 @@ import (
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"github.com/linkerd/linkerd2/controller/k8s"
pkgTls "github.com/linkerd/linkerd2/pkg/tls" pkgTls "github.com/linkerd/linkerd2/pkg/tls"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"k8s.io/client-go/kubernetes"
) )
// WebhookServer is the webhook's HTTP server. It has an embedded webhook which // 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. // 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) c, err := tlsConfig(rootCA, controllerNamespace)
if err != nil { if err != nil {
return nil, err return nil, err
@ -32,7 +32,7 @@ func NewWebhookServer(client kubernetes.Interface, addr, controllerNamespace str
TLSConfig: c, TLSConfig: c,
} }
webhook, err := NewWebhook(client, controllerNamespace, noInitContainer) webhook, err := NewWebhook(api, controllerNamespace, noInitContainer)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -9,6 +9,7 @@ import (
"reflect" "reflect"
"testing" "testing"
"github.com/linkerd/linkerd2/controller/k8s"
"github.com/linkerd/linkerd2/controller/proxy-injector/fake" "github.com/linkerd/linkerd2/controller/proxy-injector/fake"
"github.com/linkerd/linkerd2/pkg/tls" "github.com/linkerd/linkerd2/pkg/tls"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@ -20,9 +21,12 @@ var (
func init() { func init() {
// create a webhook which uses its fake client to seed the sidecar configmap // 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 { if err != nil {
panic(err) panic(err)
} }
@ -73,13 +77,12 @@ func TestNewWebhookServer(t *testing.T) {
log.Fatalf("failed to create root CA: %s", err) log.Fatalf("failed to create root CA: %s", err)
} }
var ( addr := ":7070"
addr = ":7070" k8sAPI, err := k8s.NewFakeAPI()
kubeconfig = "" if err != nil {
) t.Fatalf("NewFakeAPI returned an error: %s", err)
fakeClient := fake.NewClient(kubeconfig) }
server, err := NewWebhookServer(k8sAPI, addr, fake.DefaultControllerNamespace, false, rootCA)
server, err := NewWebhookServer(fakeClient, addr, fake.DefaultControllerNamespace, false, rootCA)
if err != nil { if err != nil {
t.Fatal("Unexpected error: ", err) t.Fatal("Unexpected error: ", err)
} }

View File

@ -17,6 +17,6 @@ webhooks:
caBundle: {{ .CABundle }} caBundle: {{ .CABundle }}
rules: rules:
- operations: [ "CREATE" , "UPDATE" ] - operations: [ "CREATE" , "UPDATE" ]
apiGroups: ["apps", "extensions"] apiGroups: [""]
apiVersions: ["v1", "v1beta1", "v1beta2"] apiVersions: ["v1"]
resources: ["deployments"]` resources: ["pods"]`

View File

@ -4,16 +4,17 @@ import (
"fmt" "fmt"
pb "github.com/linkerd/linkerd2/controller/gen/config" 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/config"
"github.com/linkerd/linkerd2/pkg/inject" "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" "github.com/linkerd/linkerd2/pkg/version"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
admissionv1beta1 "k8s.io/api/admission/v1beta1" admissionv1beta1 "k8s.io/api/admission/v1beta1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/client-go/kubernetes"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
) )
@ -21,21 +22,21 @@ import (
// requests by injecting sidecar container spec into the pod spec during pod // requests by injecting sidecar container spec into the pod spec during pod
// creation. // creation.
type Webhook struct { type Webhook struct {
client kubernetes.Interface k8sAPI *k8s.API
deserializer runtime.Decoder deserializer runtime.Decoder
controllerNamespace string controllerNamespace string
noInitContainer bool noInitContainer bool
} }
// NewWebhook returns a new instance of Webhook. // 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 ( var (
scheme = runtime.NewScheme() scheme = runtime.NewScheme()
codecs = serializer.NewCodecFactory(scheme) codecs = serializer.NewCodecFactory(scheme)
) )
return &Webhook{ return &Webhook{
client: client, k8sAPI: api,
deserializer: codecs.UniversalDeserializer(), deserializer: codecs.UniversalDeserializer(),
controllerNamespace: controllerNamespace, controllerNamespace: controllerNamespace,
noInitContainer: noInitContainer, 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) { func (w *Webhook) inject(request *admissionv1beta1.AdmissionRequest) (*admissionv1beta1.AdmissionResponse, error) {
log.Debugf("request object bytes: %s", request.Object.Raw) log.Debugf("request object bytes: %s", request.Object.Raw)
globalConfig, err := config.Global(k8s.MountPathGlobalConfig) globalConfig, err := config.Global(pkgK8s.MountPathGlobalConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }
proxyConfig, err := config.Proxy(k8s.MountPathProxyConfig) proxyConfig, err := config.Proxy(pkgK8s.MountPathProxyConfig)
if err != nil { if err != nil {
return nil, err 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 { if err != nil {
return nil, err return nil, err
} }
@ -105,6 +106,7 @@ func (w *Webhook) inject(request *admissionv1beta1.AdmissionRequest) (*admission
configs := &pb.All{Global: globalConfig, Proxy: proxyConfig} configs := &pb.All{Global: globalConfig, Proxy: proxyConfig}
conf := inject.NewResourceConfig(configs). conf := inject.NewResourceConfig(configs).
WithOwnerRetriever(w.ownerRetriever(request.Namespace)).
WithNsAnnotations(nsAnnotations). WithNsAnnotations(nsAnnotations).
WithKind(request.Kind.Kind) WithKind(request.Kind.Kind)
nonEmpty, err := conf.ParseMeta(request.Object.Raw) nonEmpty, err := conf.ParseMeta(request.Object.Raw)
@ -120,7 +122,7 @@ func (w *Webhook) inject(request *admissionv1beta1.AdmissionRequest) (*admission
return admissionResponse, nil 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 { if err != nil {
return nil, err 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)) 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() patchJSON, err := p.Marshal()
if err != nil { if err != nil {
return nil, err 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) log.Debugf("patch: %s", patchJSON)
patchType := admissionv1beta1.PatchTypeJSONPatch patchType := admissionv1beta1.PatchTypeJSONPatch
@ -150,3 +150,10 @@ func (w *Webhook) inject(request *admissionv1beta1.AdmissionRequest) (*admission
return admissionResponse, nil 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)
}
}

View File

@ -5,6 +5,7 @@ import (
"encoding/base64" "encoding/base64"
"text/template" "text/template"
"github.com/linkerd/linkerd2/controller/k8s"
"github.com/linkerd/linkerd2/controller/proxy-injector/tmpl" "github.com/linkerd/linkerd2/controller/proxy-injector/tmpl"
k8sPkg "github.com/linkerd/linkerd2/pkg/k8s" k8sPkg "github.com/linkerd/linkerd2/pkg/k8s"
"github.com/linkerd/linkerd2/pkg/tls" "github.com/linkerd/linkerd2/pkg/tls"
@ -12,7 +13,6 @@ import (
arv1beta1 "k8s.io/api/admissionregistration/v1beta1" arv1beta1 "k8s.io/api/admissionregistration/v1beta1"
apierrors "k8s.io/apimachinery/pkg/api/errors" apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
) )
@ -22,11 +22,11 @@ type WebhookConfig struct {
webhookServiceName string webhookServiceName string
trustAnchor []byte trustAnchor []byte
configTemplate *template.Template configTemplate *template.Template
k8sAPI kubernetes.Interface k8sAPI *k8s.API
} }
// NewWebhookConfig returns a new instance of initiator. // 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() trustAnchor := rootCA.Cred.EncodeCertificatePEM()
t := template.New(k8sPkg.ProxyInjectorWebhookConfig) t := template.New(k8sPkg.ProxyInjectorWebhookConfig)
@ -36,7 +36,7 @@ func NewWebhookConfig(client kubernetes.Interface, controllerNamespace, webhookS
webhookServiceName: webhookServiceName, webhookServiceName: webhookServiceName,
trustAnchor: []byte(trustAnchor), trustAnchor: []byte(trustAnchor),
configTemplate: template.Must(t.Parse(tmpl.MutatingWebhookConfigurationSpec)), configTemplate: template.Must(t.Parse(tmpl.MutatingWebhookConfigurationSpec)),
k8sAPI: client, k8sAPI: api,
}, nil }, nil
} }
@ -97,15 +97,18 @@ func (w *WebhookConfig) create() (*arv1beta1.MutatingWebhookConfiguration, error
return nil, err 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) { func (w *WebhookConfig) get() (*arv1beta1.MutatingWebhookConfiguration, error) {
return w.k8sAPI.AdmissionregistrationV1beta1().MutatingWebhookConfigurations(). return w.k8sAPI.Client.
AdmissionregistrationV1beta1().MutatingWebhookConfigurations().
Get(k8sPkg.ProxyInjectorWebhookConfig, metav1.GetOptions{}) Get(k8sPkg.ProxyInjectorWebhookConfig, metav1.GetOptions{})
} }
func (w *WebhookConfig) delete() error { func (w *WebhookConfig) delete() error {
return w.k8sAPI.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Delete( return w.k8sAPI.Client.
k8sPkg.ProxyInjectorWebhookConfig, &metav1.DeleteOptions{}) AdmissionregistrationV1beta1().MutatingWebhookConfigurations().
Delete(k8sPkg.ProxyInjectorWebhookConfig, &metav1.DeleteOptions{})
} }

View File

@ -5,6 +5,7 @@ import (
"log" "log"
"testing" "testing"
"github.com/linkerd/linkerd2/controller/k8s"
"github.com/linkerd/linkerd2/controller/proxy-injector/fake" "github.com/linkerd/linkerd2/controller/proxy-injector/fake"
"github.com/linkerd/linkerd2/pkg/tls" "github.com/linkerd/linkerd2/pkg/tls"
) )
@ -16,14 +17,17 @@ func TestCreate(t *testing.T) {
) )
log.SetOutput(ioutil.Discard) 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") rootCA, err := tls.GenerateRootCAWithDefaults("Test CA")
if err != nil { if err != nil {
t.Fatalf("failed to create root CA: %s", err) 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 { if err != nil {
t.Fatal("Unexpected error: ", err) t.Fatal("Unexpected error: ", err)
} }

View File

@ -7,7 +7,7 @@ import (
"github.com/linkerd/linkerd2/controller/gen/config" "github.com/linkerd/linkerd2/controller/gen/config"
"github.com/linkerd/linkerd2/controller/proxy-injector/fake" "github.com/linkerd/linkerd2/controller/proxy-injector/fake"
"github.com/linkerd/linkerd2/pkg/inject" "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" admissionv1beta1 "k8s.io/api/admission/v1beta1"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -42,7 +42,7 @@ var (
func confNsEnabled() *inject.ResourceConfig { func confNsEnabled() *inject.ResourceConfig {
return inject. return inject.
NewResourceConfig(configs). NewResourceConfig(configs).
WithNsAnnotations(map[string]string{k8s.ProxyInjectAnnotation: k8s.ProxyInjectEnabled}) WithNsAnnotations(map[string]string{pkgK8s.ProxyInjectAnnotation: pkgK8s.ProxyInjectEnabled})
} }
func confNsDisabled() *inject.ResourceConfig { func confNsDisabled() *inject.ResourceConfig {

View File

@ -73,6 +73,10 @@ var injectableKinds = []string{
k8s.StatefulSet, 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 // ResourceConfig contains the parsed information for a given workload
type ResourceConfig struct { type ResourceConfig struct {
configs *config.All configs *config.All
@ -80,6 +84,7 @@ type ResourceConfig struct {
destinationDNSOverride string destinationDNSOverride string
identityDNSOverride string identityDNSOverride string
proxyOutboundCapacity map[string]uint proxyOutboundCapacity map[string]uint
ownerRetriever OwnerRetrieverFunc
workload struct { workload struct {
obj runtime.Object obj runtime.Object
@ -108,20 +113,6 @@ func NewResourceConfig(configs *config.All) *ResourceConfig {
return config 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 // WithKind enriches ResourceConfig with the workload kind
func (conf *ResourceConfig) WithKind(kind string) *ResourceConfig { func (conf *ResourceConfig) WithKind(kind string) *ResourceConfig {
conf.workload.metaType = metav1.TypeMeta{Kind: kind} conf.workload.metaType = metav1.TypeMeta{Kind: kind}
@ -143,6 +134,13 @@ func (conf *ResourceConfig) WithProxyOutboundCapacity(m map[string]uint) *Resour
return conf 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 // YamlMarshalObj returns the yaml for the workload in conf
func (conf *ResourceConfig) YamlMarshalObj() ([]byte, error) { func (conf *ResourceConfig) YamlMarshalObj() ([]byte, error) {
return yaml.Marshal(conf.workload.obj) return yaml.Marshal(conf.workload.obj)
@ -158,7 +156,7 @@ func (conf *ResourceConfig) ParseMetaAndYaml(bytes []byte) (*Report, error) {
} }
// ParseMeta extracts metadata from bytes. // 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) { func (conf *ResourceConfig) ParseMeta(bytes []byte) (bool, error) {
if err := yaml.Unmarshal(bytes, &conf.workload.metaType); err != nil { if err := yaml.Unmarshal(bytes, &conf.workload.metaType); err != nil {
return false, err return false, err
@ -181,12 +179,7 @@ func (conf *ResourceConfig) GetPatch(
return nil, nil, err return nil, nil, err
} }
var patch *Patch patch := NewPatch(conf.workload.metaType.Kind)
if strings.ToLower(conf.workload.metaType.Kind) == k8s.Pod {
patch = NewPatchPod()
} else {
patch = NewPatchDeployment()
}
// If we don't inject anything into the pod template then output the // If we don't inject anything into the pod template then output the
// original serialization of the original object. Otherwise, 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) 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 { func (conf *ResourceConfig) parse(bytes []byte) error {
// The Kubernetes API is versioned and each version has an API modeled // The Kubernetes API is versioned and each version has an API modeled
// with its own distinct Go types. If we tell `yaml.Unmarshal()` which // 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.workload.obj = v
conf.pod.spec = &v.Spec conf.pod.spec = &v.Spec
conf.pod.Meta = &v.ObjectMeta 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 return nil
@ -364,8 +377,9 @@ func (conf *ResourceConfig) complete(template *v1.PodTemplateSpec) {
// injectPodSpec adds linkerd sidecars to the provided PodSpec. // injectPodSpec adds linkerd sidecars to the provided PodSpec.
func (conf *ResourceConfig) injectPodSpec(patch *Patch) { func (conf *ResourceConfig) injectPodSpec(patch *Patch) {
saVolumeMount := conf.serviceAccountVolumeMount()
if !conf.configs.GetGlobal().GetCniEnabled() { if !conf.configs.GetGlobal().GetCniEnabled() {
conf.injectProxyInit(patch) conf.injectProxyInit(patch, saVolumeMount)
} }
proxyUID := conf.proxyUID() 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() idctx := conf.configs.GetGlobal().GetIdentityContext()
if idctx == nil { if idctx == nil {
sidecar.Env = append(sidecar.Env, v1.EnvVar{ sidecar.Env = append(sidecar.Env, v1.EnvVar{
@ -527,7 +545,7 @@ func (conf *ResourceConfig) injectPodSpec(patch *Patch) {
patch.addContainer(&sidecar) patch.addContainer(&sidecar)
} }
func (conf *ResourceConfig) injectProxyInit(patch *Patch) { func (conf *ResourceConfig) injectProxyInit(patch *Patch, saVolumeMount *v1.VolumeMount) {
nonRoot := false nonRoot := false
runAsUser := int64(0) runAsUser := int64(0)
initContainer := &v1.Container{ initContainer := &v1.Container{
@ -545,12 +563,28 @@ func (conf *ResourceConfig) injectProxyInit(patch *Patch) {
RunAsUser: &runAsUser, RunAsUser: &runAsUser,
}, },
} }
if saVolumeMount != nil {
initContainer.VolumeMounts = []v1.VolumeMount{*saVolumeMount}
}
if len(conf.pod.spec.InitContainers) == 0 { if len(conf.pod.spec.InitContainers) == 0 {
patch.addInitContainerRoot() patch.addInitContainerRoot()
} }
patch.addInitContainer(initContainer) 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 // Given a ObjectMeta, update ObjectMeta in place with the new labels and
// annotations. // annotations.
func (conf *ResourceConfig) injectObjectMeta(patch *Patch) { func (conf *ResourceConfig) injectObjectMeta(patch *Patch) {
@ -565,15 +599,13 @@ func (conf *ResourceConfig) injectObjectMeta(patch *Patch) {
patch.addPodAnnotation(k8s.IdentityModeAnnotation, k8s.IdentityModeDisabled) patch.addPodAnnotation(k8s.IdentityModeAnnotation, k8s.IdentityModeDisabled)
} }
for k, v := range conf.pod.labels { if len(conf.pod.labels) > 0 {
patch.addPodLabel(k, v) if len(conf.pod.Meta.Labels) == 0 {
} patch.addPodLabelsRoot()
} }
for k, v := range conf.pod.labels {
// AddRootLabels adds all the pod labels into the root workload (e.g. Deployment) patch.addPodLabel(k, v)
func (conf *ResourceConfig) AddRootLabels(patch *Patch) { }
for k, v := range conf.pod.labels {
patch.addRootLabel(k, v)
} }
} }

View File

@ -227,7 +227,7 @@ func TestConfigAccessors(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
if err := resourceConfig.parse(data); err != nil { if _, err := resourceConfig.ParseMetaAndYaml(data); err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -24,8 +24,21 @@ type Patch struct {
patchPathPodAnnotations string patchPathPodAnnotations string
} }
// NewPatchDeployment returns a new instance of Patch for Deployment-like workloads // NewPatch returns a new instance of Patch for any kind of workload kind
func NewPatchDeployment() *Patch { 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{ return &Patch{
patchOps: []*patchOp{}, patchOps: []*patchOp{},
patchPathContainer: "/spec/template/spec/containers/-", 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 // Marshal returns the patch as JSON
func (p *Patch) Marshal() ([]byte, error) { func (p *Patch) Marshal() ([]byte, error) {
return json.Marshal(p.patchOps) 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) { func (p *Patch) addPodLabel(key, value string) {
p.patchOps = append(p.patchOps, &patchOp{ p.patchOps = append(p.patchOps, &patchOp{
Op: "add", 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() { func (p *Patch) addPodAnnotationsRoot() {
p.patchOps = append(p.patchOps, &patchOp{ p.patchOps = append(p.patchOps, &patchOp{
Op: "add", Op: "add",

View File

@ -13,11 +13,10 @@ import (
func TestPatch(t *testing.T) { func TestPatch(t *testing.T) {
fixture := fake.NewFactory(filepath.Join("..", "..", "controller", "proxy-injector", "fake", "data")) fixture := fake.NewFactory(filepath.Join("..", "..", "controller", "proxy-injector", "fake", "data"))
// TODO end entity volume secrets, err := fixture.Volume("inject-linkerd-secrets-volume-spec.yaml")
// secrets, err := fixture.Volume("inject-linkerd-secrets-volume-spec.yaml") if err != nil {
// if err != nil { t.Fatal("Unexpected error: ", err)
// t.Fatal("Unexpected error: ", err) }
// }
sidecar, err := fixture.Container("inject-sidecar-container-spec.yaml") sidecar, err := fixture.Container("inject-sidecar-container-spec.yaml")
if err != nil { if err != nil {
@ -34,22 +33,22 @@ func TestPatch(t *testing.T) {
createdBy = "linkerd/cli v18.8.4" createdBy = "linkerd/cli v18.8.4"
) )
actual := NewPatchDeployment() actual := NewPatch(k8sPkg.Deployment)
actual.addContainer(sidecar) actual.addContainer(sidecar)
actual.addInitContainerRoot() actual.addInitContainerRoot()
actual.addInitContainer(init) actual.addInitContainer(init)
//actual.addVolumeRoot() actual.addVolumeRoot()
//actual.addVolume(secrets) actual.addVolume(secrets)
actual.addPodLabel(k8sPkg.ControllerNSLabel, controllerNamespace) actual.addPodLabel(k8sPkg.ControllerNSLabel, controllerNamespace)
actual.addPodAnnotation(k8sPkg.CreatedByAnnotation, createdBy) actual.addPodAnnotation(k8sPkg.CreatedByAnnotation, createdBy)
expected := NewPatchDeployment() expected := NewPatch(k8sPkg.Deployment)
expected.patchOps = []*patchOp{ expected.patchOps = []*patchOp{
{Op: "add", Path: expected.patchPathContainer, Value: sidecar}, {Op: "add", Path: expected.patchPathContainer, Value: sidecar},
{Op: "add", Path: expected.patchPathInitContainerRoot, Value: []*v1.Container{}}, {Op: "add", Path: expected.patchPathInitContainerRoot, Value: []*v1.Container{}},
{Op: "add", Path: expected.patchPathInitContainer, Value: init}, {Op: "add", Path: expected.patchPathInitContainer, Value: init},
//{Op: "add", Path: expected.patchPathVolumeRoot, Value: []*v1.Volume{}}, {Op: "add", Path: expected.patchPathVolumeRoot, Value: []*v1.Volume{}},
//{Op: "add", Path: expected.patchPathVolume, Value: secrets}, {Op: "add", Path: expected.patchPathVolume, Value: secrets},
{Op: "add", Path: expected.patchPathPodLabels + "/" + escapeKey(k8sPkg.ControllerNSLabel), Value: controllerNamespace}, {Op: "add", Path: expected.patchPathPodLabels + "/" + escapeKey(k8sPkg.ControllerNSLabel), Value: controllerNamespace},
{Op: "add", Path: expected.patchPathPodAnnotations + "/" + escapeKey(k8sPkg.CreatedByAnnotation), Value: createdBy}, {Op: "add", Path: expected.patchPathPodAnnotations + "/" + escapeKey(k8sPkg.CreatedByAnnotation), Value: createdBy},
} }

View File

@ -27,7 +27,11 @@ func newReport(conf *ResourceConfig) Report {
var name string var name string
if m := conf.pod.Meta; m != nil { if m := conf.pod.Meta; m != nil {
name = m.Name name = m.Name
if name == "" {
name = m.GenerateName
}
} }
return Report{ return Report{
Kind: strings.ToLower(conf.workload.metaType.Kind), Kind: strings.ToLower(conf.workload.metaType.Kind),
Name: name, 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 // 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) 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 // 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 // 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 return !r.HostNetwork && !r.Sidecar && !r.UnsupportedResource && !r.InjectDisabled
} }

View File

@ -190,6 +190,10 @@ const (
// MountPathBase is the base directory of the mount path. // MountPathBase is the base directory of the mount path.
MountPathBase = "/var/run/linkerd" 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 is the path at which the global config file is mounted.
MountPathGlobalConfig = MountPathBase + "/config/global" MountPathGlobalConfig = MountPathBase + "/config/global"