mirror of
https://github.com/k3s-io/kubernetes.git
synced 2026-01-05 23:47:50 +00:00
kubelet: Support ClusterTrustBundlePEM projections
This commit is contained in:
@@ -333,6 +333,13 @@ type KubeletVolumeHost interface {
|
||||
WaitForCacheSync() error
|
||||
// Returns hostutil.HostUtils
|
||||
GetHostUtil() hostutil.HostUtils
|
||||
|
||||
// Returns trust anchors from the named ClusterTrustBundle.
|
||||
GetTrustAnchorsByName(name string, allowMissing bool) ([]byte, error)
|
||||
|
||||
// Returns trust anchors from the ClusterTrustBundles selected by signer
|
||||
// name and label selector.
|
||||
GetTrustAnchorsBySigner(signerName string, labelSelector *metav1.LabelSelector, allowMissing bool) ([]byte, error)
|
||||
}
|
||||
|
||||
// AttachDetachVolumeHost is a AttachDetach Controller specific interface that plugins can use
|
||||
|
||||
@@ -45,6 +45,7 @@ const (
|
||||
|
||||
type projectedPlugin struct {
|
||||
host volume.VolumeHost
|
||||
kvHost volume.KubeletVolumeHost
|
||||
getSecret func(namespace, name string) (*v1.Secret, error)
|
||||
getConfigMap func(namespace, name string) (*v1.ConfigMap, error)
|
||||
getServiceAccountToken func(namespace, name string, tr *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error)
|
||||
@@ -69,6 +70,7 @@ func getPath(uid types.UID, volName string, host volume.VolumeHost) string {
|
||||
|
||||
func (plugin *projectedPlugin) Init(host volume.VolumeHost) error {
|
||||
plugin.host = host
|
||||
plugin.kvHost = host.(volume.KubeletVolumeHost)
|
||||
plugin.getSecret = host.GetSecretFunc()
|
||||
plugin.getConfigMap = host.GetConfigMapFunc()
|
||||
plugin.getServiceAccountToken = host.GetServiceAccountTokenFunc()
|
||||
@@ -353,6 +355,42 @@ func (s *projectedVolumeMounter) collectData(mounterArgs volume.MounterArgs) (ma
|
||||
Mode: mode,
|
||||
FsUser: mounterArgs.FsUser,
|
||||
}
|
||||
case source.ClusterTrustBundle != nil:
|
||||
allowEmpty := false
|
||||
if source.ClusterTrustBundle.Optional != nil && *source.ClusterTrustBundle.Optional {
|
||||
allowEmpty = true
|
||||
}
|
||||
|
||||
var trustAnchors []byte
|
||||
if source.ClusterTrustBundle.Name != nil {
|
||||
var err error
|
||||
trustAnchors, err = s.plugin.kvHost.GetTrustAnchorsByName(*source.ClusterTrustBundle.Name, allowEmpty)
|
||||
if err != nil {
|
||||
errlist = append(errlist, err)
|
||||
continue
|
||||
}
|
||||
} else if source.ClusterTrustBundle.SignerName != nil {
|
||||
var err error
|
||||
trustAnchors, err = s.plugin.kvHost.GetTrustAnchorsBySigner(*source.ClusterTrustBundle.SignerName, source.ClusterTrustBundle.LabelSelector, allowEmpty)
|
||||
if err != nil {
|
||||
errlist = append(errlist, err)
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
errlist = append(errlist, fmt.Errorf("ClusterTrustBundle projection requires either name or signerName to be set"))
|
||||
continue
|
||||
}
|
||||
|
||||
mode := *s.source.DefaultMode
|
||||
if mounterArgs.FsUser != nil || mounterArgs.FsGroup != nil {
|
||||
mode = 0600
|
||||
}
|
||||
|
||||
payload[source.ClusterTrustBundle.Path] = volumeutil.FileProjection{
|
||||
Data: trustAnchors,
|
||||
Mode: mode,
|
||||
FsUser: mounterArgs.FsUser,
|
||||
}
|
||||
}
|
||||
}
|
||||
return payload, utilerrors.NewAggregate(errlist)
|
||||
|
||||
@@ -17,7 +17,13 @@ limitations under the License.
|
||||
package projected
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
@@ -26,6 +32,7 @@ import (
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
authenticationv1 "k8s.io/api/authentication/v1"
|
||||
certificatesv1alpha1 "k8s.io/api/certificates/v1alpha1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
@@ -872,13 +879,172 @@ func TestCollectDataWithServiceAccountToken(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectDataWithClusterTrustBundle(t *testing.T) {
|
||||
// This test is limited by the use of a fake clientset and volume host. We
|
||||
// can't meaningfully test that label selectors end up doing the correct
|
||||
// thing for example.
|
||||
|
||||
goodCert1 := mustMakeRoot(t, "root1")
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
|
||||
source v1.ProjectedVolumeSource
|
||||
bundles []runtime.Object
|
||||
|
||||
fsUser *int64
|
||||
fsGroup *int64
|
||||
|
||||
wantPayload map[string]util.FileProjection
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "single ClusterTrustBundle by name",
|
||||
source: v1.ProjectedVolumeSource{
|
||||
Sources: []v1.VolumeProjection{
|
||||
{
|
||||
ClusterTrustBundle: &v1.ClusterTrustBundleProjection{
|
||||
Name: utilptr.String("foo"),
|
||||
Path: "bundle.pem",
|
||||
},
|
||||
},
|
||||
},
|
||||
DefaultMode: utilptr.Int32(0644),
|
||||
},
|
||||
bundles: []runtime.Object{
|
||||
&certificatesv1alpha1.ClusterTrustBundle{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Spec: certificatesv1alpha1.ClusterTrustBundleSpec{
|
||||
TrustBundle: string(goodCert1),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantPayload: map[string]util.FileProjection{
|
||||
"bundle.pem": {
|
||||
Data: []byte(goodCert1),
|
||||
Mode: 0644,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single ClusterTrustBundle by signer name",
|
||||
source: v1.ProjectedVolumeSource{
|
||||
Sources: []v1.VolumeProjection{
|
||||
{
|
||||
ClusterTrustBundle: &v1.ClusterTrustBundleProjection{
|
||||
SignerName: utilptr.String("foo.example/bar"), // Note: fake client doesn't understand selection by signer name.
|
||||
LabelSelector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{
|
||||
"key": "non-value", // Note: fake client doesn't actually act on label selectors.
|
||||
},
|
||||
},
|
||||
Path: "bundle.pem",
|
||||
},
|
||||
},
|
||||
},
|
||||
DefaultMode: utilptr.Int32(0644),
|
||||
},
|
||||
bundles: []runtime.Object{
|
||||
&certificatesv1alpha1.ClusterTrustBundle{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo:example:bar",
|
||||
Labels: map[string]string{
|
||||
"key": "value",
|
||||
},
|
||||
},
|
||||
Spec: certificatesv1alpha1.ClusterTrustBundleSpec{
|
||||
SignerName: "foo.example/bar",
|
||||
TrustBundle: string(goodCert1),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantPayload: map[string]util.FileProjection{
|
||||
"bundle.pem": {
|
||||
Data: []byte(goodCert1),
|
||||
Mode: 0644,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single ClusterTrustBundle by name, non-default mode",
|
||||
source: v1.ProjectedVolumeSource{
|
||||
Sources: []v1.VolumeProjection{
|
||||
{
|
||||
ClusterTrustBundle: &v1.ClusterTrustBundleProjection{
|
||||
Name: utilptr.String("foo"),
|
||||
Path: "bundle.pem",
|
||||
},
|
||||
},
|
||||
},
|
||||
DefaultMode: utilptr.Int32(0600),
|
||||
},
|
||||
bundles: []runtime.Object{
|
||||
&certificatesv1alpha1.ClusterTrustBundle{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
},
|
||||
Spec: certificatesv1alpha1.ClusterTrustBundleSpec{
|
||||
TrustBundle: string(goodCert1),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantPayload: map[string]util.FileProjection{
|
||||
"bundle.pem": {
|
||||
Data: []byte(goodCert1),
|
||||
Mode: 0600,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
pod := &v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
UID: types.UID("test_pod_uid"),
|
||||
},
|
||||
Spec: v1.PodSpec{ServiceAccountName: "foo"},
|
||||
}
|
||||
|
||||
client := fake.NewSimpleClientset(tc.bundles...)
|
||||
|
||||
tempDir, host := newTestHost(t, client)
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
var myVolumeMounter = projectedVolumeMounter{
|
||||
projectedVolume: &projectedVolume{
|
||||
sources: tc.source.Sources,
|
||||
podUID: pod.UID,
|
||||
plugin: &projectedPlugin{
|
||||
host: host,
|
||||
kvHost: host.(volume.KubeletVolumeHost),
|
||||
},
|
||||
},
|
||||
source: tc.source,
|
||||
pod: pod,
|
||||
}
|
||||
|
||||
gotPayload, err := myVolumeMounter.collectData(volume.MounterArgs{FsUser: tc.fsUser, FsGroup: tc.fsGroup})
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected failure making payload: %v", err)
|
||||
}
|
||||
if diff := cmp.Diff(tc.wantPayload, gotPayload); diff != "" {
|
||||
t.Fatalf("Bad payload; diff (-want +got)\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newTestHost(t *testing.T, clientset clientset.Interface) (string, volume.VolumeHost) {
|
||||
tempDir, err := os.MkdirTemp("", "projected_volume_test.")
|
||||
if err != nil {
|
||||
t.Fatalf("can't make a temp rootdir: %v", err)
|
||||
}
|
||||
|
||||
return tempDir, volumetest.NewFakeVolumeHost(t, tempDir, clientset, emptydir.ProbeVolumePlugins())
|
||||
return tempDir, volumetest.NewFakeKubeletVolumeHost(t, tempDir, clientset, emptydir.ProbeVolumePlugins())
|
||||
}
|
||||
|
||||
func TestCanSupport(t *testing.T) {
|
||||
@@ -1322,3 +1488,30 @@ func doTestCleanAndTeardown(plugin volume.VolumePlugin, podUID types.UID, testVo
|
||||
t.Errorf("TearDown() failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func mustMakeRoot(t *testing.T, cn string) string {
|
||||
pub, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatalf("Error while generating key: %v", err)
|
||||
}
|
||||
|
||||
template := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(0),
|
||||
Subject: pkix.Name{
|
||||
CommonName: cn,
|
||||
},
|
||||
IsCA: true,
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
|
||||
cert, err := x509.CreateCertificate(rand.Reader, template, template, pub, priv)
|
||||
if err != nil {
|
||||
t.Fatalf("Error while making certificate: %v", err)
|
||||
}
|
||||
|
||||
return string(pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Headers: nil,
|
||||
Bytes: cert,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ limitations under the License.
|
||||
package testing
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
@@ -437,3 +438,30 @@ func (f *fakeKubeletVolumeHost) WaitForCacheSync() error {
|
||||
func (f *fakeKubeletVolumeHost) GetHostUtil() hostutil.HostUtils {
|
||||
return f.hostUtil
|
||||
}
|
||||
|
||||
func (f *fakeKubeletVolumeHost) GetTrustAnchorsByName(name string, allowMissing bool) ([]byte, error) {
|
||||
ctb, err := f.kubeClient.CertificatesV1alpha1().ClusterTrustBundles().Get(context.Background(), name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("while getting ClusterTrustBundle %s: %w", name, err)
|
||||
}
|
||||
|
||||
return []byte(ctb.Spec.TrustBundle), nil
|
||||
}
|
||||
|
||||
// Note: we do none of the deduplication and sorting that the real deal should do.
|
||||
func (f *fakeKubeletVolumeHost) GetTrustAnchorsBySigner(signerName string, labelSelector *metav1.LabelSelector, allowMissing bool) ([]byte, error) {
|
||||
ctbList, err := f.kubeClient.CertificatesV1alpha1().ClusterTrustBundles().List(context.Background(), metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("while listing all ClusterTrustBundles: %w", err)
|
||||
}
|
||||
|
||||
fullSet := bytes.Buffer{}
|
||||
for i, ctb := range ctbList.Items {
|
||||
fullSet.WriteString(ctb.Spec.TrustBundle)
|
||||
if i != len(ctbList.Items)-1 {
|
||||
fullSet.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
return fullSet.Bytes(), nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user