mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-21 19:01:49 +00:00
Merge pull request #93258 from zshihang/token
mv TokenRequest and TokenRequestProjection to GA
This commit is contained in:
commit
bf67247124
52
api/openapi-spec/swagger.json
generated
52
api/openapi-spec/swagger.json
generated
@ -18905,6 +18905,32 @@
|
|||||||
"version": "unversioned"
|
"version": "unversioned"
|
||||||
},
|
},
|
||||||
"paths": {
|
"paths": {
|
||||||
|
"/.well-known/openid-configuration/": {
|
||||||
|
"get": {
|
||||||
|
"description": "get service account issuer OpenID configuration, also known as the 'OIDC discovery doc'",
|
||||||
|
"operationId": "getServiceAccountIssuerOpenIDConfiguration",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"schemes": [
|
||||||
|
"https"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"WellKnown"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/": {
|
"/api/": {
|
||||||
"get": {
|
"get": {
|
||||||
"consumes": [
|
"consumes": [
|
||||||
@ -105104,6 +105130,32 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"/openid/v1/jwks/": {
|
||||||
|
"get": {
|
||||||
|
"description": "get service account issuer OpenID JSON Web Key Set (contains public token verification keys)",
|
||||||
|
"operationId": "getServiceAccountIssuerOpenIDKeyset",
|
||||||
|
"produces": [
|
||||||
|
"application/jwk-set+json"
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"schemes": [
|
||||||
|
"https"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"openid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/version/": {
|
"/version/": {
|
||||||
"get": {
|
"get": {
|
||||||
"consumes": [
|
"consumes": [
|
||||||
|
@ -270,7 +270,7 @@ func (s *ServerRunOptions) Flags() (fss cliflag.NamedFlagSets) {
|
|||||||
"Turns on aggregator routing requests to endpoints IP rather than cluster IP.")
|
"Turns on aggregator routing requests to endpoints IP rather than cluster IP.")
|
||||||
|
|
||||||
fs.StringVar(&s.ServiceAccountSigningKeyFile, "service-account-signing-key-file", s.ServiceAccountSigningKeyFile, ""+
|
fs.StringVar(&s.ServiceAccountSigningKeyFile, "service-account-signing-key-file", s.ServiceAccountSigningKeyFile, ""+
|
||||||
"Path to the file that contains the current private key of the service account token issuer. The issuer will sign issued ID tokens with this private key. (Requires the 'TokenRequest' feature gate.)")
|
"Path to the file that contains the current private key of the service account token issuer. The issuer will sign issued ID tokens with this private key.")
|
||||||
|
|
||||||
return fss
|
return fss
|
||||||
}
|
}
|
||||||
|
@ -120,14 +120,6 @@ func validateTokenRequest(options *ServerRunOptions) []error {
|
|||||||
|
|
||||||
enableSucceeded := options.ServiceAccountIssuer != nil
|
enableSucceeded := options.ServiceAccountIssuer != nil
|
||||||
|
|
||||||
if enableAttempted && !utilfeature.DefaultFeatureGate.Enabled(features.TokenRequest) {
|
|
||||||
errs = append(errs, errors.New("the TokenRequest feature is not enabled but --service-account-signing-key-file, --service-account-issuer and/or --api-audiences flags were passed"))
|
|
||||||
}
|
|
||||||
|
|
||||||
if utilfeature.DefaultFeatureGate.Enabled(features.BoundServiceAccountTokenVolume) && !utilfeature.DefaultFeatureGate.Enabled(features.TokenRequest) {
|
|
||||||
errs = append(errs, errors.New("the BoundServiceAccountTokenVolume feature depends on the TokenRequest feature, but the TokenRequest features is not enabled"))
|
|
||||||
}
|
|
||||||
|
|
||||||
if !enableAttempted && utilfeature.DefaultFeatureGate.Enabled(features.BoundServiceAccountTokenVolume) {
|
if !enableAttempted && utilfeature.DefaultFeatureGate.Enabled(features.BoundServiceAccountTokenVolume) {
|
||||||
errs = append(errs, errors.New("--service-account-signing-key-file and --service-account-issuer are required flags"))
|
errs = append(errs, errors.New("--service-account-signing-key-file and --service-account-issuer are required flags"))
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,6 @@ import (
|
|||||||
"k8s.io/apiserver/pkg/server"
|
"k8s.io/apiserver/pkg/server"
|
||||||
"k8s.io/apiserver/pkg/server/healthz"
|
"k8s.io/apiserver/pkg/server/healthz"
|
||||||
"k8s.io/apiserver/pkg/server/mux"
|
"k8s.io/apiserver/pkg/server/mux"
|
||||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
|
||||||
cacheddiscovery "k8s.io/client-go/discovery/cached"
|
cacheddiscovery "k8s.io/client-go/discovery/cached"
|
||||||
"k8s.io/client-go/informers"
|
"k8s.io/client-go/informers"
|
||||||
clientset "k8s.io/client-go/kubernetes"
|
clientset "k8s.io/client-go/kubernetes"
|
||||||
@ -67,7 +66,6 @@ import (
|
|||||||
"k8s.io/kubernetes/pkg/controller"
|
"k8s.io/kubernetes/pkg/controller"
|
||||||
kubectrlmgrconfig "k8s.io/kubernetes/pkg/controller/apis/config"
|
kubectrlmgrconfig "k8s.io/kubernetes/pkg/controller/apis/config"
|
||||||
serviceaccountcontroller "k8s.io/kubernetes/pkg/controller/serviceaccount"
|
serviceaccountcontroller "k8s.io/kubernetes/pkg/controller/serviceaccount"
|
||||||
"k8s.io/kubernetes/pkg/features"
|
|
||||||
"k8s.io/kubernetes/pkg/serviceaccount"
|
"k8s.io/kubernetes/pkg/serviceaccount"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -621,9 +619,6 @@ func readCA(file string) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func shouldTurnOnDynamicClient(client clientset.Interface) bool {
|
func shouldTurnOnDynamicClient(client clientset.Interface) bool {
|
||||||
if !utilfeature.DefaultFeatureGate.Enabled(features.TokenRequest) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
apiResourceList, err := client.Discovery().ServerResourcesForGroupVersion(v1.SchemeGroupVersion.String())
|
apiResourceList, err := client.Discovery().ServerResourcesForGroupVersion(v1.SchemeGroupVersion.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
klog.Warningf("fetch api resource lists failed, use legacy client builder: %v", err)
|
klog.Warningf("fetch api resource lists failed, use legacy client builder: %v", err)
|
||||||
|
@ -137,6 +137,8 @@ func getAPIServerCommand(cfg *kubeadmapi.ClusterConfiguration, localAPIEndpoint
|
|||||||
"enable-admission-plugins": "NodeRestriction",
|
"enable-admission-plugins": "NodeRestriction",
|
||||||
"service-cluster-ip-range": cfg.Networking.ServiceSubnet,
|
"service-cluster-ip-range": cfg.Networking.ServiceSubnet,
|
||||||
"service-account-key-file": filepath.Join(cfg.CertificatesDir, kubeadmconstants.ServiceAccountPublicKeyName),
|
"service-account-key-file": filepath.Join(cfg.CertificatesDir, kubeadmconstants.ServiceAccountPublicKeyName),
|
||||||
|
"service-account-signing-key-file": filepath.Join(cfg.CertificatesDir, kubeadmconstants.ServiceAccountPrivateKeyName),
|
||||||
|
"service-account-issuer": fmt.Sprintf("https://kubernetes.default.svc.%s", cfg.Networking.DNSDomain),
|
||||||
"client-ca-file": filepath.Join(cfg.CertificatesDir, kubeadmconstants.CACertName),
|
"client-ca-file": filepath.Join(cfg.CertificatesDir, kubeadmconstants.CACertName),
|
||||||
"tls-cert-file": filepath.Join(cfg.CertificatesDir, kubeadmconstants.APIServerCertName),
|
"tls-cert-file": filepath.Join(cfg.CertificatesDir, kubeadmconstants.APIServerCertName),
|
||||||
"tls-private-key-file": filepath.Join(cfg.CertificatesDir, kubeadmconstants.APIServerKeyName),
|
"tls-private-key-file": filepath.Join(cfg.CertificatesDir, kubeadmconstants.APIServerKeyName),
|
||||||
|
@ -197,7 +197,7 @@ func TestGetAPIServerCommand(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "testing defaults",
|
name: "testing defaults",
|
||||||
cfg: &kubeadmapi.ClusterConfiguration{
|
cfg: &kubeadmapi.ClusterConfiguration{
|
||||||
Networking: kubeadmapi.Networking{ServiceSubnet: "bar"},
|
Networking: kubeadmapi.Networking{ServiceSubnet: "bar", DNSDomain: "cluster.local"},
|
||||||
CertificatesDir: testCertsDir,
|
CertificatesDir: testCertsDir,
|
||||||
},
|
},
|
||||||
endpoint: &kubeadmapi.APIEndpoint{BindPort: 123, AdvertiseAddress: "1.2.3.4"},
|
endpoint: &kubeadmapi.APIEndpoint{BindPort: 123, AdvertiseAddress: "1.2.3.4"},
|
||||||
@ -207,6 +207,8 @@ func TestGetAPIServerCommand(t *testing.T) {
|
|||||||
"--enable-admission-plugins=NodeRestriction",
|
"--enable-admission-plugins=NodeRestriction",
|
||||||
"--service-cluster-ip-range=bar",
|
"--service-cluster-ip-range=bar",
|
||||||
"--service-account-key-file=" + testCertsDir + "/sa.pub",
|
"--service-account-key-file=" + testCertsDir + "/sa.pub",
|
||||||
|
"--service-account-signing-key-file=" + testCertsDir + "/sa.key",
|
||||||
|
"--service-account-issuer=https://kubernetes.default.svc.cluster.local",
|
||||||
"--client-ca-file=" + testCertsDir + "/ca.crt",
|
"--client-ca-file=" + testCertsDir + "/ca.crt",
|
||||||
"--tls-cert-file=" + testCertsDir + "/apiserver.crt",
|
"--tls-cert-file=" + testCertsDir + "/apiserver.crt",
|
||||||
"--tls-private-key-file=" + testCertsDir + "/apiserver.key",
|
"--tls-private-key-file=" + testCertsDir + "/apiserver.key",
|
||||||
@ -234,7 +236,7 @@ func TestGetAPIServerCommand(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "ipv6 advertise address",
|
name: "ipv6 advertise address",
|
||||||
cfg: &kubeadmapi.ClusterConfiguration{
|
cfg: &kubeadmapi.ClusterConfiguration{
|
||||||
Networking: kubeadmapi.Networking{ServiceSubnet: "bar"},
|
Networking: kubeadmapi.Networking{ServiceSubnet: "bar", DNSDomain: "cluster.local"},
|
||||||
CertificatesDir: testCertsDir,
|
CertificatesDir: testCertsDir,
|
||||||
},
|
},
|
||||||
endpoint: &kubeadmapi.APIEndpoint{BindPort: 123, AdvertiseAddress: "2001:db8::1"},
|
endpoint: &kubeadmapi.APIEndpoint{BindPort: 123, AdvertiseAddress: "2001:db8::1"},
|
||||||
@ -244,6 +246,8 @@ func TestGetAPIServerCommand(t *testing.T) {
|
|||||||
"--enable-admission-plugins=NodeRestriction",
|
"--enable-admission-plugins=NodeRestriction",
|
||||||
"--service-cluster-ip-range=bar",
|
"--service-cluster-ip-range=bar",
|
||||||
"--service-account-key-file=" + testCertsDir + "/sa.pub",
|
"--service-account-key-file=" + testCertsDir + "/sa.pub",
|
||||||
|
"--service-account-signing-key-file=" + testCertsDir + "/sa.key",
|
||||||
|
"--service-account-issuer=https://kubernetes.default.svc.cluster.local",
|
||||||
"--client-ca-file=" + testCertsDir + "/ca.crt",
|
"--client-ca-file=" + testCertsDir + "/ca.crt",
|
||||||
"--tls-cert-file=" + testCertsDir + "/apiserver.crt",
|
"--tls-cert-file=" + testCertsDir + "/apiserver.crt",
|
||||||
"--tls-private-key-file=" + testCertsDir + "/apiserver.key",
|
"--tls-private-key-file=" + testCertsDir + "/apiserver.key",
|
||||||
@ -271,7 +275,7 @@ func TestGetAPIServerCommand(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "an external etcd with custom ca, certs and keys",
|
name: "an external etcd with custom ca, certs and keys",
|
||||||
cfg: &kubeadmapi.ClusterConfiguration{
|
cfg: &kubeadmapi.ClusterConfiguration{
|
||||||
Networking: kubeadmapi.Networking{ServiceSubnet: "bar"},
|
Networking: kubeadmapi.Networking{ServiceSubnet: "bar", DNSDomain: "cluster.local"},
|
||||||
Etcd: kubeadmapi.Etcd{
|
Etcd: kubeadmapi.Etcd{
|
||||||
External: &kubeadmapi.ExternalEtcd{
|
External: &kubeadmapi.ExternalEtcd{
|
||||||
Endpoints: []string{"https://[2001:abcd:bcda::1]:2379", "https://[2001:abcd:bcda::2]:2379"},
|
Endpoints: []string{"https://[2001:abcd:bcda::1]:2379", "https://[2001:abcd:bcda::2]:2379"},
|
||||||
@ -289,6 +293,8 @@ func TestGetAPIServerCommand(t *testing.T) {
|
|||||||
"--enable-admission-plugins=NodeRestriction",
|
"--enable-admission-plugins=NodeRestriction",
|
||||||
"--service-cluster-ip-range=bar",
|
"--service-cluster-ip-range=bar",
|
||||||
"--service-account-key-file=" + testCertsDir + "/sa.pub",
|
"--service-account-key-file=" + testCertsDir + "/sa.pub",
|
||||||
|
"--service-account-signing-key-file=" + testCertsDir + "/sa.key",
|
||||||
|
"--service-account-issuer=https://kubernetes.default.svc.cluster.local",
|
||||||
"--client-ca-file=" + testCertsDir + "/ca.crt",
|
"--client-ca-file=" + testCertsDir + "/ca.crt",
|
||||||
"--tls-cert-file=" + testCertsDir + "/apiserver.crt",
|
"--tls-cert-file=" + testCertsDir + "/apiserver.crt",
|
||||||
"--tls-private-key-file=" + testCertsDir + "/apiserver.key",
|
"--tls-private-key-file=" + testCertsDir + "/apiserver.key",
|
||||||
@ -316,7 +322,7 @@ func TestGetAPIServerCommand(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "an insecure etcd",
|
name: "an insecure etcd",
|
||||||
cfg: &kubeadmapi.ClusterConfiguration{
|
cfg: &kubeadmapi.ClusterConfiguration{
|
||||||
Networking: kubeadmapi.Networking{ServiceSubnet: "bar"},
|
Networking: kubeadmapi.Networking{ServiceSubnet: "bar", DNSDomain: "cluster.local"},
|
||||||
Etcd: kubeadmapi.Etcd{
|
Etcd: kubeadmapi.Etcd{
|
||||||
External: &kubeadmapi.ExternalEtcd{
|
External: &kubeadmapi.ExternalEtcd{
|
||||||
Endpoints: []string{"http://[::1]:2379", "http://[::1]:2380"},
|
Endpoints: []string{"http://[::1]:2379", "http://[::1]:2380"},
|
||||||
@ -331,6 +337,8 @@ func TestGetAPIServerCommand(t *testing.T) {
|
|||||||
"--enable-admission-plugins=NodeRestriction",
|
"--enable-admission-plugins=NodeRestriction",
|
||||||
"--service-cluster-ip-range=bar",
|
"--service-cluster-ip-range=bar",
|
||||||
"--service-account-key-file=" + testCertsDir + "/sa.pub",
|
"--service-account-key-file=" + testCertsDir + "/sa.pub",
|
||||||
|
"--service-account-signing-key-file=" + testCertsDir + "/sa.key",
|
||||||
|
"--service-account-issuer=https://kubernetes.default.svc.cluster.local",
|
||||||
"--client-ca-file=" + testCertsDir + "/ca.crt",
|
"--client-ca-file=" + testCertsDir + "/ca.crt",
|
||||||
"--tls-cert-file=" + testCertsDir + "/apiserver.crt",
|
"--tls-cert-file=" + testCertsDir + "/apiserver.crt",
|
||||||
"--tls-private-key-file=" + testCertsDir + "/apiserver.key",
|
"--tls-private-key-file=" + testCertsDir + "/apiserver.key",
|
||||||
@ -355,7 +363,7 @@ func TestGetAPIServerCommand(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "test APIServer.ExtraArgs works as expected",
|
name: "test APIServer.ExtraArgs works as expected",
|
||||||
cfg: &kubeadmapi.ClusterConfiguration{
|
cfg: &kubeadmapi.ClusterConfiguration{
|
||||||
Networking: kubeadmapi.Networking{ServiceSubnet: "bar"},
|
Networking: kubeadmapi.Networking{ServiceSubnet: "bar", DNSDomain: "cluster.local"},
|
||||||
CertificatesDir: testCertsDir,
|
CertificatesDir: testCertsDir,
|
||||||
APIServer: kubeadmapi.APIServer{
|
APIServer: kubeadmapi.APIServer{
|
||||||
ControlPlaneComponent: kubeadmapi.ControlPlaneComponent{
|
ControlPlaneComponent: kubeadmapi.ControlPlaneComponent{
|
||||||
@ -375,6 +383,8 @@ func TestGetAPIServerCommand(t *testing.T) {
|
|||||||
"--enable-admission-plugins=NodeRestriction",
|
"--enable-admission-plugins=NodeRestriction",
|
||||||
"--service-cluster-ip-range=baz",
|
"--service-cluster-ip-range=baz",
|
||||||
"--service-account-key-file=" + testCertsDir + "/sa.pub",
|
"--service-account-key-file=" + testCertsDir + "/sa.pub",
|
||||||
|
"--service-account-signing-key-file=" + testCertsDir + "/sa.key",
|
||||||
|
"--service-account-issuer=https://kubernetes.default.svc.cluster.local",
|
||||||
"--client-ca-file=" + testCertsDir + "/ca.crt",
|
"--client-ca-file=" + testCertsDir + "/ca.crt",
|
||||||
"--tls-cert-file=" + testCertsDir + "/apiserver.crt",
|
"--tls-cert-file=" + testCertsDir + "/apiserver.crt",
|
||||||
"--tls-private-key-file=" + testCertsDir + "/apiserver.key",
|
"--tls-private-key-file=" + testCertsDir + "/apiserver.key",
|
||||||
@ -404,7 +414,7 @@ func TestGetAPIServerCommand(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "authorization-mode extra-args ABAC",
|
name: "authorization-mode extra-args ABAC",
|
||||||
cfg: &kubeadmapi.ClusterConfiguration{
|
cfg: &kubeadmapi.ClusterConfiguration{
|
||||||
Networking: kubeadmapi.Networking{ServiceSubnet: "bar"},
|
Networking: kubeadmapi.Networking{ServiceSubnet: "bar", DNSDomain: "cluster.local"},
|
||||||
CertificatesDir: testCertsDir,
|
CertificatesDir: testCertsDir,
|
||||||
APIServer: kubeadmapi.APIServer{
|
APIServer: kubeadmapi.APIServer{
|
||||||
ControlPlaneComponent: kubeadmapi.ControlPlaneComponent{
|
ControlPlaneComponent: kubeadmapi.ControlPlaneComponent{
|
||||||
@ -421,6 +431,8 @@ func TestGetAPIServerCommand(t *testing.T) {
|
|||||||
"--enable-admission-plugins=NodeRestriction",
|
"--enable-admission-plugins=NodeRestriction",
|
||||||
"--service-cluster-ip-range=bar",
|
"--service-cluster-ip-range=bar",
|
||||||
"--service-account-key-file=" + testCertsDir + "/sa.pub",
|
"--service-account-key-file=" + testCertsDir + "/sa.pub",
|
||||||
|
"--service-account-signing-key-file=" + testCertsDir + "/sa.key",
|
||||||
|
"--service-account-issuer=https://kubernetes.default.svc.cluster.local",
|
||||||
"--client-ca-file=" + testCertsDir + "/ca.crt",
|
"--client-ca-file=" + testCertsDir + "/ca.crt",
|
||||||
"--tls-cert-file=" + testCertsDir + "/apiserver.crt",
|
"--tls-cert-file=" + testCertsDir + "/apiserver.crt",
|
||||||
"--tls-private-key-file=" + testCertsDir + "/apiserver.key",
|
"--tls-private-key-file=" + testCertsDir + "/apiserver.key",
|
||||||
@ -448,7 +460,7 @@ func TestGetAPIServerCommand(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "insecure-port extra-args",
|
name: "insecure-port extra-args",
|
||||||
cfg: &kubeadmapi.ClusterConfiguration{
|
cfg: &kubeadmapi.ClusterConfiguration{
|
||||||
Networking: kubeadmapi.Networking{ServiceSubnet: "bar"},
|
Networking: kubeadmapi.Networking{ServiceSubnet: "bar", DNSDomain: "cluster.local"},
|
||||||
CertificatesDir: testCertsDir,
|
CertificatesDir: testCertsDir,
|
||||||
APIServer: kubeadmapi.APIServer{
|
APIServer: kubeadmapi.APIServer{
|
||||||
ControlPlaneComponent: kubeadmapi.ControlPlaneComponent{
|
ControlPlaneComponent: kubeadmapi.ControlPlaneComponent{
|
||||||
@ -465,6 +477,8 @@ func TestGetAPIServerCommand(t *testing.T) {
|
|||||||
"--enable-admission-plugins=NodeRestriction",
|
"--enable-admission-plugins=NodeRestriction",
|
||||||
"--service-cluster-ip-range=bar",
|
"--service-cluster-ip-range=bar",
|
||||||
"--service-account-key-file=" + testCertsDir + "/sa.pub",
|
"--service-account-key-file=" + testCertsDir + "/sa.pub",
|
||||||
|
"--service-account-signing-key-file=" + testCertsDir + "/sa.key",
|
||||||
|
"--service-account-issuer=https://kubernetes.default.svc.cluster.local",
|
||||||
"--client-ca-file=" + testCertsDir + "/ca.crt",
|
"--client-ca-file=" + testCertsDir + "/ca.crt",
|
||||||
"--tls-cert-file=" + testCertsDir + "/apiserver.crt",
|
"--tls-cert-file=" + testCertsDir + "/apiserver.crt",
|
||||||
"--tls-private-key-file=" + testCertsDir + "/apiserver.key",
|
"--tls-private-key-file=" + testCertsDir + "/apiserver.key",
|
||||||
@ -492,7 +506,7 @@ func TestGetAPIServerCommand(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "authorization-mode extra-args Webhook",
|
name: "authorization-mode extra-args Webhook",
|
||||||
cfg: &kubeadmapi.ClusterConfiguration{
|
cfg: &kubeadmapi.ClusterConfiguration{
|
||||||
Networking: kubeadmapi.Networking{ServiceSubnet: "bar"},
|
Networking: kubeadmapi.Networking{ServiceSubnet: "bar", DNSDomain: "cluster.local"},
|
||||||
CertificatesDir: testCertsDir,
|
CertificatesDir: testCertsDir,
|
||||||
APIServer: kubeadmapi.APIServer{
|
APIServer: kubeadmapi.APIServer{
|
||||||
ControlPlaneComponent: kubeadmapi.ControlPlaneComponent{
|
ControlPlaneComponent: kubeadmapi.ControlPlaneComponent{
|
||||||
@ -513,6 +527,8 @@ func TestGetAPIServerCommand(t *testing.T) {
|
|||||||
"--enable-admission-plugins=NodeRestriction",
|
"--enable-admission-plugins=NodeRestriction",
|
||||||
"--service-cluster-ip-range=bar",
|
"--service-cluster-ip-range=bar",
|
||||||
"--service-account-key-file=" + testCertsDir + "/sa.pub",
|
"--service-account-key-file=" + testCertsDir + "/sa.pub",
|
||||||
|
"--service-account-signing-key-file=" + testCertsDir + "/sa.key",
|
||||||
|
"--service-account-issuer=https://kubernetes.default.svc.cluster.local",
|
||||||
"--client-ca-file=" + testCertsDir + "/ca.crt",
|
"--client-ca-file=" + testCertsDir + "/ca.crt",
|
||||||
"--tls-cert-file=" + testCertsDir + "/apiserver.crt",
|
"--tls-cert-file=" + testCertsDir + "/apiserver.crt",
|
||||||
"--tls-private-key-file=" + testCertsDir + "/apiserver.key",
|
"--tls-private-key-file=" + testCertsDir + "/apiserver.key",
|
||||||
@ -628,7 +644,7 @@ func TestGetControllerManagerCommand(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "custom cluster-cidr for " + cpVersion,
|
name: "custom cluster-cidr for " + cpVersion,
|
||||||
cfg: &kubeadmapi.ClusterConfiguration{
|
cfg: &kubeadmapi.ClusterConfiguration{
|
||||||
Networking: kubeadmapi.Networking{PodSubnet: "10.0.1.15/16"},
|
Networking: kubeadmapi.Networking{PodSubnet: "10.0.1.15/16", DNSDomain: "cluster.local"},
|
||||||
CertificatesDir: testCertsDir,
|
CertificatesDir: testCertsDir,
|
||||||
KubernetesVersion: cpVersion,
|
KubernetesVersion: cpVersion,
|
||||||
},
|
},
|
||||||
@ -657,7 +673,9 @@ func TestGetControllerManagerCommand(t *testing.T) {
|
|||||||
cfg: &kubeadmapi.ClusterConfiguration{
|
cfg: &kubeadmapi.ClusterConfiguration{
|
||||||
Networking: kubeadmapi.Networking{
|
Networking: kubeadmapi.Networking{
|
||||||
PodSubnet: "10.0.1.15/16",
|
PodSubnet: "10.0.1.15/16",
|
||||||
ServiceSubnet: "172.20.0.0/24"},
|
ServiceSubnet: "172.20.0.0/24",
|
||||||
|
DNSDomain: "cluster.local",
|
||||||
|
},
|
||||||
CertificatesDir: testCertsDir,
|
CertificatesDir: testCertsDir,
|
||||||
KubernetesVersion: cpVersion,
|
KubernetesVersion: cpVersion,
|
||||||
},
|
},
|
||||||
@ -685,7 +703,7 @@ func TestGetControllerManagerCommand(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "custom extra-args for " + cpVersion,
|
name: "custom extra-args for " + cpVersion,
|
||||||
cfg: &kubeadmapi.ClusterConfiguration{
|
cfg: &kubeadmapi.ClusterConfiguration{
|
||||||
Networking: kubeadmapi.Networking{PodSubnet: "10.0.1.15/16"},
|
Networking: kubeadmapi.Networking{PodSubnet: "10.0.1.15/16", DNSDomain: "cluster.local"},
|
||||||
ControllerManager: kubeadmapi.ControlPlaneComponent{
|
ControllerManager: kubeadmapi.ControlPlaneComponent{
|
||||||
ExtraArgs: map[string]string{"node-cidr-mask-size": "20"},
|
ExtraArgs: map[string]string{"node-cidr-mask-size": "20"},
|
||||||
},
|
},
|
||||||
@ -719,7 +737,9 @@ func TestGetControllerManagerCommand(t *testing.T) {
|
|||||||
Networking: kubeadmapi.Networking{
|
Networking: kubeadmapi.Networking{
|
||||||
PodSubnet: "2001:db8::/64",
|
PodSubnet: "2001:db8::/64",
|
||||||
ServiceSubnet: "fd03::/112",
|
ServiceSubnet: "fd03::/112",
|
||||||
|
DNSDomain: "cluster.local",
|
||||||
},
|
},
|
||||||
|
|
||||||
CertificatesDir: testCertsDir,
|
CertificatesDir: testCertsDir,
|
||||||
KubernetesVersion: cpVersion,
|
KubernetesVersion: cpVersion,
|
||||||
},
|
},
|
||||||
@ -750,6 +770,7 @@ func TestGetControllerManagerCommand(t *testing.T) {
|
|||||||
Networking: kubeadmapi.Networking{
|
Networking: kubeadmapi.Networking{
|
||||||
PodSubnet: "2001:db8::/64,10.1.0.0/16",
|
PodSubnet: "2001:db8::/64,10.1.0.0/16",
|
||||||
ServiceSubnet: "fd03::/112,192.168.0.0/16",
|
ServiceSubnet: "fd03::/112,192.168.0.0/16",
|
||||||
|
DNSDomain: "cluster.local",
|
||||||
},
|
},
|
||||||
CertificatesDir: testCertsDir,
|
CertificatesDir: testCertsDir,
|
||||||
KubernetesVersion: cpVersion,
|
KubernetesVersion: cpVersion,
|
||||||
@ -780,7 +801,10 @@ func TestGetControllerManagerCommand(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "dual-stack networking custom extra-args for " + cpVersion,
|
name: "dual-stack networking custom extra-args for " + cpVersion,
|
||||||
cfg: &kubeadmapi.ClusterConfiguration{
|
cfg: &kubeadmapi.ClusterConfiguration{
|
||||||
Networking: kubeadmapi.Networking{PodSubnet: "10.0.1.15/16,2001:db8::/64"},
|
Networking: kubeadmapi.Networking{
|
||||||
|
PodSubnet: "10.0.1.15/16,2001:db8::/64",
|
||||||
|
DNSDomain: "cluster.local",
|
||||||
|
},
|
||||||
ControllerManager: kubeadmapi.ControlPlaneComponent{
|
ControllerManager: kubeadmapi.ControlPlaneComponent{
|
||||||
ExtraArgs: map[string]string{"node-cidr-mask-size-ipv4": "20", "node-cidr-mask-size-ipv6": "80"},
|
ExtraArgs: map[string]string{"node-cidr-mask-size-ipv4": "20", "node-cidr-mask-size-ipv6": "80"},
|
||||||
},
|
},
|
||||||
|
@ -34,6 +34,15 @@ source "${KUBE_ROOT}/hack/lib/init.sh"
|
|||||||
source "${KUBE_ROOT}/hack/lib/test.sh"
|
source "${KUBE_ROOT}/hack/lib/test.sh"
|
||||||
source "${KUBE_ROOT}/test/cmd/legacy-script.sh"
|
source "${KUBE_ROOT}/test/cmd/legacy-script.sh"
|
||||||
|
|
||||||
|
# setup envs for TokenRequest required flags
|
||||||
|
SERVICE_ACCOUNT_LOOKUP=${SERVICE_ACCOUNT_LOOKUP:-true}
|
||||||
|
SERVICE_ACCOUNT_KEY=${SERVICE_ACCOUNT_KEY:-/tmp/kube-serviceaccount.key}
|
||||||
|
# Generate ServiceAccount key if needed
|
||||||
|
if [[ ! -f "${SERVICE_ACCOUNT_KEY}" ]]; then
|
||||||
|
mkdir -p "$(dirname "${SERVICE_ACCOUNT_KEY}")"
|
||||||
|
openssl genrsa -out "${SERVICE_ACCOUNT_KEY}" 2048 2>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
# Runs kube-apiserver
|
# Runs kube-apiserver
|
||||||
#
|
#
|
||||||
# Exports:
|
# Exports:
|
||||||
@ -64,6 +73,10 @@ function run_kube_apiserver() {
|
|||||||
--disable-admission-plugins="${DISABLE_ADMISSION_PLUGINS}" \
|
--disable-admission-plugins="${DISABLE_ADMISSION_PLUGINS}" \
|
||||||
--etcd-servers="http://${ETCD_HOST}:${ETCD_PORT}" \
|
--etcd-servers="http://${ETCD_HOST}:${ETCD_PORT}" \
|
||||||
--runtime-config=api/v1 \
|
--runtime-config=api/v1 \
|
||||||
|
--service-account-key-file="${SERVICE_ACCOUNT_KEY}" \
|
||||||
|
--service-account-lookup="${SERVICE_ACCOUNT_LOOKUP}" \
|
||||||
|
--service-account-issuer="https://kubernetes.default.svc" \
|
||||||
|
--service-account-signing-key-file="${SERVICE_ACCOUNT_KEY}" \
|
||||||
--storage-media-type="${KUBE_TEST_API_STORAGE_TYPE-}" \
|
--storage-media-type="${KUBE_TEST_API_STORAGE_TYPE-}" \
|
||||||
--cert-dir="${TMPDIR:-/tmp/}" \
|
--cert-dir="${TMPDIR:-/tmp/}" \
|
||||||
--service-cluster-ip-range="10.0.0.0/24" \
|
--service-cluster-ip-range="10.0.0.0/24" \
|
||||||
|
@ -58,6 +58,15 @@ kube::etcd::start
|
|||||||
|
|
||||||
echo "dummy_token,admin,admin" > "${TMP_DIR}/tokenauth.csv"
|
echo "dummy_token,admin,admin" > "${TMP_DIR}/tokenauth.csv"
|
||||||
|
|
||||||
|
# setup envs for TokenRequest required flags
|
||||||
|
SERVICE_ACCOUNT_LOOKUP=${SERVICE_ACCOUNT_LOOKUP:-true}
|
||||||
|
SERVICE_ACCOUNT_KEY=${SERVICE_ACCOUNT_KEY:-/tmp/kube-serviceaccount.key}
|
||||||
|
# Generate ServiceAccount key if needed
|
||||||
|
if [[ ! -f "${SERVICE_ACCOUNT_KEY}" ]]; then
|
||||||
|
mkdir -p "$(dirname "${SERVICE_ACCOUNT_KEY}")"
|
||||||
|
openssl genrsa -out "${SERVICE_ACCOUNT_KEY}" 2048 2>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
# Start kube-apiserver
|
# Start kube-apiserver
|
||||||
kube::log::status "Starting kube-apiserver"
|
kube::log::status "Starting kube-apiserver"
|
||||||
"${KUBE_OUTPUT_HOSTBIN}/kube-apiserver" \
|
"${KUBE_OUTPUT_HOSTBIN}/kube-apiserver" \
|
||||||
@ -69,8 +78,10 @@ kube::log::status "Starting kube-apiserver"
|
|||||||
--runtime-config="api/all=true" \
|
--runtime-config="api/all=true" \
|
||||||
--token-auth-file="${TMP_DIR}/tokenauth.csv" \
|
--token-auth-file="${TMP_DIR}/tokenauth.csv" \
|
||||||
--authorization-mode=RBAC \
|
--authorization-mode=RBAC \
|
||||||
--service-account-issuer="https://kubernetes.default.svc/" \
|
--service-account-key-file="${SERVICE_ACCOUNT_KEY}" \
|
||||||
--service-account-signing-key-file="${KUBE_ROOT}/staging/src/k8s.io/client-go/util/cert/testdata/dontUseThisKey.pem" \
|
--service-account-lookup="${SERVICE_ACCOUNT_LOOKUP}" \
|
||||||
|
--service-account-issuer="https://kubernetes.default.svc" \
|
||||||
|
--service-account-signing-key-file="${SERVICE_ACCOUNT_KEY}" \
|
||||||
--logtostderr \
|
--logtostderr \
|
||||||
--v=2 \
|
--v=2 \
|
||||||
--service-cluster-ip-range="10.0.0.0/24" >"${API_LOGFILE}" 2>&1 &
|
--service-cluster-ip-range="10.0.0.0/24" >"${API_LOGFILE}" 2>&1 &
|
||||||
|
@ -19,7 +19,7 @@ package pod
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
api "k8s.io/kubernetes/pkg/apis/core"
|
api "k8s.io/kubernetes/pkg/apis/core"
|
||||||
@ -349,18 +349,6 @@ func dropDisabledFields(
|
|||||||
podSpec = &api.PodSpec{}
|
podSpec = &api.PodSpec{}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !utilfeature.DefaultFeatureGate.Enabled(features.TokenRequestProjection) &&
|
|
||||||
!tokenRequestProjectionInUse(oldPodSpec) {
|
|
||||||
for i := range podSpec.Volumes {
|
|
||||||
if podSpec.Volumes[i].Projected != nil {
|
|
||||||
for j := range podSpec.Volumes[i].Projected.Sources {
|
|
||||||
podSpec.Volumes[i].Projected.Sources[j].ServiceAccountToken = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !utilfeature.DefaultFeatureGate.Enabled(features.AppArmor) && !appArmorInUse(oldPodAnnotations) {
|
if !utilfeature.DefaultFeatureGate.Enabled(features.AppArmor) && !appArmorInUse(oldPodAnnotations) {
|
||||||
for k := range podAnnotations {
|
for k := range podAnnotations {
|
||||||
if strings.HasPrefix(k, v1.AppArmorBetaContainerAnnotationKeyPrefix) {
|
if strings.HasPrefix(k, v1.AppArmorBetaContainerAnnotationKeyPrefix) {
|
||||||
@ -593,23 +581,6 @@ func appArmorInUse(podAnnotations map[string]string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func tokenRequestProjectionInUse(podSpec *api.PodSpec) bool {
|
|
||||||
if podSpec == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, v := range podSpec.Volumes {
|
|
||||||
if v.Projected == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, s := range v.Projected.Sources {
|
|
||||||
if s.ServiceAccountToken != nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// podPriorityInUse returns true if the pod spec is non-nil and has Priority or PriorityClassName set.
|
// podPriorityInUse returns true if the pod spec is non-nil and has Priority or PriorityClassName set.
|
||||||
func podPriorityInUse(podSpec *api.PodSpec) bool {
|
func podPriorityInUse(podSpec *api.PodSpec) bool {
|
||||||
if podSpec == nil {
|
if podSpec == nil {
|
||||||
|
@ -24,7 +24,7 @@ import (
|
|||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
|
|
||||||
"k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
"k8s.io/apimachinery/pkg/api/resource"
|
"k8s.io/apimachinery/pkg/api/resource"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/util/diff"
|
"k8s.io/apimachinery/pkg/util/diff"
|
||||||
@ -1106,122 +1106,6 @@ func TestDropAppArmor(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDropTokenRequestProjection(t *testing.T) {
|
|
||||||
podWithoutTRProjection := func() *api.Pod {
|
|
||||||
return &api.Pod{
|
|
||||||
Spec: api.PodSpec{
|
|
||||||
Volumes: []api.Volume{{
|
|
||||||
VolumeSource: api.VolumeSource{
|
|
||||||
Projected: &api.ProjectedVolumeSource{
|
|
||||||
Sources: []api.VolumeProjection{{
|
|
||||||
ServiceAccountToken: nil,
|
|
||||||
}},
|
|
||||||
}}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
podWithoutProjectedVolumeSource := func() *api.Pod {
|
|
||||||
return &api.Pod{
|
|
||||||
Spec: api.PodSpec{
|
|
||||||
Volumes: []api.Volume{
|
|
||||||
{VolumeSource: api.VolumeSource{
|
|
||||||
ConfigMap: &api.ConfigMapVolumeSource{},
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
podWithTRProjection := func() *api.Pod {
|
|
||||||
return &api.Pod{
|
|
||||||
Spec: api.PodSpec{
|
|
||||||
Volumes: []api.Volume{{
|
|
||||||
VolumeSource: api.VolumeSource{
|
|
||||||
Projected: &api.ProjectedVolumeSource{
|
|
||||||
Sources: []api.VolumeProjection{{
|
|
||||||
ServiceAccountToken: &api.ServiceAccountTokenProjection{
|
|
||||||
Audience: "api",
|
|
||||||
ExpirationSeconds: 3600,
|
|
||||||
Path: "token",
|
|
||||||
}},
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
podInfo := []struct {
|
|
||||||
description string
|
|
||||||
hasTRProjection bool
|
|
||||||
pod func() *api.Pod
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
description: "has TokenRequestProjection",
|
|
||||||
hasTRProjection: true,
|
|
||||||
pod: podWithTRProjection,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "does not have TokenRequestProjection",
|
|
||||||
hasTRProjection: false,
|
|
||||||
pod: podWithoutTRProjection,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "does not have ProjectedVolumeSource",
|
|
||||||
hasTRProjection: false,
|
|
||||||
pod: podWithoutProjectedVolumeSource,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "is nil",
|
|
||||||
hasTRProjection: false,
|
|
||||||
pod: func() *api.Pod { return nil },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, enabled := range []bool{true, false} {
|
|
||||||
for _, oldPodInfo := range podInfo {
|
|
||||||
for _, newPodInfo := range podInfo {
|
|
||||||
oldPodhasTRProjection, oldPod := oldPodInfo.hasTRProjection, oldPodInfo.pod()
|
|
||||||
newPodhasTRProjection, newPod := newPodInfo.hasTRProjection, newPodInfo.pod()
|
|
||||||
if newPod == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
t.Run(fmt.Sprintf("feature enabled=%v, old pod %v, new pod %v", enabled, oldPodInfo.description, newPodInfo.description), func(t *testing.T) {
|
|
||||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.TokenRequestProjection, enabled)()
|
|
||||||
var oldPodSpec *api.PodSpec
|
|
||||||
if oldPod != nil {
|
|
||||||
oldPodSpec = &oldPod.Spec
|
|
||||||
}
|
|
||||||
dropDisabledFields(&newPod.Spec, nil, oldPodSpec, nil)
|
|
||||||
// old pod should never be changed
|
|
||||||
if !reflect.DeepEqual(oldPod, oldPodInfo.pod()) {
|
|
||||||
t.Errorf("old pod changed: %v", diff.ObjectReflectDiff(oldPod, oldPodInfo.pod()))
|
|
||||||
}
|
|
||||||
switch {
|
|
||||||
case enabled || oldPodhasTRProjection:
|
|
||||||
if !reflect.DeepEqual(newPod, newPodInfo.pod()) {
|
|
||||||
t.Errorf("new pod changed: %v", diff.ObjectReflectDiff(newPod, newPodInfo.pod()))
|
|
||||||
}
|
|
||||||
case newPodhasTRProjection:
|
|
||||||
// new pod should be changed
|
|
||||||
if reflect.DeepEqual(newPod, newPodInfo.pod()) {
|
|
||||||
t.Errorf("%v", oldPod)
|
|
||||||
t.Errorf("%v", newPod)
|
|
||||||
t.Errorf("new pod was not changed")
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(newPod, podWithoutTRProjection()) {
|
|
||||||
t.Errorf("new pod had Tokenrequestprojection: %v", diff.ObjectReflectDiff(newPod, podWithoutTRProjection()))
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
// new pod should not need to be changed
|
|
||||||
if !reflect.DeepEqual(newPod, newPodInfo.pod()) {
|
|
||||||
t.Errorf("new pod changed: %v", diff.ObjectReflectDiff(newPod, newPodInfo.pod()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDropRunAsGroup(t *testing.T) {
|
func TestDropRunAsGroup(t *testing.T) {
|
||||||
group := func() *int64 {
|
group := func() *int64 {
|
||||||
testGroup := int64(1000)
|
testGroup := int64(1000)
|
||||||
|
@ -211,12 +211,14 @@ const (
|
|||||||
|
|
||||||
// owner: @mikedanese
|
// owner: @mikedanese
|
||||||
// beta: v1.12
|
// beta: v1.12
|
||||||
|
// ga: v1.20
|
||||||
//
|
//
|
||||||
// Implement TokenRequest endpoint on service account resources.
|
// Implement TokenRequest endpoint on service account resources.
|
||||||
TokenRequest featuregate.Feature = "TokenRequest"
|
TokenRequest featuregate.Feature = "TokenRequest"
|
||||||
|
|
||||||
// owner: @mikedanese
|
// owner: @mikedanese
|
||||||
// beta: v1.12
|
// beta: v1.12
|
||||||
|
// ga: v1.20
|
||||||
//
|
//
|
||||||
// Enable ServiceAccountTokenVolumeProjection support in ProjectedVolumes.
|
// Enable ServiceAccountTokenVolumeProjection support in ProjectedVolumes.
|
||||||
TokenRequestProjection featuregate.Feature = "TokenRequestProjection"
|
TokenRequestProjection featuregate.Feature = "TokenRequestProjection"
|
||||||
@ -686,8 +688,8 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS
|
|||||||
SupportPodPidsLimit: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.21
|
SupportPodPidsLimit: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.21
|
||||||
SupportNodePidsLimit: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.21
|
SupportNodePidsLimit: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.21
|
||||||
HyperVContainer: {Default: false, PreRelease: featuregate.Deprecated},
|
HyperVContainer: {Default: false, PreRelease: featuregate.Deprecated},
|
||||||
TokenRequest: {Default: true, PreRelease: featuregate.Beta},
|
TokenRequest: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.21
|
||||||
TokenRequestProjection: {Default: true, PreRelease: featuregate.Beta},
|
TokenRequestProjection: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.21
|
||||||
BoundServiceAccountTokenVolume: {Default: false, PreRelease: featuregate.Alpha},
|
BoundServiceAccountTokenVolume: {Default: false, PreRelease: featuregate.Alpha},
|
||||||
ServiceAccountIssuerDiscovery: {Default: true, PreRelease: featuregate.Beta},
|
ServiceAccountIssuerDiscovery: {Default: true, PreRelease: featuregate.Beta},
|
||||||
CRIContainerLogRotation: {Default: true, PreRelease: featuregate.Beta},
|
CRIContainerLogRotation: {Default: true, PreRelease: featuregate.Beta},
|
||||||
|
@ -10,7 +10,6 @@ go_library(
|
|||||||
srcs = ["config.go"],
|
srcs = ["config.go"],
|
||||||
importpath = "k8s.io/kubernetes/pkg/kubeapiserver/authenticator",
|
importpath = "k8s.io/kubernetes/pkg/kubeapiserver/authenticator",
|
||||||
deps = [
|
deps = [
|
||||||
"//pkg/features:go_default_library",
|
|
||||||
"//pkg/serviceaccount:go_default_library",
|
"//pkg/serviceaccount:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/util/net:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/util/net:go_default_library",
|
||||||
"//staging/src/k8s.io/apiserver/pkg/authentication/authenticator:go_default_library",
|
"//staging/src/k8s.io/apiserver/pkg/authentication/authenticator:go_default_library",
|
||||||
@ -26,7 +25,6 @@ go_library(
|
|||||||
"//staging/src/k8s.io/apiserver/pkg/authentication/token/tokenfile:go_default_library",
|
"//staging/src/k8s.io/apiserver/pkg/authentication/token/tokenfile:go_default_library",
|
||||||
"//staging/src/k8s.io/apiserver/pkg/authentication/token/union:go_default_library",
|
"//staging/src/k8s.io/apiserver/pkg/authentication/token/union:go_default_library",
|
||||||
"//staging/src/k8s.io/apiserver/pkg/server/dynamiccertificates:go_default_library",
|
"//staging/src/k8s.io/apiserver/pkg/server/dynamiccertificates:go_default_library",
|
||||||
"//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library",
|
|
||||||
"//staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc:go_default_library",
|
"//staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc:go_default_library",
|
||||||
"//staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/webhook:go_default_library",
|
"//staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/webhook:go_default_library",
|
||||||
"//staging/src/k8s.io/client-go/plugin/pkg/client/auth:go_default_library",
|
"//staging/src/k8s.io/client-go/plugin/pkg/client/auth:go_default_library",
|
||||||
|
@ -35,14 +35,12 @@ import (
|
|||||||
"k8s.io/apiserver/pkg/authentication/token/tokenfile"
|
"k8s.io/apiserver/pkg/authentication/token/tokenfile"
|
||||||
tokenunion "k8s.io/apiserver/pkg/authentication/token/union"
|
tokenunion "k8s.io/apiserver/pkg/authentication/token/union"
|
||||||
"k8s.io/apiserver/pkg/server/dynamiccertificates"
|
"k8s.io/apiserver/pkg/server/dynamiccertificates"
|
||||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
|
||||||
"k8s.io/apiserver/plugin/pkg/authenticator/token/oidc"
|
"k8s.io/apiserver/plugin/pkg/authenticator/token/oidc"
|
||||||
"k8s.io/apiserver/plugin/pkg/authenticator/token/webhook"
|
"k8s.io/apiserver/plugin/pkg/authenticator/token/webhook"
|
||||||
|
|
||||||
// Initialize all known client auth plugins.
|
// Initialize all known client auth plugins.
|
||||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||||
"k8s.io/client-go/util/keyutil"
|
"k8s.io/client-go/util/keyutil"
|
||||||
"k8s.io/kubernetes/pkg/features"
|
|
||||||
"k8s.io/kubernetes/pkg/serviceaccount"
|
"k8s.io/kubernetes/pkg/serviceaccount"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -127,7 +125,7 @@ func (config Config) New() (authenticator.Request, *spec.SecurityDefinitions, er
|
|||||||
}
|
}
|
||||||
tokenAuthenticators = append(tokenAuthenticators, serviceAccountAuth)
|
tokenAuthenticators = append(tokenAuthenticators, serviceAccountAuth)
|
||||||
}
|
}
|
||||||
if utilfeature.DefaultFeatureGate.Enabled(features.TokenRequest) && config.ServiceAccountIssuer != "" {
|
if config.ServiceAccountIssuer != "" {
|
||||||
serviceAccountAuth, err := newServiceAccountAuthenticator(config.ServiceAccountIssuer, config.ServiceAccountKeyFiles, config.APIAudiences, config.ServiceAccountTokenGetter)
|
serviceAccountAuth, err := newServiceAccountAuthenticator(config.ServiceAccountIssuer, config.ServiceAccountKeyFiles, config.APIAudiences, config.ServiceAccountTokenGetter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
|
@ -192,10 +192,6 @@ func (o *BuiltInAuthenticationOptions) Validate() []error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if o.ServiceAccounts != nil && utilfeature.DefaultFeatureGate.Enabled(features.BoundServiceAccountTokenVolume) {
|
if o.ServiceAccounts != nil && utilfeature.DefaultFeatureGate.Enabled(features.BoundServiceAccountTokenVolume) {
|
||||||
if !utilfeature.DefaultFeatureGate.Enabled(features.TokenRequest) || !utilfeature.DefaultFeatureGate.Enabled(features.TokenRequestProjection) {
|
|
||||||
allErrors = append(allErrors, errors.New("if the BoundServiceAccountTokenVolume feature is enabled,"+
|
|
||||||
" the TokenRequest and TokenRequestProjection features must also be enabled"))
|
|
||||||
}
|
|
||||||
if len(o.ServiceAccounts.Issuer) == 0 {
|
if len(o.ServiceAccounts.Issuer) == 0 {
|
||||||
allErrors = append(allErrors, errors.New("service-account-issuer is a required flag when BoundServiceAccountTokenVolume is enabled"))
|
allErrors = append(allErrors, errors.New("service-account-issuer is a required flag when BoundServiceAccountTokenVolume is enabled"))
|
||||||
}
|
}
|
||||||
@ -313,7 +309,7 @@ func (o *BuiltInAuthenticationOptions) AddFlags(fs *pflag.FlagSet) {
|
|||||||
"that this value comply with the OpenID spec: https://openid.net/specs/openid-connect-discovery-1_0.html. "+
|
"that this value comply with the OpenID spec: https://openid.net/specs/openid-connect-discovery-1_0.html. "+
|
||||||
"In practice, this means that service-account-issuer must be an https URL. It is also highly "+
|
"In practice, this means that service-account-issuer must be an https URL. It is also highly "+
|
||||||
"recommended that this URL be capable of serving OpenID discovery documents at "+
|
"recommended that this URL be capable of serving OpenID discovery documents at "+
|
||||||
"`{service-account-issuer}/.well-known/openid-configuration`.")
|
"{service-account-issuer}/.well-known/openid-configuration.")
|
||||||
|
|
||||||
fs.StringVar(&o.ServiceAccounts.JWKSURI, "service-account-jwks-uri", o.ServiceAccounts.JWKSURI, ""+
|
fs.StringVar(&o.ServiceAccounts.JWKSURI, "service-account-jwks-uri", o.ServiceAccounts.JWKSURI, ""+
|
||||||
"Overrides the URI for the JSON Web Key Set in the discovery doc served at "+
|
"Overrides the URI for the JSON Web Key Set in the discovery doc served at "+
|
||||||
@ -464,14 +460,13 @@ func (o *BuiltInAuthenticationOptions) ApplyTo(authInfo *genericapiserver.Authen
|
|||||||
authInfo.APIAudiences = authenticator.Audiences{o.ServiceAccounts.Issuer}
|
authInfo.APIAudiences = authenticator.Audiences{o.ServiceAccounts.Issuer}
|
||||||
}
|
}
|
||||||
|
|
||||||
if o.ServiceAccounts.Lookup || utilfeature.DefaultFeatureGate.Enabled(features.TokenRequest) {
|
|
||||||
authenticatorConfig.ServiceAccountTokenGetter = serviceaccountcontroller.NewGetterFromClient(
|
authenticatorConfig.ServiceAccountTokenGetter = serviceaccountcontroller.NewGetterFromClient(
|
||||||
extclient,
|
extclient,
|
||||||
versionedInformer.Core().V1().Secrets().Lister(),
|
versionedInformer.Core().V1().Secrets().Lister(),
|
||||||
versionedInformer.Core().V1().ServiceAccounts().Lister(),
|
versionedInformer.Core().V1().ServiceAccounts().Lister(),
|
||||||
versionedInformer.Core().V1().Pods().Lister(),
|
versionedInformer.Core().V1().Pods().Lister(),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
authenticatorConfig.BootstrapTokenAuthenticator = bootstrap.NewTokenAuthenticator(
|
authenticatorConfig.BootstrapTokenAuthenticator = bootstrap.NewTokenAuthenticator(
|
||||||
versionedInformer.Core().V1().Secrets().Lister().Secrets(metav1.NamespaceSystem),
|
versionedInformer.Core().V1().Secrets().Lister().Secrets(metav1.NamespaceSystem),
|
||||||
)
|
)
|
||||||
|
@ -181,7 +181,7 @@ func (c LegacyRESTStorageProvider) NewLegacyRESTStorage(restOptionsGetter generi
|
|||||||
}
|
}
|
||||||
|
|
||||||
var serviceAccountStorage *serviceaccountstore.REST
|
var serviceAccountStorage *serviceaccountstore.REST
|
||||||
if c.ServiceAccountIssuer != nil && utilfeature.DefaultFeatureGate.Enabled(features.TokenRequest) {
|
if c.ServiceAccountIssuer != nil {
|
||||||
serviceAccountStorage, err = serviceaccountstore.NewREST(restOptionsGetter, c.ServiceAccountIssuer, c.APIAudiences, c.ServiceAccountMaxExpiration, podStorage.Pod.Store, secretStorage.Store, c.ExtendExpiration)
|
serviceAccountStorage, err = serviceaccountstore.NewREST(restOptionsGetter, c.ServiceAccountIssuer, c.APIAudiences, c.ServiceAccountMaxExpiration, podStorage.Pod.Store, secretStorage.Store, c.ExtendExpiration)
|
||||||
} else {
|
} else {
|
||||||
serviceAccountStorage, err = serviceaccountstore.NewREST(restOptionsGetter, nil, nil, 0, nil, nil, false)
|
serviceAccountStorage, err = serviceaccountstore.NewREST(restOptionsGetter, nil, nil, 0, nil, nil, false)
|
||||||
|
@ -36,7 +36,6 @@ go_library(
|
|||||||
srcs = ["projected.go"],
|
srcs = ["projected.go"],
|
||||||
importpath = "k8s.io/kubernetes/pkg/volume/projected",
|
importpath = "k8s.io/kubernetes/pkg/volume/projected",
|
||||||
deps = [
|
deps = [
|
||||||
"//pkg/features:go_default_library",
|
|
||||||
"//pkg/volume:go_default_library",
|
"//pkg/volume:go_default_library",
|
||||||
"//pkg/volume/configmap:go_default_library",
|
"//pkg/volume/configmap:go_default_library",
|
||||||
"//pkg/volume/downwardapi:go_default_library",
|
"//pkg/volume/downwardapi:go_default_library",
|
||||||
@ -48,7 +47,6 @@ go_library(
|
|||||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/types:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/types:go_default_library",
|
||||||
"//staging/src/k8s.io/apimachinery/pkg/util/errors:go_default_library",
|
"//staging/src/k8s.io/apimachinery/pkg/util/errors:go_default_library",
|
||||||
"//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library",
|
|
||||||
"//vendor/k8s.io/klog/v2:go_default_library",
|
"//vendor/k8s.io/klog/v2:go_default_library",
|
||||||
"//vendor/k8s.io/utils/strings:go_default_library",
|
"//vendor/k8s.io/utils/strings:go_default_library",
|
||||||
],
|
],
|
||||||
|
@ -25,9 +25,7 @@ import (
|
|||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
|
||||||
"k8s.io/klog/v2"
|
"k8s.io/klog/v2"
|
||||||
"k8s.io/kubernetes/pkg/features"
|
|
||||||
"k8s.io/kubernetes/pkg/volume"
|
"k8s.io/kubernetes/pkg/volume"
|
||||||
"k8s.io/kubernetes/pkg/volume/configmap"
|
"k8s.io/kubernetes/pkg/volume/configmap"
|
||||||
"k8s.io/kubernetes/pkg/volume/downwardapi"
|
"k8s.io/kubernetes/pkg/volume/downwardapi"
|
||||||
@ -322,10 +320,6 @@ func (s *projectedVolumeMounter) collectData(mounterArgs volume.MounterArgs) (ma
|
|||||||
payload[k] = v
|
payload[k] = v
|
||||||
}
|
}
|
||||||
case source.ServiceAccountToken != nil:
|
case source.ServiceAccountToken != nil:
|
||||||
if !utilfeature.DefaultFeatureGate.Enabled(features.TokenRequestProjection) {
|
|
||||||
errlist = append(errlist, fmt.Errorf("pod request ServiceAccountToken projection but the TokenRequestProjection feature was not enabled"))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
tp := source.ServiceAccountToken
|
tp := source.ServiceAccountToken
|
||||||
|
|
||||||
// When FsGroup is set, we depend on SetVolumeOwnership to
|
// When FsGroup is set, we depend on SetVolumeOwnership to
|
||||||
|
@ -71,7 +71,6 @@ type Plugin struct {
|
|||||||
podsGetter corev1lister.PodLister
|
podsGetter corev1lister.PodLister
|
||||||
nodesGetter corev1lister.NodeLister
|
nodesGetter corev1lister.NodeLister
|
||||||
|
|
||||||
tokenRequestEnabled bool
|
|
||||||
csiNodeInfoEnabled bool
|
csiNodeInfoEnabled bool
|
||||||
expandPersistentVolumesEnabled bool
|
expandPersistentVolumesEnabled bool
|
||||||
}
|
}
|
||||||
@ -84,7 +83,6 @@ var (
|
|||||||
|
|
||||||
// InspectFeatureGates allows setting bools without taking a dep on a global variable
|
// InspectFeatureGates allows setting bools without taking a dep on a global variable
|
||||||
func (p *Plugin) InspectFeatureGates(featureGates featuregate.FeatureGate) {
|
func (p *Plugin) InspectFeatureGates(featureGates featuregate.FeatureGate) {
|
||||||
p.tokenRequestEnabled = featureGates.Enabled(features.TokenRequest)
|
|
||||||
p.csiNodeInfoEnabled = featureGates.Enabled(features.CSINodeInfo)
|
p.csiNodeInfoEnabled = featureGates.Enabled(features.CSINodeInfo)
|
||||||
p.expandPersistentVolumesEnabled = featureGates.Enabled(features.ExpandPersistentVolumes)
|
p.expandPersistentVolumesEnabled = featureGates.Enabled(features.ExpandPersistentVolumes)
|
||||||
}
|
}
|
||||||
@ -159,10 +157,7 @@ func (p *Plugin) Admit(ctx context.Context, a admission.Attributes, o admission.
|
|||||||
}
|
}
|
||||||
|
|
||||||
case svcacctResource:
|
case svcacctResource:
|
||||||
if p.tokenRequestEnabled {
|
|
||||||
return p.admitServiceAccount(nodeName, a)
|
return p.admitServiceAccount(nodeName, a)
|
||||||
}
|
|
||||||
return nil
|
|
||||||
|
|
||||||
case leaseResource:
|
case leaseResource:
|
||||||
return p.admitLease(nodeName, a)
|
return p.admitLease(nodeName, a)
|
||||||
|
@ -48,7 +48,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
trEnabledFeature = featuregate.NewFeatureGate()
|
|
||||||
csiNodeInfoEnabledFeature = featuregate.NewFeatureGate()
|
csiNodeInfoEnabledFeature = featuregate.NewFeatureGate()
|
||||||
csiNodeInfoDisabledFeature = featuregate.NewFeatureGate()
|
csiNodeInfoDisabledFeature = featuregate.NewFeatureGate()
|
||||||
)
|
)
|
||||||
@ -56,15 +55,12 @@ var (
|
|||||||
func init() {
|
func init() {
|
||||||
// all features need to be set on all featuregates for the tests. We set everything and then then the if's below override it.
|
// all features need to be set on all featuregates for the tests. We set everything and then then the if's below override it.
|
||||||
relevantFeatures := map[featuregate.Feature]featuregate.FeatureSpec{
|
relevantFeatures := map[featuregate.Feature]featuregate.FeatureSpec{
|
||||||
features.TokenRequest: {Default: false},
|
|
||||||
features.CSINodeInfo: {Default: false},
|
features.CSINodeInfo: {Default: false},
|
||||||
features.ExpandPersistentVolumes: {Default: false},
|
features.ExpandPersistentVolumes: {Default: false},
|
||||||
}
|
}
|
||||||
utilruntime.Must(trEnabledFeature.Add(relevantFeatures))
|
|
||||||
utilruntime.Must(csiNodeInfoEnabledFeature.Add(relevantFeatures))
|
utilruntime.Must(csiNodeInfoEnabledFeature.Add(relevantFeatures))
|
||||||
utilruntime.Must(csiNodeInfoDisabledFeature.Add(relevantFeatures))
|
utilruntime.Must(csiNodeInfoDisabledFeature.Add(relevantFeatures))
|
||||||
|
|
||||||
utilruntime.Must(trEnabledFeature.SetFromMap(map[string]bool{string(features.TokenRequest): true}))
|
|
||||||
utilruntime.Must(csiNodeInfoEnabledFeature.SetFromMap(map[string]bool{string(features.CSINodeInfo): true}))
|
utilruntime.Must(csiNodeInfoEnabledFeature.SetFromMap(map[string]bool{string(features.CSINodeInfo): true}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1086,35 +1082,30 @@ func Test_nodePlugin_Admit(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "forbid create of unbound token",
|
name: "forbid create of unbound token",
|
||||||
podsGetter: noExistingPods,
|
podsGetter: noExistingPods,
|
||||||
features: trEnabledFeature,
|
|
||||||
attributes: admission.NewAttributesRecord(makeTokenRequest("", ""), nil, tokenrequestKind, "ns", "mysa", svcacctResource, "token", admission.Create, &metav1.CreateOptions{}, false, mynode),
|
attributes: admission.NewAttributesRecord(makeTokenRequest("", ""), nil, tokenrequestKind, "ns", "mysa", svcacctResource, "token", admission.Create, &metav1.CreateOptions{}, false, mynode),
|
||||||
err: "not bound to a pod",
|
err: "not bound to a pod",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "forbid create of token bound to nonexistant pod",
|
name: "forbid create of token bound to nonexistant pod",
|
||||||
podsGetter: noExistingPods,
|
podsGetter: noExistingPods,
|
||||||
features: trEnabledFeature,
|
|
||||||
attributes: admission.NewAttributesRecord(makeTokenRequest("nopod", "someuid"), nil, tokenrequestKind, "ns", "mysa", svcacctResource, "token", admission.Create, &metav1.CreateOptions{}, false, mynode),
|
attributes: admission.NewAttributesRecord(makeTokenRequest("nopod", "someuid"), nil, tokenrequestKind, "ns", "mysa", svcacctResource, "token", admission.Create, &metav1.CreateOptions{}, false, mynode),
|
||||||
err: "not found",
|
err: "not found",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "forbid create of token bound to pod without uid",
|
name: "forbid create of token bound to pod without uid",
|
||||||
podsGetter: existingPods,
|
podsGetter: existingPods,
|
||||||
features: trEnabledFeature,
|
|
||||||
attributes: admission.NewAttributesRecord(makeTokenRequest(coremypod.Name, ""), nil, tokenrequestKind, "ns", "mysa", svcacctResource, "token", admission.Create, &metav1.CreateOptions{}, false, mynode),
|
attributes: admission.NewAttributesRecord(makeTokenRequest(coremypod.Name, ""), nil, tokenrequestKind, "ns", "mysa", svcacctResource, "token", admission.Create, &metav1.CreateOptions{}, false, mynode),
|
||||||
err: "pod binding without a uid",
|
err: "pod binding without a uid",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "forbid create of token bound to pod scheduled on another node",
|
name: "forbid create of token bound to pod scheduled on another node",
|
||||||
podsGetter: existingPods,
|
podsGetter: existingPods,
|
||||||
features: trEnabledFeature,
|
|
||||||
attributes: admission.NewAttributesRecord(makeTokenRequest(coreotherpod.Name, coreotherpod.UID), nil, tokenrequestKind, coreotherpod.Namespace, "mysa", svcacctResource, "token", admission.Create, &metav1.CreateOptions{}, false, mynode),
|
attributes: admission.NewAttributesRecord(makeTokenRequest(coreotherpod.Name, coreotherpod.UID), nil, tokenrequestKind, coreotherpod.Namespace, "mysa", svcacctResource, "token", admission.Create, &metav1.CreateOptions{}, false, mynode),
|
||||||
err: "pod scheduled on a different node",
|
err: "pod scheduled on a different node",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "allow create of token bound to pod scheduled this node",
|
name: "allow create of token bound to pod scheduled this node",
|
||||||
podsGetter: existingPods,
|
podsGetter: existingPods,
|
||||||
features: trEnabledFeature,
|
|
||||||
attributes: admission.NewAttributesRecord(makeTokenRequest(coremypod.Name, coremypod.UID), nil, tokenrequestKind, coremypod.Namespace, "mysa", svcacctResource, "token", admission.Create, &metav1.CreateOptions{}, false, mynode),
|
attributes: admission.NewAttributesRecord(makeTokenRequest(coremypod.Name, coremypod.UID), nil, tokenrequestKind, coremypod.Namespace, "mysa", svcacctResource, "token", admission.Create, &metav1.CreateOptions{}, false, mynode),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -123,10 +123,7 @@ func (r *NodeAuthorizer) Authorize(ctx context.Context, attrs authorizer.Attribu
|
|||||||
case vaResource:
|
case vaResource:
|
||||||
return r.authorizeGet(nodeName, vaVertexType, attrs)
|
return r.authorizeGet(nodeName, vaVertexType, attrs)
|
||||||
case svcAcctResource:
|
case svcAcctResource:
|
||||||
if r.features.Enabled(features.TokenRequest) {
|
|
||||||
return r.authorizeCreateToken(nodeName, serviceAccountVertexType, attrs)
|
return r.authorizeCreateToken(nodeName, serviceAccountVertexType, attrs)
|
||||||
}
|
|
||||||
return authorizer.DecisionNoOpinion, fmt.Sprintf("disabled by feature gate %s", features.TokenRequest), nil
|
|
||||||
case leaseResource:
|
case leaseResource:
|
||||||
return r.authorizeLease(nodeName, attrs)
|
return r.authorizeLease(nodeName, attrs)
|
||||||
case csiNodeResource:
|
case csiNodeResource:
|
||||||
|
@ -42,19 +42,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
trEnabledFeature = featuregate.NewFeatureGate()
|
|
||||||
trDisabledFeature = featuregate.NewFeatureGate()
|
|
||||||
csiNodeInfoEnabledFeature = featuregate.NewFeatureGate()
|
csiNodeInfoEnabledFeature = featuregate.NewFeatureGate()
|
||||||
csiNodeInfoDisabledFeature = featuregate.NewFeatureGate()
|
csiNodeInfoDisabledFeature = featuregate.NewFeatureGate()
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
if err := trEnabledFeature.Add(map[featuregate.Feature]featuregate.FeatureSpec{features.TokenRequest: {Default: true}}); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
if err := trDisabledFeature.Add(map[featuregate.Feature]featuregate.FeatureSpec{features.TokenRequest: {Default: false}}); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
if err := csiNodeInfoEnabledFeature.Add(map[featuregate.Feature]featuregate.FeatureSpec{features.CSINodeInfo: {Default: true}}); err != nil {
|
if err := csiNodeInfoEnabledFeature.Add(map[featuregate.Feature]featuregate.FeatureSpec{features.CSINodeInfo: {Default: true}}); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@ -189,33 +181,23 @@ func TestAuthorizer(t *testing.T) {
|
|||||||
expect: authorizer.DecisionAllow,
|
expect: authorizer.DecisionAllow,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "allowed svcacct token create - feature enabled",
|
name: "allowed svcacct token create",
|
||||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "create", Resource: "serviceaccounts", Subresource: "token", Name: "svcacct0-node0", Namespace: "ns0"},
|
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "create", Resource: "serviceaccounts", Subresource: "token", Name: "svcacct0-node0", Namespace: "ns0"},
|
||||||
features: trEnabledFeature,
|
|
||||||
expect: authorizer.DecisionAllow,
|
expect: authorizer.DecisionAllow,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "disallowed svcacct token create - serviceaccount not attached to node",
|
name: "disallowed svcacct token create - serviceaccount not attached to node",
|
||||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "create", Resource: "serviceaccounts", Subresource: "token", Name: "svcacct0-node1", Namespace: "ns0"},
|
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "create", Resource: "serviceaccounts", Subresource: "token", Name: "svcacct0-node1", Namespace: "ns0"},
|
||||||
features: trEnabledFeature,
|
|
||||||
expect: authorizer.DecisionNoOpinion,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "disallowed svcacct token create - feature disabled",
|
|
||||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "create", Resource: "serviceaccounts", Subresource: "token", Name: "svcacct0-node0", Namespace: "ns0"},
|
|
||||||
features: trDisabledFeature,
|
|
||||||
expect: authorizer.DecisionNoOpinion,
|
expect: authorizer.DecisionNoOpinion,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "disallowed svcacct token create - no subresource",
|
name: "disallowed svcacct token create - no subresource",
|
||||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "create", Resource: "serviceaccounts", Name: "svcacct0-node0", Namespace: "ns0"},
|
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "create", Resource: "serviceaccounts", Name: "svcacct0-node0", Namespace: "ns0"},
|
||||||
features: trEnabledFeature,
|
|
||||||
expect: authorizer.DecisionNoOpinion,
|
expect: authorizer.DecisionNoOpinion,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "disallowed svcacct token create - non create",
|
name: "disallowed svcacct token create - non create",
|
||||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "update", Resource: "serviceaccounts", Subresource: "token", Name: "svcacct0-node0", Namespace: "ns0"},
|
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "update", Resource: "serviceaccounts", Subresource: "token", Name: "svcacct0-node0", Namespace: "ns0"},
|
||||||
features: trEnabledFeature,
|
|
||||||
expect: authorizer.DecisionNoOpinion,
|
expect: authorizer.DecisionNoOpinion,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -153,6 +153,10 @@ func NodeRules() []rbacv1.PolicyRule {
|
|||||||
|
|
||||||
// CSI
|
// CSI
|
||||||
rbacv1helpers.NewRule("get").Groups(storageGroup).Resources("volumeattachments").RuleOrDie(),
|
rbacv1helpers.NewRule("get").Groups(storageGroup).Resources("volumeattachments").RuleOrDie(),
|
||||||
|
|
||||||
|
// Use the Node authorization to limit a node to create tokens for service accounts running on that node
|
||||||
|
// Use the NodeRestriction admission plugin to limit a node to create tokens bound to pods on that node
|
||||||
|
rbacv1helpers.NewRule("create").Groups(legacyGroup).Resources("serviceaccounts/token").RuleOrDie(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if utilfeature.DefaultFeatureGate.Enabled(features.ExpandPersistentVolumes) {
|
if utilfeature.DefaultFeatureGate.Enabled(features.ExpandPersistentVolumes) {
|
||||||
@ -162,13 +166,6 @@ func NodeRules() []rbacv1.PolicyRule {
|
|||||||
nodePolicyRules = append(nodePolicyRules, pvcStatusPolicyRule)
|
nodePolicyRules = append(nodePolicyRules, pvcStatusPolicyRule)
|
||||||
}
|
}
|
||||||
|
|
||||||
if utilfeature.DefaultFeatureGate.Enabled(features.TokenRequest) {
|
|
||||||
// Use the Node authorization to limit a node to create tokens for service accounts running on that node
|
|
||||||
// Use the NodeRestriction admission plugin to limit a node to create tokens bound to pods on that node
|
|
||||||
tokenRequestRule := rbacv1helpers.NewRule("create").Groups(legacyGroup).Resources("serviceaccounts/token").RuleOrDie()
|
|
||||||
nodePolicyRules = append(nodePolicyRules, tokenRequestRule)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CSI
|
// CSI
|
||||||
csiDriverRule := rbacv1helpers.NewRule("get", "watch", "list").Groups("storage.k8s.io").Resources("csidrivers").RuleOrDie()
|
csiDriverRule := rbacv1helpers.NewRule("get", "watch", "list").Groups("storage.k8s.io").Resources("csidrivers").RuleOrDie()
|
||||||
nodePolicyRules = append(nodePolicyRules, csiDriverRule)
|
nodePolicyRules = append(nodePolicyRules, csiDriverRule)
|
||||||
|
@ -1018,6 +1018,12 @@ items:
|
|||||||
- volumeattachments
|
- volumeattachments
|
||||||
verbs:
|
verbs:
|
||||||
- get
|
- get
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- serviceaccounts/token
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
- apiGroups:
|
- apiGroups:
|
||||||
- ""
|
- ""
|
||||||
resources:
|
resources:
|
||||||
@ -1026,12 +1032,6 @@ items:
|
|||||||
- get
|
- get
|
||||||
- patch
|
- patch
|
||||||
- update
|
- update
|
||||||
- apiGroups:
|
|
||||||
- ""
|
|
||||||
resources:
|
|
||||||
- serviceaccounts/token
|
|
||||||
verbs:
|
|
||||||
- create
|
|
||||||
- apiGroups:
|
- apiGroups:
|
||||||
- storage.k8s.io
|
- storage.k8s.io
|
||||||
resources:
|
resources:
|
||||||
|
@ -41,6 +41,7 @@ import (
|
|||||||
e2epod "k8s.io/kubernetes/test/e2e/framework/pod"
|
e2epod "k8s.io/kubernetes/test/e2e/framework/pod"
|
||||||
e2eskipper "k8s.io/kubernetes/test/e2e/framework/skipper"
|
e2eskipper "k8s.io/kubernetes/test/e2e/framework/skipper"
|
||||||
imageutils "k8s.io/kubernetes/test/utils/image"
|
imageutils "k8s.io/kubernetes/test/utils/image"
|
||||||
|
utilptr "k8s.io/utils/pointer"
|
||||||
|
|
||||||
"github.com/onsi/ginkgo"
|
"github.com/onsi/ginkgo"
|
||||||
)
|
)
|
||||||
@ -418,6 +419,59 @@ var _ = SIGDescribe("ServiceAccounts", func() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/*
|
||||||
|
Release : v1.20
|
||||||
|
Testname: TokenRequestProjection should mount a projected volume with token using TokenRequest API.
|
||||||
|
Description: Ensure that projected service account token is mounted.
|
||||||
|
*/
|
||||||
|
ginkgo.It("should mount projected service account token when requested", func() {
|
||||||
|
|
||||||
|
var (
|
||||||
|
podName = "test-pod-" + string(uuid.NewUUID())
|
||||||
|
volumeName = "test-volume"
|
||||||
|
volumeMountPath = "/test-volume"
|
||||||
|
tokenVolumePath = "/test-volume/token"
|
||||||
|
)
|
||||||
|
|
||||||
|
volumes := []v1.Volume{
|
||||||
|
{
|
||||||
|
Name: volumeName,
|
||||||
|
VolumeSource: v1.VolumeSource{
|
||||||
|
Projected: &v1.ProjectedVolumeSource{
|
||||||
|
Sources: []v1.VolumeProjection{
|
||||||
|
{
|
||||||
|
ServiceAccountToken: &v1.ServiceAccountTokenProjection{
|
||||||
|
Path: "token",
|
||||||
|
ExpirationSeconds: utilptr.Int64Ptr(60 * 60),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
volumeMounts := []v1.VolumeMount{
|
||||||
|
{
|
||||||
|
Name: volumeName,
|
||||||
|
MountPath: volumeMountPath,
|
||||||
|
ReadOnly: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mounttestArgs := []string{
|
||||||
|
"mounttest",
|
||||||
|
fmt.Sprintf("--file_content=%v", tokenVolumePath),
|
||||||
|
}
|
||||||
|
|
||||||
|
pod := e2epod.NewAgnhostPod(f.Namespace.Name, podName, volumes, volumeMounts, nil, mounttestArgs...)
|
||||||
|
pod.Spec.RestartPolicy = v1.RestartPolicyNever
|
||||||
|
|
||||||
|
output := []string{
|
||||||
|
fmt.Sprintf("content of file \"%v\": %s", tokenVolumePath, `[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*`),
|
||||||
|
}
|
||||||
|
|
||||||
|
f.TestContainerOutputRegexp("service account token: ", pod, 0, output)
|
||||||
|
})
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Testname: Projected service account token file ownership and permission.
|
Testname: Projected service account token file ownership and permission.
|
||||||
Description: Ensure that Projected Service Account Token is mounted with
|
Description: Ensure that Projected Service Account Token is mounted with
|
||||||
@ -431,24 +485,17 @@ var _ = SIGDescribe("ServiceAccounts", func() {
|
|||||||
Containers MUST verify that the projected service account token can be
|
Containers MUST verify that the projected service account token can be
|
||||||
read and has correct file mode set including ownership and permission.
|
read and has correct file mode set including ownership and permission.
|
||||||
*/
|
*/
|
||||||
ginkgo.It("should set ownership and permission when RunAsUser or FsGroup is present [LinuxOnly] [NodeFeature:FSGroup] [Feature:TokenRequestProjection]", func() {
|
ginkgo.It("should set ownership and permission when RunAsUser or FsGroup is present [LinuxOnly] [NodeFeature:FSGroup]", func() {
|
||||||
e2eskipper.SkipIfNodeOSDistroIs("windows")
|
e2eskipper.SkipIfNodeOSDistroIs("windows")
|
||||||
|
|
||||||
var (
|
var (
|
||||||
podName = "test-pod-" + string(uuid.NewUUID())
|
podName = "test-pod-" + string(uuid.NewUUID())
|
||||||
containerName = "test-container"
|
|
||||||
volumeName = "test-volume"
|
volumeName = "test-volume"
|
||||||
volumeMountPath = "/test-volume"
|
volumeMountPath = "/test-volume"
|
||||||
tokenVolumePath = "/test-volume/token"
|
tokenVolumePath = "/test-volume/token"
|
||||||
int64p = func(i int64) *int64 { return &i }
|
|
||||||
)
|
)
|
||||||
|
|
||||||
pod := &v1.Pod{
|
volumes := []v1.Volume{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
Name: podName,
|
|
||||||
},
|
|
||||||
Spec: v1.PodSpec{
|
|
||||||
Volumes: []v1.Volume{
|
|
||||||
{
|
{
|
||||||
Name: volumeName,
|
Name: volumeName,
|
||||||
VolumeSource: v1.VolumeSource{
|
VolumeSource: v1.VolumeSource{
|
||||||
@ -457,36 +504,31 @@ var _ = SIGDescribe("ServiceAccounts", func() {
|
|||||||
{
|
{
|
||||||
ServiceAccountToken: &v1.ServiceAccountTokenProjection{
|
ServiceAccountToken: &v1.ServiceAccountTokenProjection{
|
||||||
Path: "token",
|
Path: "token",
|
||||||
ExpirationSeconds: int64p(60 * 60),
|
ExpirationSeconds: utilptr.Int64Ptr(60 * 60),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
Containers: []v1.Container{
|
volumeMounts := []v1.VolumeMount{
|
||||||
{
|
{
|
||||||
Name: containerName,
|
Name: volumeName,
|
||||||
Image: imageutils.GetE2EImage(imageutils.Agnhost),
|
MountPath: volumeMountPath,
|
||||||
Args: []string{
|
ReadOnly: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mounttestArgs := []string{
|
||||||
"mounttest",
|
"mounttest",
|
||||||
fmt.Sprintf("--file_perm=%v", tokenVolumePath),
|
fmt.Sprintf("--file_perm=%v", tokenVolumePath),
|
||||||
fmt.Sprintf("--file_owner=%v", tokenVolumePath),
|
fmt.Sprintf("--file_owner=%v", tokenVolumePath),
|
||||||
fmt.Sprintf("--file_content=%v", tokenVolumePath),
|
fmt.Sprintf("--file_content=%v", tokenVolumePath),
|
||||||
},
|
|
||||||
VolumeMounts: []v1.VolumeMount{
|
|
||||||
{
|
|
||||||
Name: volumeName,
|
|
||||||
MountPath: volumeMountPath,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
RestartPolicy: v1.RestartPolicyNever,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pod := e2epod.NewAgnhostPod(f.Namespace.Name, podName, volumes, volumeMounts, nil, mounttestArgs...)
|
||||||
|
pod.Spec.RestartPolicy = v1.RestartPolicyNever
|
||||||
|
|
||||||
testcases := []struct {
|
testcases := []struct {
|
||||||
runAsUser bool
|
runAsUser bool
|
||||||
fsGroup bool
|
fsGroup bool
|
||||||
@ -531,7 +573,7 @@ var _ = SIGDescribe("ServiceAccounts", func() {
|
|||||||
|
|
||||||
output := []string{
|
output := []string{
|
||||||
fmt.Sprintf("perms of file \"%v\": %s", tokenVolumePath, tc.wantPerm),
|
fmt.Sprintf("perms of file \"%v\": %s", tokenVolumePath, tc.wantPerm),
|
||||||
fmt.Sprintf("content of file \"%v\": %s", tokenVolumePath, ".+"),
|
fmt.Sprintf("content of file \"%v\": %s", tokenVolumePath, `[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*`),
|
||||||
fmt.Sprintf("owner UID of \"%v\": %d", tokenVolumePath, tc.wantUID),
|
fmt.Sprintf("owner UID of \"%v\": %d", tokenVolumePath, tc.wantUID),
|
||||||
fmt.Sprintf("owner GID of \"%v\": %d", tokenVolumePath, tc.wantGID),
|
fmt.Sprintf("owner GID of \"%v\": %d", tokenVolumePath, tc.wantGID),
|
||||||
}
|
}
|
||||||
@ -539,7 +581,7 @@ var _ = SIGDescribe("ServiceAccounts", func() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
ginkgo.It("should support InClusterConfig with token rotation [Slow] [Feature:TokenRequestProjection]", func() {
|
ginkgo.It("should support InClusterConfig with token rotation [Slow]", func() {
|
||||||
cfg, err := framework.LoadConfig()
|
cfg, err := framework.LoadConfig()
|
||||||
framework.ExpectNoError(err)
|
framework.ExpectNoError(err)
|
||||||
|
|
||||||
|
@ -26,21 +26,16 @@ import (
|
|||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||||
"k8s.io/apiserver/pkg/authorization/authorizerfactory"
|
"k8s.io/apiserver/pkg/authorization/authorizerfactory"
|
||||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
|
||||||
clientset "k8s.io/client-go/kubernetes"
|
clientset "k8s.io/client-go/kubernetes"
|
||||||
restclient "k8s.io/client-go/rest"
|
restclient "k8s.io/client-go/rest"
|
||||||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
|
||||||
"k8s.io/kubernetes/cmd/kube-apiserver/app/options"
|
"k8s.io/kubernetes/cmd/kube-apiserver/app/options"
|
||||||
"k8s.io/kubernetes/pkg/controller"
|
"k8s.io/kubernetes/pkg/controller"
|
||||||
"k8s.io/kubernetes/pkg/controlplane"
|
"k8s.io/kubernetes/pkg/controlplane"
|
||||||
"k8s.io/kubernetes/pkg/features"
|
|
||||||
kubeoptions "k8s.io/kubernetes/pkg/kubeapiserver/options"
|
kubeoptions "k8s.io/kubernetes/pkg/kubeapiserver/options"
|
||||||
"k8s.io/kubernetes/test/integration/framework"
|
"k8s.io/kubernetes/test/integration/framework"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDynamicClientBuilder(t *testing.T) {
|
func TestDynamicClientBuilder(t *testing.T) {
|
||||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.TokenRequest, true)()
|
|
||||||
|
|
||||||
tmpfile, err := ioutil.TempFile("/tmp", "key")
|
tmpfile, err := ioutil.TempFile("/tmp", "key")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("create temp file failed: %v", err)
|
t.Fatalf("create temp file failed: %v", err)
|
||||||
|
@ -64,7 +64,6 @@ AwEHoUQDQgAEH6cuzP8XuD5wal6wf9M6xDljTOPLX2i8uIp/C/ASqiIGUeeKQtX0
|
|||||||
-----END EC PRIVATE KEY-----`
|
-----END EC PRIVATE KEY-----`
|
||||||
|
|
||||||
func TestServiceAccountTokenCreate(t *testing.T) {
|
func TestServiceAccountTokenCreate(t *testing.T) {
|
||||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.TokenRequest, true)()
|
|
||||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServiceAccountIssuerDiscovery, true)()
|
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServiceAccountIssuerDiscovery, true)()
|
||||||
|
|
||||||
// Build client config, clientset, and informers
|
// Build client config, clientset, and informers
|
||||||
|
Loading…
Reference in New Issue
Block a user