Skip to content

Commit

Permalink
fix(config): Implement K8s client-go in secondary API client for secr…
Browse files Browse the repository at this point in the history
…et/config retrieval
  • Loading branch information
m8rmclaren committed Dec 6, 2023
1 parent b58d286 commit 5f0e521
Show file tree
Hide file tree
Showing 8 changed files with 419 additions and 119 deletions.
60 changes: 30 additions & 30 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ go 1.20

require (
github.com/Keyfactor/keyfactor-go-client-sdk v1.0.2
github.com/go-logr/logr v1.2.4
github.com/stretchr/testify v1.8.2
github.com/go-logr/logr v1.3.0
github.com/stretchr/testify v1.8.4
k8s.io/api v0.28.4
k8s.io/apimachinery v0.28.4
k8s.io/client-go v0.28.4
k8s.io/klog/v2 v2.110.1
k8s.io/utils v0.0.0-20231127182322-b307cd553661
sigs.k8s.io/controller-runtime v0.16.3
)
Expand All @@ -19,54 +20,53 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
github.com/evanphx/json-patch/v5 v5.6.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-logr/zapr v1.2.4 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/evanphx/json-patch/v5 v5.7.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-logr/zapr v1.3.0 // indirect
github.com/go-openapi/jsonpointer v0.20.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/go-openapi/swag v0.22.4 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/imdario/mergo v0.3.6 // indirect
github.com/google/uuid v1.4.0 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.16.0 // indirect
github.com/prometheus/client_model v0.4.0 // indirect
github.com/prometheus/common v0.44.0 // indirect
github.com/prometheus/procfs v0.10.1 // indirect
github.com/prometheus/client_golang v1.17.0 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.45.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.25.0 // indirect
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/oauth2 v0.8.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/term v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/time v0.3.0 // indirect
go.uber.org/zap v1.26.0 // indirect
golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/oauth2 v0.15.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/term v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.5.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/apiextensions-apiserver v0.28.3 // indirect
k8s.io/component-base v0.28.3 // indirect
k8s.io/klog/v2 v2.100.1 // indirect
k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect
k8s.io/apiextensions-apiserver v0.28.4 // indirect
k8s.io/component-base v0.28.4 // indirect
k8s.io/kube-openapi v0.0.0-20231206194836-bf4651e18aa8 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)
160 changes: 75 additions & 85 deletions go.sum

Large diffs are not rendered by default.

12 changes: 8 additions & 4 deletions internal/controllers/certificatesigningrequest_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (

type CertificateSigningRequestReconciler struct {
client.Client
ConfigClient util.ConfigClient
Scheme *runtime.Scheme
SignerBuilder signer.Builder
ClusterResourceNamespace string
Expand Down Expand Up @@ -97,23 +98,26 @@ func (c *CertificateSigningRequestReconciler) Reconcile(ctx context.Context, req

reconcileLog.Info(fmt.Sprintf("Preparing to sign CSR called %q", certificateSigningRequest.GetName()))

// Set the context on the config client
c.ConfigClient.SetContext(ctx)

// Get the credentials secret
var creds corev1.Secret
if err = c.Get(ctx, c.CredsSecret, &creds); err != nil {
if err = c.ConfigClient.GetSecret(c.CredsSecret, &creds); err != nil {
return ctrl.Result{}, fmt.Errorf("failed to get Secret containing Signer credentials, secret name: %s, reason: %v", c.CredsSecret.Name, err)
}

// Get the signer configuration
var config corev1.ConfigMap
if err = c.Get(ctx, c.ConfigMap, &config); err != nil {
if err = c.ConfigClient.GetConfigMap(c.ConfigMap, &config); err != nil {
return ctrl.Result{}, fmt.Errorf("failed to get ConfigMap containing Signer configuration, configmap name: %s, reason: %v", c.ConfigMap.Name, err)
}

// Get the CA certificate
var root corev1.ConfigMap
// If the CA secret name is not specified, we will not attempt to retrieve it
if c.CaCertConfigmap.Name != "" {
// If the CA secret name is not specified, we will not attempt to retrieve it
err = c.Get(ctx, c.CaCertConfigmap, &root)
err = c.ConfigClient.GetConfigMap(c.CaCertConfigmap, &root)
if err != nil {
return ctrl.Result{}, fmt.Errorf("caSecretName was provided, but failed to get ConfigMap containing CA certificate, configmap name: %q, reason: %v", c.CaCertConfigmap, err)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ func TestCertificateSigningRequestReconciler_Reconcile(t *testing.T) {
Build()
controller := CertificateSigningRequestReconciler{
Client: fakeClient,
ConfigClient: NewFakeConfigClient(fakeClient),
Scheme: scheme,
ClusterResourceNamespace: tc.clusterResourceNamespace,
SignerBuilder: tc.signerBuilder,
Expand Down
57 changes: 57 additions & 0 deletions internal/controllers/fake_configclient_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
Copyright 2023 The Keyfactor Command Authors.
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 controllers

import (
"context"
"github.com/Keyfactor/k8s-csr-signer/pkg/util"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
)

// FakeConfigClient is a fake implementation of the util.ConfigClient interface
// It forwards requests destined for the Kubernetes API server implemented by
// the util.ConfigClient interface to a fake Kubernetes API server implemented
// by the client.Client interface.

// Force the compiler to check that FakeConfigClient implements the util.ConfigClient interface
var _ util.ConfigClient = &FakeConfigClient{}

type FakeConfigClient struct {
client client.Client
ctx context.Context
}

// NewFakeConfigClient uses the
func NewFakeConfigClient(fakeControllerRuntimeClient client.Client) util.ConfigClient {
return &FakeConfigClient{
client: fakeControllerRuntimeClient,
}
}

func (f FakeConfigClient) SetContext(ctx context.Context) {
f.ctx = ctx
}

func (f FakeConfigClient) GetConfigMap(name types.NamespacedName, out *corev1.ConfigMap) error {
return f.client.Get(f.ctx, name, out)
}

func (f FakeConfigClient) GetSecret(name types.NamespacedName, out *corev1.Secret) error {
return f.client.Get(f.ctx, name, out)
}
8 changes: 8 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package main

import (
"context"
"errors"
"flag"
"github.com/Keyfactor/k8s-csr-signer/internal/controllers"
Expand Down Expand Up @@ -97,6 +98,12 @@ func main() {
os.Exit(1)
}

ctx := context.Background()
configClient, err := util.NewConfigClient(ctx)
if err != nil {
setupLog.Error(err, "error creating config client")
}

mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
HealthProbeBindAddress: probeAddr,
Expand Down Expand Up @@ -130,6 +137,7 @@ func main() {

if err = (&controllers.CertificateSigningRequestReconciler{
Client: mgr.GetClient(),
ConfigClient: configClient,
Scheme: mgr.GetScheme(),
SignerBuilder: commandSignerBuilder,
ClusterResourceNamespace: clusterResourceNamespace,
Expand Down
152 changes: 152 additions & 0 deletions pkg/util/configclient.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/*
Copyright 2023 The Keyfactor Command Authors.
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 util

import (
"context"
"fmt"
authv1 "k8s.io/api/authorization/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes"
"k8s.io/klog/v2"
ctrl "sigs.k8s.io/controller-runtime"
)

type ConfigClient interface {
SetContext(ctx context.Context)
GetConfigMap(name types.NamespacedName, out *corev1.ConfigMap) error
GetSecret(name types.NamespacedName, out *corev1.Secret) error
}

type configClient struct {
ctx context.Context
logger klog.Logger
client kubernetes.Interface
accessCache map[string]bool

verifyAccessFunc func(apiResource string, resource types.NamespacedName) error
}

func NewConfigClient(ctx context.Context) (ConfigClient, error) {
config := ctrl.GetConfigOrDie()

// Create the clientset
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
return nil, fmt.Errorf("failed to create clientset: %w", err)
}

client := &configClient{
client: clientset,
accessCache: make(map[string]bool),
ctx: ctx,
logger: klog.NewKlogr(),
}

client.verifyAccessFunc = client.verifyAccessToResource

return client, nil
}

func (c *configClient) SetContext(ctx context.Context) {
c.ctx = ctx
c.logger = klog.FromContext(ctx)
}

func (c *configClient) verifyAccessToResource(apiResource string, resource types.NamespacedName) error {
verbs := []string{"get", "list", "watch"}

for _, verb := range verbs {
ssar := &authv1.SelfSubjectAccessReview{
Spec: authv1.SelfSubjectAccessReviewSpec{
ResourceAttributes: &authv1.ResourceAttributes{
Name: resource.Name,
Namespace: resource.Namespace,

Group: "",
Resource: apiResource,
Verb: verb,
},
},
}

ssar, err := c.client.AuthorizationV1().SelfSubjectAccessReviews().Create(c.ctx, ssar, metav1.CreateOptions{})
if err != nil {
return fmt.Errorf("failed to create SelfSubjectAccessReview to check access to %s for verb %q: %w", apiResource, verb, err)
}

if !ssar.Status.Allowed {
return fmt.Errorf("client does not have access to %s called %q for verb %q, reason: %v", apiResource, resource.String(), verb, ssar.Status.String())
}
}

c.logger.Info(fmt.Sprintf("Client has access to %s called %q", apiResource, resource.String()))

return nil
}

func (c *configClient) GetConfigMap(name types.NamespacedName, out *corev1.ConfigMap) error {
if c == nil {
return fmt.Errorf("config client is nil")
}

// Check if the client has access to the configmap resource
if ok, _ := c.accessCache[name.String()]; !ok {
err := c.verifyAccessFunc("configmaps", name)
if err != nil {
return err
}
c.accessCache[name.String()] = true
}

// Get the configmap
configmap, err := c.client.CoreV1().ConfigMaps(name.Namespace).Get(c.ctx, name.Name, metav1.GetOptions{})
if err != nil {
return err
}

// Copy the configmap into the out parameter
configmap.DeepCopyInto(out)
return nil
}

func (c *configClient) GetSecret(name types.NamespacedName, out *corev1.Secret) error {
if c == nil {
return fmt.Errorf("config client is nil")
}

// Check if the client has access to the secret resource
if ok, _ := c.accessCache[name.String()]; !ok {
err := c.verifyAccessFunc("secrets", name)
if err != nil {
return err
}
c.accessCache[name.String()] = true
}

// Get the secret
secret, err := c.client.CoreV1().Secrets(name.Namespace).Get(c.ctx, name.Name, metav1.GetOptions{})
if err != nil {
return err
}

// Copy the secret into the out parameter
secret.DeepCopyInto(out)
return nil
}
Loading

0 comments on commit 5f0e521

Please sign in to comment.