mirror of
https://github.com/tailscale/tailscale.git
synced 2025-08-11 13:18:53 +00:00
cmd/containerboot,kube: consolidate the two kube clients
We had two implemenetations of the kube client, merge them. containerboot was also using a raw http.Transport, this also has the side effect of making it use a http.Client Signed-off-by: Maisem Ali <maisem@tailscale.com>
This commit is contained in:
181
kube/client.go
181
kube/client.go
@@ -14,11 +14,15 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tailscale.com/util/multierr"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -26,7 +30,19 @@ const (
|
||||
defaultURL = "https://kubernetes.default.svc"
|
||||
)
|
||||
|
||||
// rootPathForTests is set by tests to override the root path to the
|
||||
// service account directory.
|
||||
var rootPathForTests string
|
||||
|
||||
// SetRootPathForTesting sets the path to the service account directory.
|
||||
func SetRootPathForTesting(p string) {
|
||||
rootPathForTests = p
|
||||
}
|
||||
|
||||
func readFile(n string) ([]byte, error) {
|
||||
if rootPathForTests != "" {
|
||||
return os.ReadFile(filepath.Join(rootPathForTests, saPath, n))
|
||||
}
|
||||
return os.ReadFile(filepath.Join(saPath, n))
|
||||
}
|
||||
|
||||
@@ -68,6 +84,12 @@ func New() (*Client, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SetURL sets the URL to use for the Kubernetes API.
|
||||
// This is used only for testing.
|
||||
func (c *Client) SetURL(url string) {
|
||||
c.url = url
|
||||
}
|
||||
|
||||
func (c *Client) expireToken() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
@@ -111,28 +133,27 @@ func getError(resp *http.Response) error {
|
||||
return st
|
||||
}
|
||||
|
||||
func (c *Client) doRequest(ctx context.Context, method, url string, in, out any) error {
|
||||
tk, err := c.getOrRenewToken()
|
||||
func setHeader(key, value string) func(*http.Request) {
|
||||
return func(req *http.Request) {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
// doRequest performs an HTTP request to the Kubernetes API.
|
||||
// If in is not nil, it is expected to be a JSON-encodable object and will be
|
||||
// sent as the request body.
|
||||
// If out is not nil, it is expected to be a pointer to an object that can be
|
||||
// decoded from JSON.
|
||||
// If the request fails with a 401, the token is expired and a new one is
|
||||
// requested.
|
||||
func (c *Client) doRequest(ctx context.Context, method, url string, in, out any, opts ...func(*http.Request)) error {
|
||||
req, err := c.newRequest(ctx, method, url, in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var body io.Reader
|
||||
if in != nil {
|
||||
var b bytes.Buffer
|
||||
if err := json.NewEncoder(&b).Encode(in); err != nil {
|
||||
return err
|
||||
}
|
||||
body = &b
|
||||
for _, opt := range opts {
|
||||
opt(req)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, method, url, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if body != nil {
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
}
|
||||
req.Header.Add("Accept", "application/json")
|
||||
req.Header.Add("Authorization", "Bearer "+tk)
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -150,6 +171,36 @@ func (c *Client) doRequest(ctx context.Context, method, url string, in, out any)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) newRequest(ctx context.Context, method, url string, in any) (*http.Request, error) {
|
||||
tk, err := c.getOrRenewToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var body io.Reader
|
||||
if in != nil {
|
||||
switch in := in.(type) {
|
||||
case []byte:
|
||||
body = bytes.NewReader(in)
|
||||
default:
|
||||
var b bytes.Buffer
|
||||
if err := json.NewEncoder(&b).Encode(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body = &b
|
||||
}
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, method, url, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if body != nil {
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
}
|
||||
req.Header.Add("Accept", "application/json")
|
||||
req.Header.Add("Authorization", "Bearer "+tk)
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// GetSecret fetches the secret from the Kubernetes API.
|
||||
func (c *Client) GetSecret(ctx context.Context, name string) (*Secret, error) {
|
||||
s := &Secret{Data: make(map[string][]byte)}
|
||||
@@ -169,3 +220,97 @@ func (c *Client) CreateSecret(ctx context.Context, s *Secret) error {
|
||||
func (c *Client) UpdateSecret(ctx context.Context, s *Secret) error {
|
||||
return c.doRequest(ctx, "PUT", c.secretURL(s.Name), s, nil)
|
||||
}
|
||||
|
||||
// JSONPatch is a JSON patch operation.
|
||||
// It currently (2023-03-02) only supports the "remove" operation.
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc6902
|
||||
type JSONPatch struct {
|
||||
Op string `json:"op"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
// JSONPatchSecret updates a secret in the Kubernetes API using a JSON patch.
|
||||
// It currently (2023-03-02) only supports the "remove" operation.
|
||||
func (c *Client) JSONPatchSecret(ctx context.Context, name string, patch []JSONPatch) error {
|
||||
for _, p := range patch {
|
||||
if p.Op != "remove" {
|
||||
panic(fmt.Errorf("unsupported JSON patch operation: %q", p.Op))
|
||||
}
|
||||
}
|
||||
return c.doRequest(ctx, "PATCH", c.secretURL(name), patch, nil, setHeader("Content-Type", "application/json-patch+json"))
|
||||
}
|
||||
|
||||
// StrategicMergePatchSecret updates a secret in the Kubernetes API using a
|
||||
// strategic merge patch.
|
||||
// If a fieldManager is provided, it will be used to track the patch.
|
||||
func (c *Client) StrategicMergePatchSecret(ctx context.Context, name string, s *Secret, fieldManager string) error {
|
||||
surl := c.secretURL(name)
|
||||
if fieldManager != "" {
|
||||
uv := url.Values{
|
||||
"fieldManager": {fieldManager},
|
||||
}
|
||||
surl += "?" + uv.Encode()
|
||||
}
|
||||
s.Namespace = c.ns
|
||||
s.Name = name
|
||||
return c.doRequest(ctx, "PATCH", surl, s, nil, setHeader("Content-Type", "application/strategic-merge-patch+json"))
|
||||
}
|
||||
|
||||
// CheckSecretPermissions checks the secret access permissions of the current
|
||||
// pod. It returns an error if the basic permissions tailscale needs are
|
||||
// missing, and reports whether the patch permission is additionally present.
|
||||
//
|
||||
// Errors encountered during the access checking process are logged, but ignored
|
||||
// so that the pod tries to fail alive if the permissions exist and there's just
|
||||
// something wrong with SelfSubjectAccessReviews. There shouldn't be, pods
|
||||
// should always be able to use SSARs to assess their own permissions, but since
|
||||
// we didn't use to check permissions this way we'll be cautious in case some
|
||||
// old version of k8s deviates from the current behavior.
|
||||
func (c *Client) CheckSecretPermissions(ctx context.Context, secretName string) (canPatch bool, err error) {
|
||||
var errs []error
|
||||
for _, verb := range []string{"get", "update"} {
|
||||
ok, err := c.checkPermission(ctx, verb, secretName)
|
||||
if err != nil {
|
||||
log.Printf("error checking %s permission on secret %s: %v", verb, secretName, err)
|
||||
} else if !ok {
|
||||
errs = append(errs, fmt.Errorf("missing %s permission on secret %q", verb, secretName))
|
||||
}
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return false, multierr.New(errs...)
|
||||
}
|
||||
ok, err := c.checkPermission(ctx, "patch", secretName)
|
||||
if err != nil {
|
||||
log.Printf("error checking patch permission on secret %s: %v", secretName, err)
|
||||
return false, nil
|
||||
}
|
||||
return ok, nil
|
||||
}
|
||||
|
||||
// checkPermission reports whether the current pod has permission to use the
|
||||
// given verb (e.g. get, update, patch) on secretName.
|
||||
func (c *Client) checkPermission(ctx context.Context, verb, secretName string) (bool, error) {
|
||||
sar := map[string]any{
|
||||
"apiVersion": "authorization.k8s.io/v1",
|
||||
"kind": "SelfSubjectAccessReview",
|
||||
"spec": map[string]any{
|
||||
"resourceAttributes": map[string]any{
|
||||
"namespace": c.ns,
|
||||
"verb": verb,
|
||||
"resource": "secrets",
|
||||
"name": secretName,
|
||||
},
|
||||
},
|
||||
}
|
||||
var res struct {
|
||||
Status struct {
|
||||
Allowed bool `json:"allowed"`
|
||||
} `json:"status"`
|
||||
}
|
||||
url := c.url + "/apis/authorization.k8s.io/v1/selfsubjectaccessreviews"
|
||||
if err := c.doRequest(ctx, "POST", url, sar, &res); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return res.Status.Allowed, nil
|
||||
}
|
||||
|
Reference in New Issue
Block a user