8000 Introduce tap APIService, update `linkerd tap` by siggy · Pull Request #3167 · linkerd/linkerd2 · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 9 additions & 0 deletions BUILD.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,15 @@ You can also send test requests to the destination's discovery interface:
bin/go-run controller/script/discovery-client
```

##### Running the Tap APIService for development

```bash
openssl req -nodes -x509 -newkey rsa:4096 -keyout $HOME/key.pem -out $HOME/crt.pem -subj "/C=US"
bin/go-run controller/cmd/tap --disable-common-names --tls-cert=$HOME/crt.pem --tls-key=$HOME/key.pem

curl -k https://localhost:8089/apis/tap.linkerd.io/v1alpha1
```

#### Generating CLI docs

The [documentation](https://linkerd.io/2/cli/) for the CLI
Expand Down
16 changes: 14 additions & 2 deletions bin/test-cleanup
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,18 @@ if ! customresourcedefinitions=$(kubectl --context=$k8s_context get customresour
echo "no customresourcedefinitions found" >&2
fi

if [[ $namespaces_controlplane || $namespaces_dataplane || $clusterrolebindings || $clusterroles || $webhookconfigs || $validatingconfigs || $podsecuritypolicies || $customresourcedefinitions ]]; then
kubectl --context=$k8s_context delete $namespaces_controlplane $namespaces_dataplane $clusterrolebindings $clusterroles $webhookconfigs $validatingconfigs $podsecuritypolicies $customresourcedefinitions
if ! apiservices=$(kubectl --context=$k8s_context get apiservices -l linkerd.io/control-plane-ns -oname); then
echo "no apiservices found" >&2
fi

if [[ $namespaces_controlplane || $namespaces_dataplane || $clusterrolebindings || $clusterroles || $webhookconfigs || $validatingconfigs || $podsecuritypolicies || $customresourcedefinitions || $apiservices ]]; then
kubectl --context=$k8s_context delete $namespaces_controlplane $namespaces_dataplane $clusterrolebindings $clusterroles $webhookconfigs $validatingconfigs $podsecuritypolicies $customresourcedefinitions $apiservices
fi

echo "cleaning up rolebindings in kube-system namespace in k8s-context [${k8s_context}]"
if ! rolebindings=$(kubectl --context=$k8s_context -n kube-system get rolebindings -l linkerd.io/control-plane-ns -oname); then
echo "no rolebindings found in the kube-system namespace" >&2
fi
if [[ $rolebindings ]]; then
Comment thread
kleimkuhler marked this conversation as resolved.
Resolved
kubectl --context=$k8s_context delete $rolebindings -n kube-system
fi
65 changes: 65 additions & 0 deletions chart/templates/tap-rbac.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,22 @@ subjects:
name: linkerd-tap
namespace: {{.Namespace}}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: linkerd-{{.Namespace}}-tap-auth-delegator
labels:
{{.ControllerComponentLabel}}: tap
{{.ControllerNamespaceLabel}}: {{.Namespace}}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:auth-delegator
subjects:
- kind: ServiceAccount
name: linkerd-tap
namespace: {{.Namespace}}
---
kind: ServiceAccount
apiVersion: v1
metadata:
Expand All @@ -46,4 +62,53 @@ metadata:
labels:
{{.ControllerComponentLabel}}: tap
{{.ControllerNamespaceLabel}}: {{.Namespace}}
---
apiVersion: rbac.authorization.k8s.io/v1
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of this RoleBinding and why is it needed in addition to the ClusterRoleBinding for the auth delegator?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RoleBinding/linkerd-linkerd-tap-auth-reader, in the kube-system namespace, gives access to cm/extension-apiserver-authentication in the kube-system namespace, which gives us client TLS and header info, for authn and authz of incoming requests from Kubernetes to our apiserver. More details in the comment above apiServerAuth, also at:
https://kubernetes.io/docs/tasks/access-kubernetes-api/#kubernetes-apiserver-client-authentication

ClusterRoleBinding/linkerd-linkerd-tap-auth-delegator gives the TAP APIServer SubjectAccessReview privileges. See handleTap for that implementation, also for more details:
https://kubernetes.io/docs/tasks/access-kubernetes-api/#extension-apiserver-authorizes-the-request

kind: RoleBinding
metadata:
name: linkerd-{{.Namespace}}-tap-auth-reader
namespace: kube-system
labels:
{{.ControllerComponentLabel}}: tap
{{.ControllerNamespaceLabel}}: {{.Namespace}}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: extension-apiserver-authentication-reader
subjects:
- kind: ServiceAccount
name: linkerd-tap
namespace: {{.Namespace}}
---
kind: Secret
apiVersion: v1
metadata:
name: linkerd-tap-tls
namespace: {{ .Namespace }}
labels:
{{.ControllerComponentLabel}}: tap
{{.ControllerNamespaceLabel}}: {{.Namespace}}
annotations:
{{ .CreatedByAnnotation }}: {{ .CliVersion }}
type: Opaque
data:
crt.pem: {{ b64enc .Tap.CrtPEM }}
key.pem: {{ b64enc .Tap.KeyPEM }}
---
apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
name: v1alpha1.tap.linkerd.io
labels:
{{.ControllerComponentLabel}}: tap
{{.ControllerNamespaceLabel}}: {{.Namespace}}
spec:
group: tap.linkerd.io
version: v1alpha1
groupPriorityMinimum: 1000
versionPriority: 100
service:
name: linkerd-tap
namespace: {{.Namespace}}
caBundle: {{ b64enc .Tap.CrtPEM }}
{{end -}}
13 changes: 13 additions & 0 deletions chart/templates/tap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ spec:
- name: grpc
port: 8088
targetPort: 8088
- name: apiserver
port: 443
targetPort: apiserver
---
kind: Deployment
apiVersion: extensions/v1beta1
Expand All @@ -48,8 +51,14 @@ spec:
ports:
- name: grpc
containerPort: 8088
- name: apiserver
containerPort: 8089
- name: admin-http
containerPort: 9998
volumeMounts:
- name: tls
mountPath: /var/run/linkerd/tls
readOnly: true
image: {{.ControllerImage}}
imagePullPolicy: {{.ImagePullPolicy}}
args:
Expand All @@ -69,6 +78,10 @@ spec:
{{- include "resources" .TapResources | nindent 8 }}
securityContext:
runAsUser: {{.ControllerUID}}
volumes:
- name: tls
secret:
secretName: linkerd-tap-tls
{{- if .HighAvailability }}
{{- $local := dict "Label" .ControllerComponentLabel "Component" "tap" }}
{{- include "pod-affinity" $local | nindent 6 }}
Expand Down
12 changes: 12 additions & 0 deletions cli/cmd/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ type (
Identity *installIdentityValues
ProxyInjector *proxyInjectorValues
ProfileValidator *profileValidatorValues
Tap *tapValues
}

configJSONs struct{ Global, Proxy, Install string }
Expand Down Expand Up @@ -115,6 +116,10 @@ type (
*tlsValues
}

tapValues struct {
*tlsValues
}

tlsValues struct {
KeyPEM, CrtPEM string
}
Expand Down Expand Up @@ -231,6 +236,7 @@ func newInstallOptionsWithDefaults() *installOptions {
return id.String()
},

// TODO: rename to generateTLS or something
Comment thread
kleimkuhler marked this conversation as resolved.
Resolved
generateWebhookTLS: func(webhook string) (*tlsValues, error) {
root, err := tls.GenerateRootCAWithDefaults(webhookCommonName(webhook))
if err != nil {
Expand Down Expand Up @@ -429,6 +435,12 @@ func (options *installOptions) validateAndBuild(stage string, flags *pflag.FlagS
}
values.ProfileValidator = &profileValidatorValues{profileValidatorTLS}

tapTLS, err := options.generateWebhookTLS(k8s.TapServiceName)
if err != nil {
return nil, nil, err
}
values.Tap = &tapValues{tapTLS}

values.stage = stage

return values, configs, nil
Expand Down
12 changes: 12 additions & 0 deletions cli/cmd/install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ func TestRender(t *testing.T) {
CrtPEM: "profile validator crt",
},
},
Tap: &tapValues{
&tlsValues{
KeyPEM: "tap key",
CrtPEM: "tap crt",
},
},
}

haOptions := testInstallOptions()
Expand Down Expand Up @@ -236,7 +242,13 @@ func fakeGenerateWebhookTLS(webhook string) (*tlsValues, error) {
KeyPEM: "profile validator key",
CrtPEM: "profile validator crt",
}, nil
case k8s.TapServiceName:
return &tlsValues{
KeyPEM: "tap key",
CrtPEM: "tap crt",
}, nil
}

return nil, nil
}

Expand Down
61 changes: 50 additions & 11 deletions cli/cmd/tap.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
package cmd

import (
"context"
"bufio"
"bytes"
"fmt"
"io"
"net/http"
"os"
"strings"
"text/tabwriter"

"github.com/golang/protobuf/proto"
"github.com/linkerd/linkerd2/controller/api/util"
pb "github.com/linkerd/linkerd2/controller/gen/public"
"github.com/linkerd/linkerd2/pkg/addr"
"github.com/linkerd/linkerd2/pkg/k8s"
"github.com/linkerd/linkerd2/pkg/protohttp"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"google.golang.org/grpc/codes"
Expand Down Expand Up @@ -112,7 +116,12 @@ func newCmdTap() *cobra.Command {
return fmt.Errorf("output format \"%s\" not recognized", options.output)
}

return requestTapByResourceFromAPI(os.Stdout, checkPublicAPIClientOrExit(), req, wide)
k8sAPI, err := k8s.NewAPI(kubeconfigPath, kubeContext, impersonate, 0)
if err != nil {
return err
}

return requestTapByResourceFromAPI(os.Stdout, k8sAPI, req, wide)
},
}

Expand All @@ -138,22 +147,51 @@ func newCmdTap() *cobra.Command {
return cmd
}

func requestTapByResourceFromAPI(w io.Writer, client pb.ApiClient, req *pb.TapByResourceRequest, wide bool) error {
func requestTapByResourceFromAPI(w io.Writer, k8sAPI *k8s.KubernetesAPI, req *pb.TapByResourceRequest, wide bool) error {
var resource string
if wide {
resource = req.Target.Resource.GetType()
resource = req.GetTarget().GetResource().GetType()
}

rsp, err := client.TapByResource(context.Background(), req)
client, err := k8sAPI.NewClient()
if err != nil {
return err
}
return renderTap(w, rsp, resource)

reqBytes, err := proto.Marshal(req)
if err != nil {
return err
}

url := protohttp.TapReqToURL(req)
httpReq, err := http.NewRequest(
http.MethodPost,
fmt.Sprintf("%s%s", k8sAPI.Host, url),
bytes.NewReader(reqBytes),
)
if err != nil {
return err
}

httpRsp, err := client.Do(httpReq)
if err != nil {
log.Debugf("Error invoking [%s]: %v", "taps", err)
return err
}
defer httpRsp.Body.Close()
log.Debugf("Response from [%s] had headers: %v", "taps", httpRsp.Header)

if err := protohttp.CheckIfResponseHasError(httpRsp); err != nil {
return err
}

reader := bufio.NewReader(httpRsp.Body)
return renderTap(w, reader, resource)
}

func renderTap(w io.Writer, tapClient pb.Api_TapByResourceClient, resource string) error {
func renderTap(w io.Writer, tapByteStream *bufio.Reader, resource string) error {
tableWriter := tabwriter.NewWriter(w, 0, 0, 0, ' ', tabwriter.AlignRight)
err := writeTapEventsToBuffer(tapClient, tableWriter, resource)
err := writeTapEventsToBuffer(tapByteStream, tableWriter, resource)
if err != nil {
return err
}
Expand All @@ -162,18 +200,19 @@ func renderTap(w io.Writer, tapClient pb.Api_TapByResourceClient, resource strin
return nil
}

func writeTapEventsToBuffer(tapClient pb.Api_TapByResourceClient, w *tabwriter.Writer, resource string) error {
func writeTapEventsToBuffer(tapByteStream *bufio.Reader, w *tabwriter.Writer, resource string) error {
for {
log.Debug("Waiting for data...")
event, err := tapClient.Recv()
event := pb.TapEvent{}
err := protohttp.FromByteStreamToProtocolBuffers(tapByteStream, &event)
if err == io.EOF {
break
}
if err != nil {
fmt.Fprintln(os.Stderr, err)
break
}
_, err = fmt.Fprintln(w, renderTapEvent(event, resource))
_, err = fmt.Fprintln(w, renderTapEvent(&event, resource))
if err != nil {
return err
}
Expand Down
Loading
0