Skip to content

feat: Add API server cert SANs patch #129

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 1, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions charts/capi-runtime-extensions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ A Helm chart for capi-runtime-extensions
| controllers.enableLeaderElection | bool | `false` | |
| deployment.replicas | int | `1` | |
| env | object | `{}` | |
| handlers.APIServerCertSANsPatch.enabled | bool | `true` | |
| handlers.APIServerCertSANsVars.enabled | bool | `true` | |
| handlers.AuditPolicyPatch.enabled | bool | `true` | |
| handlers.CalicoCNI.defaultInstallationConfigMaps.DockerCluster.configMap.content | string | `""` | |
| handlers.CalicoCNI.defaultInstallationConfigMaps.DockerCluster.configMap.name | string | `"calico-cni-installation-dockercluster"` | |
Expand Down
45 changes: 45 additions & 0 deletions charts/capi-runtime-extensions/values.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,51 @@
"default": true
}
}
},
"HTTPProxyVars": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean",
"default": true
}
}
},
"HTTPProxyPatch": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean",
"default": true
}
}
},
"AuditPolicyPatch": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean",
"default": true
}
}
},
"APIServerCertSANsVars": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean",
"default": true
}
}
},
"APIServerCertSANsPatch": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean",
"default": true
}
}
}
}
},
Expand Down
4 changes: 4 additions & 0 deletions charts/capi-runtime-extensions/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ handlers:
enabled: true
AuditPolicyPatch:
enabled: true
APIServerCertSANsVars:
enabled: true
APIServerCertSANsPatch:
enabled: true

deployment:
replicas: 1
Expand Down
3 changes: 3 additions & 0 deletions cmd/capi-runtime-extensions/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
ctrclient "sigs.k8s.io/controller-runtime/pkg/client"

"github.com/d2iq-labs/capi-runtime-extensions/internal/controllermanager"
"github.com/d2iq-labs/capi-runtime-extensions/pkg/handlers/apiservercertsans"
"github.com/d2iq-labs/capi-runtime-extensions/pkg/handlers/cni/calico"
"github.com/d2iq-labs/capi-runtime-extensions/pkg/handlers/httpproxy"
"github.com/d2iq-labs/capi-runtime-extensions/pkg/handlers/servicelbgc"
Expand Down Expand Up @@ -79,6 +80,8 @@ func main() {
calico.New(client, calicoCNIConfig),
httpproxy.NewVariable(),
httpproxy.NewPatch(),
apiservercertsans.NewVariable(),
apiservercertsans.NewPatch(),
)

// Initialize and parse command line flags.
Expand Down
47 changes: 47 additions & 0 deletions docs/content/apiserver-cert-sans.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
title: "API Server Certificate SANs"
---

If the API server can be accessed by alternative DNS addresses then setting additional SANs on the API server
certificate is necessary in order for clients to successfully validate the API server certificate.

To enable the API server certificate SANs enable the `apiservercertsansvars` and `apiservercertsanspatch` external
patches on `ClusterClass`.

```yaml
apiVersion: cluster.x-k8s.io/v1beta1
kind: ClusterClass
metadata:
name: <NAME>
spec:
patches:
- name: apiserver-cert-sans
external:
generateExtension: "apiservercertsanspatch.<external-config-name>"
discoverVariablesExtension: "apiservercertsansvars.<external-config-name>"
```

On the cluster resource then specify desired certificate SANs values:

```yaml
apiVersion: cluster.x-k8s.io/v1beta1
kind: Cluster
metadata:
name: <NAME>
spec:
topology:
variables:
- name: apiServerCertSANs
value:
- a.b.c.example.com
- d.e.f.example.com
```

Applying this configuration will result in the certificate SANs being correctly set in the
`KubeadmControlPlaneTemplate`.

This hook is enabled by default, and can be explicitly disabled by omitting the `APIServerCertSANsVars`
and `APIServerCertSANsPatch` hook from the `--runtimehooks.enabled-handlers` flag.

If deploying via Helm, then this can be disabled by setting `handlers.APIServerCertSANsVars.enabled=false` and
`handlers.APIServerCertSANsPatch.enabled=false`.
14 changes: 7 additions & 7 deletions docs/content/http-proxy.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@ metadata:
spec:
topology:
variables:
name: proxy
values:
http: http://example.com
https: http://example.com
no:
- http://no-proxy-1.example.com
- http://no-proxy-2.example.com
- name: proxy
value:
http: http://example.com
https: http://example.com
no:
- http://no-proxy-1.example.com
- http://no-proxy-2.example.com
```

Applying this configuration will result in new bootstrap files on the `KubeadmControlPlaneTemplate`
Expand Down
106 changes: 106 additions & 0 deletions pkg/handlers/apiservercertsans/inject.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright 2023 D2iQ, Inc. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package apiservercertsans

import (
"context"
_ "embed"

apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/types"
bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1"
controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1beta1"
runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1"
"sigs.k8s.io/cluster-api/exp/runtime/topologymutation"
ctrl "sigs.k8s.io/controller-runtime"

"github.com/d2iq-labs/capi-runtime-extensions/pkg/capi/clustertopology/patches"
"github.com/d2iq-labs/capi-runtime-extensions/pkg/capi/clustertopology/patches/selectors"
"github.com/d2iq-labs/capi-runtime-extensions/pkg/capi/clustertopology/variables"
"github.com/d2iq-labs/capi-runtime-extensions/server/pkg/handlers"
)

const (
// HandlerNamePatch is the name of the inject handler.
HandlerNamePatch = "APIServerCertSANsPatch"
)

type apiServerCertSANsPatchHandler struct {
decoder runtime.Decoder
}

var (
_ handlers.NamedHandler = &apiServerCertSANsPatchHandler{}
_ handlers.GeneratePatchesMutationHandler = &apiServerCertSANsPatchHandler{}
)

func NewPatch() *apiServerCertSANsPatchHandler {
scheme := runtime.NewScheme()
_ = bootstrapv1.AddToScheme(scheme)
_ = controlplanev1.AddToScheme(scheme)
return &apiServerCertSANsPatchHandler{
decoder: serializer.NewCodecFactory(scheme).UniversalDecoder(
controlplanev1.GroupVersion,
bootstrapv1.GroupVersion,
),
}
}

func (h *apiServerCertSANsPatchHandler) Name() string {
return HandlerNamePatch
}

func (h *apiServerCertSANsPatchHandler) GeneratePatches(
ctx context.Context,
req *runtimehooksv1.GeneratePatchesRequest,
resp *runtimehooksv1.GeneratePatchesResponse,
) {
topologymutation.WalkTemplates(
ctx,
h.decoder,
req,
resp,
func(
ctx context.Context,
obj runtime.Object,
vars map[string]apiextensionsv1.JSON,
holderRef runtimehooksv1.HolderReference,
) error {
log := ctrl.LoggerFrom(ctx).WithValues(
"holderRef", holderRef,
)

apiServerCertSANsVar, found, err := variables.Get[APIServerCertSANsVariables](
vars,
VariableName,
)
if err != nil {
return err
}
if !found {
log.Info("API server cert SANs variable not defined")
return nil
}

return patches.Generate(
obj, vars, &holderRef, selectors.ControlPlane(), log,
func(obj *controlplanev1.KubeadmControlPlaneTemplate) error {
log.WithValues("namespacedName", types.NamespacedName{
Name: obj.Name,
Namespace: obj.Namespace,
}).Info("adding API server extra cert SANs in kubeadm config spec")

if obj.Spec.Template.Spec.KubeadmConfigSpec.ClusterConfiguration == nil {
obj.Spec.Template.Spec.KubeadmConfigSpec.ClusterConfiguration = &bootstrapv1.ClusterConfiguration{}
}
obj.Spec.Template.Spec.KubeadmConfigSpec.ClusterConfiguration.APIServer.CertSANs = apiServerCertSANsVar

return nil
},
)
},
)
}
121 changes: 121 additions & 0 deletions pkg/handlers/apiservercertsans/inject_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Copyright 2023 D2iQ, Inc. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package apiservercertsans

import (
"bytes"
"context"
"encoding/json"
"testing"

. "github.com/onsi/gomega"
. "github.com/onsi/gomega/gstruct"
"gomodules.xyz/jsonpatch/v2"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1beta1"
runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1"
)

func TestGeneratePatches(t *testing.T) {
g := NewWithT(t)
h := NewPatch()
req := &runtimehooksv1.GeneratePatchesRequest{}
resp := &runtimehooksv1.GeneratePatchesResponse{}
h.GeneratePatches(context.Background(), req, resp)
g.Expect(resp.Status).To(Equal(runtimehooksv1.ResponseStatusSuccess))
g.Expect(resp.Items).To(BeEmpty())
}

func TestGeneratePatches_KubeadmControlPlaneTemplate(t *testing.T) {
g := NewWithT(t)
h := NewPatch()
req := &runtimehooksv1.GeneratePatchesRequest{
Variables: []runtimehooksv1.Variable{
newVariable(
VariableName,
APIServerCertSANsVariables{"a.b.c.example.com", "d.e.f.example.com"},
),
},
Items: []runtimehooksv1.GeneratePatchesRequestItem{
requestItem(
"1",
&controlplanev1.KubeadmControlPlaneTemplate{
TypeMeta: v1.TypeMeta{
Kind: "KubeadmControlPlaneTemplate",
APIVersion: controlplanev1.GroupVersion.String(),
},
},
&runtimehooksv1.HolderReference{
Kind: "Cluster",
FieldPath: "spec.controlPlaneRef",
},
),
},
}
resp := &runtimehooksv1.GeneratePatchesResponse{}
h.GeneratePatches(context.Background(), req, resp)
g.Expect(resp.Status).To(Equal(runtimehooksv1.ResponseStatusSuccess))
g.Expect(resp.Items).To(ContainElement(MatchFields(IgnoreExtras, Fields{
"UID": Equal(types.UID("1")),
"PatchType": Equal(runtimehooksv1.JSONPatchType),
"Patch": WithTransform(
func(data []byte) ([]jsonpatch.Operation, error) {
operations := []jsonpatch.Operation{}
if err := json.Unmarshal(data, &operations); err != nil {
return nil, err
}
return operations, nil
},
ConsistOf(MatchAllFields(Fields{
"Operation": Equal("add"),
"Path": Equal("/spec/template/spec/kubeadmConfigSpec/clusterConfiguration"),
"Value": HaveKeyWithValue(
"apiServer",
HaveKeyWithValue(
"certSANs",
[]interface{}{"a.b.c.example.com", "d.e.f.example.com"},
),
),
})),
),
})))
}

func toJSON(v any) []byte {
data, err := json.Marshal(v)
if err != nil {
panic(err)
}
compacted := &bytes.Buffer{}
if err := json.Compact(compacted, data); err != nil {
panic(err)
}
return compacted.Bytes()
}

// requestItem returns a GeneratePatchesRequestItem with the given uid, variables and object.
func requestItem(
uid string,
object any,
holderRef *runtimehooksv1.HolderReference,
) runtimehooksv1.GeneratePatchesRequestItem {
return runtimehooksv1.GeneratePatchesRequestItem{
UID: types.UID(uid),
Object: runtime.RawExtension{
Raw: toJSON(object),
},
HolderReference: *holderRef,
}
}

// newVariable returns a runtimehooksv1.Variable with the passed name and value.
func newVariable(name string, value any) runtimehooksv1.Variable {
return runtimehooksv1.Variable{
Name: name,
Value: apiextensionsv1.JSON{Raw: toJSON(value)},
}
}
Loading