Skip to content

Commit

Permalink
[operator] Introduce gardenerapiserver Golang component (gardener#8215
Browse files Browse the repository at this point in the history
)

* Make `kubeapiserver` values and structs reusable

* Add `gardenerapiserver` component boilerplate

* ------------ empty separator commit -------------

* Generate server certificate secret

ref https://github.com/gardener/gardener/blob/master/charts/gardener/controlplane/charts/runtime/templates/apiserver/secret-cert.yaml

* Generate etcd encryption config

Code from `kubeapiserver` component was made reusable

Ref https://github.com/gardener/gardener/blob/master/charts/gardener/controlplane/charts/runtime/templates/apiserver/secret-encryption-config.yaml

* Generate access secret for virtual garden cluster

Ref https://github.com/gardener/gardener/blob/master/charts/gardener/controlplane/charts/runtime/templates/apiserver/secret-kubeconfig.yaml

* Generate audit webhook secret

Code from `kubeapiserver` component was made reusable

Ref https://github.com/gardener/gardener/blob/master/charts/gardener/controlplane/charts/runtime/templates/apiserver/secret-audit-webhook-config.yaml

* Generate admission kubeconfig secrets

Code from `kubeapiserver` component was made reusable

Ref https://github.com/gardener/gardener/blob/master/charts/gardener/controlplane/charts/runtime/templates/apiserver/secret-admission-kubeconfig.yaml

* Generate audit policy ConfigMap

Code from `kubeapiserver` component was made reusable

Ref https://github.com/gardener/gardener/blob/master/charts/gardener/controlplane/charts/runtime/templates/apiserver/configmap-audit-policy.yaml

* Generate admission configs ConfigMaps

Code from `kubeapiserver` component was made reusable

Ref https://github.com/gardener/gardener/blob/master/charts/gardener/controlplane/charts/runtime/templates/apiserver/configmap-admission-config.yaml

* ------------ empty separator commit -------------

* Generate `PodDisruptionBudget`

Ref https://github.com/gardener/gardener/blob/master/charts/gardener/controlplane/charts/runtime/templates/apiserver/poddisruptionbudget.yaml

* Generate `Service`

Ref https://github.com/gardener/gardener/blob/master/charts/gardener/controlplane/charts/runtime/templates/apiserver/service.yaml

The `.spec.clusterIP` is not needed since we will read the assigned ClusterIP later when needed.

* Generate VPA

Ref https://github.com/gardener/gardener/blob/master/charts/gardener/controlplane/charts/runtime/templates/apiserver/vpa.yaml

* Generate HVPA

Ref https://github.com/gardener/gardener/blob/master/charts/gardener/controlplane/charts/runtime/templates/apiserver/hvpa.yaml

* Generate `Deployment`

Code from `kubeapiserver` component was made reusable

Ref https://github.com/gardener/gardener/blob/master/charts/gardener/controlplane/charts/runtime/templates/apiserver/deployment.yaml

* ------------ empty separator commit -------------

* Generate `APIService`s

Ref
- https://github.com/gardener/gardener/blob/master/charts/gardener/controlplane/charts/application/templates/apiservice-v1beta1-core-gardener-cloud.yaml
- https://github.com/gardener/gardener/blob/master/charts/gardener/controlplane/charts/application/templates/apiservice-v1alpha1-settings-gardener-cloud.yaml
- https://github.com/gardener/gardener/blob/master/charts/gardener/controlplane/charts/application/templates/apiservice-v1alpha1-seedmanagement-gardener-cloud.yaml
- https://github.com/gardener/gardener/blob/master/charts/gardener/controlplane/charts/application/templates/apiservice-v1alpha1-operations-gardener-cloud.yaml

* Generate `Service` and `Endpoints`

Ref https://github.com/gardener/gardener/blob/master/charts/gardener/controlplane/charts/application/templates/service-apiserver.yaml

* Generate RBAC resources

Ref:
- https://github.com/gardener/gardener/blob/master/charts/gardener/controlplane/charts/application/templates/clusterrole-apiserver.yaml
- https://github.com/gardener/gardener/blob/master/charts/gardener/controlplane/charts/application/templates/clusterrolebinding-apiserver.yaml
- https://github.com/gardener/gardener/blob/master/charts/gardener/controlplane/charts/application/templates/clusterrolebinding-apiserver-auth-delegator.yaml
- https://github.com/gardener/gardener/blob/master/charts/gardener/controlplane/charts/application/templates/rolebinding-apiserver-auth-reader.yaml

* Address PR review feedback

* Address PR review feedback

* Address PR review feedback

* Address PR review feedback
  • Loading branch information
rfranzke authored Jul 17, 2023
1 parent 9e1a20a commit ee4da88
Show file tree
Hide file tree
Showing 46 changed files with 5,568 additions and 980 deletions.
16 changes: 8 additions & 8 deletions docs/development/priority-classes.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ When using the `gardener-operator` for managing the garden runtime and virtual c

### `PriorityClass`es for Garden Control Plane Components

| Name | Priority | Associated Components (Examples) |
|---------------------------------- |-----------|-------------------------------------------------------------------------------------------|
| `gardener-garden-system-critical` | 999999550 | `gardener-operator`, `gardener-resource-manager`, `istio` |
| `gardener-garden-system-500` | 999999500 | `virtual-garden-etcd-events`, `virtual-garden-etcd-main`, `virtual-garden-kube-apiserver` |
| `gardener-garden-system-400` | 999999400 | `virtual-garden-gardener-resource-manager` |
| `gardener-garden-system-300` | 999999300 | `virtual-garden-kube-controller-manager`, `vpa-admission-controller`, `etcd-druid`, `nginx-ingress-controller` |
| `gardener-garden-system-200` | 999999200 | `vpa-recommender`, `vpa-updater`, `hvpa-controller` |
| `gardener-garden-system-100` | 999999100 | `kube-state-metrics` |
| Name | Priority | Associated Components (Examples) |
|---------------------------------- |-----------|-----------------------------------------------------------------------------------------------------------------|
| `gardener-garden-system-critical` | 999999550 | `gardener-operator`, `gardener-resource-manager`, `istio` |
| `gardener-garden-system-500` | 999999500 | `virtual-garden-etcd-events`, `virtual-garden-etcd-main`, `virtual-garden-kube-apiserver`, `gardener-apiserver` |
| `gardener-garden-system-400` | 999999400 | `virtual-garden-gardener-resource-manager` |
| `gardener-garden-system-300` | 999999300 | `virtual-garden-kube-controller-manager`, `vpa-admission-controller`, `etcd-druid`, `nginx-ingress-controller` |
| `gardener-garden-system-200` | 999999200 | `vpa-recommender`, `vpa-updater`, `hvpa-controller` |
| `gardener-garden-system-100` | 999999100 | `kube-state-metrics` |

## Seed Clusters

Expand Down
8 changes: 8 additions & 0 deletions pkg/apis/core/v1beta1/constants/types_constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ const (
// SecretNamePrefixETCDEncryptionConfiguration is a constant for the name prefix of a Kubernetes secret object that
// contains the configuration for encryption data in ETCD.
SecretNamePrefixETCDEncryptionConfiguration = "kube-apiserver-etcd-encryption-configuration"
// SecretNameGardenerETCDEncryptionKey is a constant for the name of a Kubernetes secret object that contains the
// key for encryption data in ETCD for gardener-apiserver.
SecretNameGardenerETCDEncryptionKey = "gardener-apiserver-etcd-encryption-key"
// SecretNamePrefixGardenerETCDEncryptionConfiguration is a constant for the name prefix of a Kubernetes secret
// object that contains the configuration for encryption data in ETCD for gardener-apiserver.
SecretNamePrefixGardenerETCDEncryptionConfiguration = "gardener-apiserver-etcd-encryption-configuration"

// SecretNameGardener is a constant for the name of a Kubernetes secret object that contains the client
// certificate and a kubeconfig for a shoot cluster. It is used by Gardener and can be used by extension
Expand Down Expand Up @@ -511,6 +517,8 @@ const (
LabelRole = "role"
// LabelKubernetes is a constant for a label for Kubernetes workload.
LabelKubernetes = "kubernetes"
// LabelGardener is a constant for a label for Gardener workload.
LabelGardener = "gardener"
// LabelAPIServer is a constant for a label for the kube-apiserver.
LabelAPIServer = "apiserver"
// LabelControllerManager is a constant for a label for the kube-controller-manager.
Expand Down
3 changes: 3 additions & 0 deletions pkg/apis/operator/v1alpha1/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,7 @@ const (

// SecretNameCARuntime is a constant for the name of a secret containing the CA for the garden runtime cluster.
SecretNameCARuntime = "ca-garden-runtime"
// SecretNameCAGardener is a constant for the name of a Kubernetes secret object that contains the CA
// certificate of the Gardener control plane.
SecretNameCAGardener = "ca-gardener"
)
269 changes: 269 additions & 0 deletions pkg/component/apiserver/admission.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
// Copyright 2023 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package apiserver

import (
"context"
"fmt"
"strings"

appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/runtime/serializer/json"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
webhookadmissionv1 "k8s.io/apiserver/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1"
webhookadmissionv1alpha1 "k8s.io/apiserver/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1alpha1"
apiserverv1alpha1 "k8s.io/apiserver/pkg/apis/apiserver/v1alpha1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/yaml"

kubernetesutils "github.com/gardener/gardener/pkg/utils/kubernetes"
)

var admissionCodec runtime.Codec

func init() {
admissionScheme := runtime.NewScheme()
utilruntime.Must(apiserverv1alpha1.AddToScheme(admissionScheme))
utilruntime.Must(webhookadmissionv1.AddToScheme(admissionScheme))
utilruntime.Must(webhookadmissionv1alpha1.AddToScheme(admissionScheme))

var (
ser = json.NewSerializerWithOptions(json.DefaultMetaFactory, admissionScheme, admissionScheme, json.SerializerOptions{
Yaml: true,
Pretty: false,
Strict: false,
})
versions = schema.GroupVersions([]schema.GroupVersion{
apiserverv1alpha1.SchemeGroupVersion,
webhookadmissionv1.SchemeGroupVersion,
webhookadmissionv1alpha1.SchemeGroupVersion,
})
)

admissionCodec = serializer.NewCodecFactory(admissionScheme).CodecForVersions(ser, ser, versions, versions)
}

const (
configMapAdmissionDataKey = "admission-configuration.yaml"

volumeNameAdmissionConfiguration = "admission-config"
volumeNameAdmissionKubeconfigSecrets = "admission-kubeconfigs"
volumeMountPathAdmissionConfiguration = "/etc/kubernetes/admission"
volumeMountPathAdmissionKubeconfigSecrets = "/etc/kubernetes/admission-kubeconfigs"
)

// ReconcileSecretAdmissionKubeconfigs reconciles the secret containing the kubeconfig for admission plugins.
func ReconcileSecretAdmissionKubeconfigs(ctx context.Context, c client.Client, secret *corev1.Secret, values Values) error {
secret.Data = make(map[string][]byte)

for _, plugin := range values.EnabledAdmissionPlugins {
if len(plugin.Kubeconfig) != 0 {
secret.Data[admissionPluginsKubeconfigFilename(plugin.Name)] = plugin.Kubeconfig
}
}

utilruntime.Must(kubernetesutils.MakeUnique(secret))
return client.IgnoreAlreadyExists(c.Create(ctx, secret))
}

// ReconcileConfigMapAdmission reconciles the ConfigMap containing the configs for the admission plugins.
func ReconcileConfigMapAdmission(ctx context.Context, c client.Client, configMap *corev1.ConfigMap, values Values) error {
configMap.Data = map[string]string{}

admissionConfig := &apiserverv1alpha1.AdmissionConfiguration{}
for _, plugin := range values.EnabledAdmissionPlugins {
rawConfig, err := computeRelevantAdmissionPluginRawConfig(plugin)
if err != nil {
return err
}

if rawConfig != nil {
admissionConfig.Plugins = append(admissionConfig.Plugins, apiserverv1alpha1.AdmissionPluginConfiguration{
Name: plugin.Name,
Path: volumeMountPathAdmissionConfiguration + "/" + admissionPluginsConfigFilename(plugin.Name),
})

configMap.Data[admissionPluginsConfigFilename(plugin.Name)] = string(rawConfig)
}
}

data, err := runtime.Encode(admissionCodec, admissionConfig)
if err != nil {
return err
}

configMap.Data[configMapAdmissionDataKey] = string(data)
utilruntime.Must(kubernetesutils.MakeUnique(configMap))

return client.IgnoreAlreadyExists(c.Create(ctx, configMap))
}

func computeRelevantAdmissionPluginRawConfig(plugin AdmissionPluginConfig) ([]byte, error) {
var (
nothingToMutate = (plugin.Config == nil || plugin.Config.Raw == nil) && len(plugin.Kubeconfig) == 0
mustDefaultConfig = (plugin.Config == nil || plugin.Config.Raw == nil) && len(plugin.Kubeconfig) > 0
kubeconfigFilePath = volumeMountPathAdmissionKubeconfigSecrets + "/" + admissionPluginsKubeconfigFilename(plugin.Name)
)

if len(plugin.Kubeconfig) == 0 {
// This makes sure that the path to the kubeconfig is overwritten if specified in case no kubeconfig was
// provided. It prevents that users can access arbitrary files in the kube-apiserver pods and disguise them as
// kubeconfigs for their admission plugin configs.
kubeconfigFilePath = ""
}

switch plugin.Name {
case "ValidatingAdmissionWebhook", "MutatingAdmissionWebhook":
if nothingToMutate {
return nil, nil
}

if mustDefaultConfig {
if plugin.Config == nil {
plugin.Config = &runtime.RawExtension{}
}
if len(plugin.Config.Raw) == 0 {
plugin.Config.Raw = []byte(fmt.Sprintf(`apiVersion: %s
kind: WebhookAdmissionConfiguration`, webhookadmissionv1.SchemeGroupVersion.String()))
}
}

configObj, err := runtime.Decode(admissionCodec, plugin.Config.Raw)
if err != nil {
return nil, fmt.Errorf("cannot decode config for admission plugin %s: %w", plugin.Name, err)
}

switch config := configObj.(type) {
case *webhookadmissionv1.WebhookAdmission:
config.KubeConfigFile = kubeconfigFilePath
return runtime.Encode(admissionCodec, config)
case *webhookadmissionv1alpha1.WebhookAdmission:
config.KubeConfigFile = kubeconfigFilePath
return runtime.Encode(admissionCodec, config)
default:
return nil, fmt.Errorf("expected apiserver.config.k8s.io/{v1alpha1.WebhookAdmission,v1.WebhookAdmissionConfiguration} in %s plugin configuration but got %T", plugin.Name, config)
}

case "ImagePolicyWebhook":
// The configuration for this admission plugin is not backed by the API machinery, hence we have to use
// regular marshalling.
if nothingToMutate {
return nil, nil
}

if mustDefaultConfig {
if plugin.Config == nil {
plugin.Config = &runtime.RawExtension{}
}
if len(plugin.Config.Raw) == 0 {
plugin.Config.Raw = []byte("imagePolicy: {}")
}
}

config := map[string]interface{}{}
if err := yaml.Unmarshal(plugin.Config.Raw, &config); err != nil {
return nil, fmt.Errorf("failed to unmarshal plugin configuration for %s: %w", plugin.Name, err)
}
if config["imagePolicy"] == nil {
return nil, fmt.Errorf(`expected "imagePolicy" key in configuration but it does not exist`)
}

config["imagePolicy"].(map[string]interface{})["kubeConfigFile"] = kubeconfigFilePath
return yaml.Marshal(config)

default:
// For all other plugins, we do not need to mutate anything, hence we only return the provided config if set.
if plugin.Config != nil && plugin.Config.Raw != nil {
return plugin.Config.Raw, nil
}
}

return nil, nil
}

func admissionPluginsConfigFilename(name string) string {
return strings.ToLower(name) + ".yaml"
}

func admissionPluginsKubeconfigFilename(name string) string {
return strings.ToLower(name) + "-kubeconfig.yaml"
}

// InjectAdmissionSettings injects the admission settings into `gardener-apiserver` and `kube-apiserver` deployments.
func InjectAdmissionSettings(deployment *appsv1.Deployment, configMapAdmissionConfigs *corev1.ConfigMap, secretAdmissionKubeconfigs *corev1.Secret, values Values) {
if len(values.EnabledAdmissionPlugins) > 0 {
deployment.Spec.Template.Spec.Containers[0].Args = append(deployment.Spec.Template.Spec.Containers[0].Args, "--enable-admission-plugins="+strings.Join(admissionPluginNames(values), ","))
}
if len(values.DisabledAdmissionPlugins) > 0 {
deployment.Spec.Template.Spec.Containers[0].Args = append(deployment.Spec.Template.Spec.Containers[0].Args, "--disable-admission-plugins="+strings.Join(disabledAdmissionPluginNames(values), ","))
}
deployment.Spec.Template.Spec.Containers[0].Args = append(deployment.Spec.Template.Spec.Containers[0].Args, fmt.Sprintf("--admission-control-config-file=%s/%s", volumeMountPathAdmissionConfiguration, configMapAdmissionDataKey))

deployment.Spec.Template.Spec.Containers[0].VolumeMounts = append(deployment.Spec.Template.Spec.Containers[0].VolumeMounts,
corev1.VolumeMount{
Name: volumeNameAdmissionConfiguration,
MountPath: volumeMountPathAdmissionConfiguration,
},
corev1.VolumeMount{
Name: volumeNameAdmissionKubeconfigSecrets,
MountPath: volumeMountPathAdmissionKubeconfigSecrets,
},
)

deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes,
corev1.Volume{
Name: volumeNameAdmissionConfiguration,
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: configMapAdmissionConfigs.Name,
},
},
},
},
corev1.Volume{
Name: volumeNameAdmissionKubeconfigSecrets,
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: secretAdmissionKubeconfigs.Name,
},
},
},
)
}

func admissionPluginNames(values Values) []string {
var out []string

for _, plugin := range values.EnabledAdmissionPlugins {
out = append(out, plugin.Name)
}

return out
}

func disabledAdmissionPluginNames(values Values) []string {
var out []string

for _, plugin := range values.DisabledAdmissionPlugins {
out = append(out, plugin.Name)
}

return out
}
Loading

0 comments on commit ee4da88

Please sign in to comment.