images/kube-webhook-certgen/rootfs: add support for patching APIService objects (#7641)
* images/kube-webhook-certgen/rootfs/pkg/k8s: return err from functions Initially only from some to preserve existing behavior. Signed-off-by: Mateusz Gozdek <mgozdek@microsoft.com> * images/kube-webhook-certgen/rootfs: make patching return error So we don't call log.Fatal in so many places, which makes code testable. Signed-off-by: Mateusz Gozdek <mgozdek@microsoft.com> * images/kube-webhook-certgen/rootfs/pkg/k8s: require context So initialize top-level contexts in tests and CLI, then pass them around all the way down, so there is an ability e.g. to add timeouts to patch operations, if needed and to follow general conventions. Signed-off-by: Mateusz Gozdek <mgozdek@microsoft.com> * images/kube-webhook-certgen/rootfs/pkg/k8s: support patching APIService APIService object is very similar to MutatingWebhookConfiguration and ValidatingWebhookConfiguration objects, so support for patching it shouldn't be too much of a burden. Signed-off-by: Mateusz Gozdek <mgozdek@microsoft.com> * images/kube-webhook-certgen/rootfs/cmd: use new patch API So old function PatchWebhookConfigurations can be unexported and CLI can be extended to also support patching APIService. Signed-off-by: Mateusz Gozdek <mgozdek@microsoft.com> * images/kube-webhook-certgen/rootfs/pkg/k8s: unexport old patch function PatchObjects should be now used instead. Signed-off-by: Mateusz Gozdek <mgozdek@microsoft.com> * images/kube-webhook-certgen/rootfs: add .gitignore To ignore manually built binaries during development process. Signed-off-by: Mateusz Gozdek <mgozdek@microsoft.com> * images/kube-webhook-certgen/rootfs/cmd: test patching By adding a PatchConfig and Patch function, it is now possible to test logic of flag validation, which was previously tied to CLI options. This commit adds nice set of tests covering existing logic. Signed-off-by: Mateusz Gozdek <mgozdek@microsoft.com> * images/kube-webhook-certgen/rootfs/cmd: improve formatting Those strings will be changed anyway in future commits, so at first we can properly capitalize used names. Signed-off-by: Mateusz Gozdek <mgozdek@microsoft.com> * images/kube-webhook-certgen/rootfs/cmd: support patching APIService As logic for creating a CA certificate and patching an object is almost the same for both webhook configuration and API services, this commit adds support to kube-webhook-certgen CLI to also patch APIService objects, so they can be served over TLS as well. Signed-off-by: Mateusz Gozdek <mgozdek@microsoft.com> * images/kube-webhook-certgen/rootfs: pass failure policy by value k8s.k8s.patchWebhookConfigurations() always dereferences it and we do not do a nil check, so the code may panic in some conditions, so it's safer to just pass it by value, as it's just a wrapped string. Signed-off-by: Mateusz Gozdek <mgozdek@microsoft.com>
This commit is contained in:
parent
5662db4509
commit
9acf62d867
9 changed files with 723 additions and 60 deletions
|
|
@ -1,6 +1,8 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jet/kube-webhook-certgen/pkg/certs"
|
||||
"github.com/jet/kube-webhook-certgen/pkg/k8s"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
|
@ -16,13 +18,17 @@ var create = &cobra.Command{
|
|||
}
|
||||
|
||||
func createCommand(cmd *cobra.Command, args []string) {
|
||||
k := k8s.New(newKubernetesClient(cfg.kubeconfig))
|
||||
ca := k.GetCaFromSecret(cfg.secretName, cfg.namespace)
|
||||
clientset, aggregatorClientset := newKubernetesClients(cfg.kubeconfig)
|
||||
k := k8s.New(clientset, aggregatorClientset)
|
||||
|
||||
ctx := context.TODO()
|
||||
|
||||
ca := k.GetCaFromSecret(ctx, cfg.secretName, cfg.namespace)
|
||||
if ca == nil {
|
||||
log.Info("creating new secret")
|
||||
newCa, newCert, newKey := certs.GenerateCerts(cfg.host)
|
||||
ca = newCa
|
||||
k.SaveCertsToSecret(cfg.secretName, cfg.namespace, cfg.certName, cfg.keyName, ca, newCert, newKey)
|
||||
k.SaveCertsToSecret(ctx, cfg.secretName, cfg.namespace, cfg.certName, cfg.keyName, ca, newCert, newKey)
|
||||
} else {
|
||||
log.Info("secret already exists")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/jet/kube-webhook-certgen/pkg/k8s"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
|
@ -9,49 +13,109 @@ import (
|
|||
|
||||
var patch = &cobra.Command{
|
||||
Use: "patch",
|
||||
Short: "Patch a validatingwebhookconfiguration and mutatingwebhookconfiguration 'webhook-name' by using the ca from 'secret-name' in 'namespace'",
|
||||
Long: "Patch a validatingwebhookconfiguration and mutatingwebhookconfiguration 'webhook-name' by using the ca from 'secret-name' in 'namespace'",
|
||||
PreRun: prePatchCommand,
|
||||
Short: "Patch a ValidatingWebhookConfiguration, MutatingWebhookConfiguration or APIService 'object-name' by using the ca from 'secret-name' in 'namespace'",
|
||||
Long: "Patch a ValidatingWebhookConfiguration, MutatingWebhookConfiguration or APIService 'object-name' by using the ca from 'secret-name' in 'namespace'",
|
||||
PreRun: configureLogging,
|
||||
Run: patchCommand,
|
||||
}
|
||||
|
||||
func prePatchCommand(cmd *cobra.Command, args []string) {
|
||||
configureLogging(cmd, args)
|
||||
if !cfg.patchMutating && !cfg.patchValidating {
|
||||
log.Fatal("patch-validating=false, patch-mutating=false. You must patch at least one kind of webhook, otherwise this command is a no-op")
|
||||
type PatchConfig struct {
|
||||
PatchMutating bool
|
||||
PatchValidating bool
|
||||
PatchFailurePolicy string
|
||||
APIServiceName string
|
||||
WebhookName string
|
||||
|
||||
SecretName string
|
||||
Namespace string
|
||||
|
||||
Patcher Patcher
|
||||
}
|
||||
|
||||
type Patcher interface {
|
||||
PatchObjects(ctx context.Context, options k8s.PatchOptions) error
|
||||
GetCaFromSecret(ctx context.Context, secretName, namespace string) []byte
|
||||
}
|
||||
|
||||
func Patch(ctx context.Context, cfg *PatchConfig) error {
|
||||
if cfg.Patcher == nil {
|
||||
return fmt.Errorf("no patcher defined")
|
||||
}
|
||||
switch cfg.patchFailurePolicy {
|
||||
|
||||
if !cfg.PatchMutating && !cfg.PatchValidating && cfg.APIServiceName == "" {
|
||||
return fmt.Errorf("patch-validating=false, patch-mutating=false. You must patch at least one kind of webhook, otherwise this command is a no-op")
|
||||
}
|
||||
|
||||
var failurePolicy admissionv1.FailurePolicyType
|
||||
|
||||
switch cfg.PatchFailurePolicy {
|
||||
case "":
|
||||
break
|
||||
case "Ignore":
|
||||
case "Fail":
|
||||
failurePolicy = admissionv1.FailurePolicyType(cfg.patchFailurePolicy)
|
||||
failurePolicy = admissionv1.FailurePolicyType(cfg.PatchFailurePolicy)
|
||||
break
|
||||
default:
|
||||
log.Fatalf("patch-failure-policy %s is not valid", cfg.patchFailurePolicy)
|
||||
return fmt.Errorf("patch-failure-policy %s is not valid", cfg.PatchFailurePolicy)
|
||||
}
|
||||
|
||||
ca := cfg.Patcher.GetCaFromSecret(ctx, cfg.SecretName, cfg.Namespace)
|
||||
|
||||
if ca == nil {
|
||||
return fmt.Errorf("no secret with '%s' in '%s'", cfg.SecretName, cfg.Namespace)
|
||||
}
|
||||
|
||||
options := k8s.PatchOptions{
|
||||
CABundle: ca,
|
||||
FailurePolicyType: failurePolicy,
|
||||
APIServiceName: cfg.APIServiceName,
|
||||
}
|
||||
|
||||
if cfg.PatchMutating {
|
||||
options.MutatingWebhookConfigurationName = cfg.WebhookName
|
||||
}
|
||||
|
||||
if cfg.PatchValidating {
|
||||
options.ValidatingWebhookConfigurationName = cfg.WebhookName
|
||||
}
|
||||
|
||||
return cfg.Patcher.PatchObjects(ctx, options)
|
||||
}
|
||||
|
||||
func patchCommand(_ *cobra.Command, _ []string) {
|
||||
k := k8s.New(newKubernetesClient(cfg.kubeconfig))
|
||||
ca := k.GetCaFromSecret(cfg.secretName, cfg.namespace)
|
||||
client, aggregationClient := newKubernetesClients(cfg.kubeconfig)
|
||||
|
||||
if ca == nil {
|
||||
log.Fatalf("no secret with '%s' in '%s'", cfg.secretName, cfg.namespace)
|
||||
config := &PatchConfig{
|
||||
SecretName: cfg.secretName,
|
||||
Namespace: cfg.namespace,
|
||||
PatchMutating: cfg.patchMutating,
|
||||
PatchValidating: cfg.patchValidating,
|
||||
PatchFailurePolicy: cfg.patchFailurePolicy,
|
||||
APIServiceName: cfg.apiServiceName,
|
||||
WebhookName: cfg.webhookName,
|
||||
Patcher: k8s.New(client, aggregationClient),
|
||||
}
|
||||
|
||||
k.PatchWebhookConfigurations(cfg.webhookName, ca, &failurePolicy, cfg.patchMutating, cfg.patchValidating)
|
||||
ctx := context.TODO()
|
||||
|
||||
if err := Patch(ctx, config); err != nil {
|
||||
if wrappedErr := errors.Unwrap(err); wrappedErr != nil {
|
||||
log.WithField("err", wrappedErr).Fatal(err.Error())
|
||||
}
|
||||
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(patch)
|
||||
patch.Flags().StringVar(&cfg.secretName, "secret-name", "", "Name of the secret where certificate information will be read from")
|
||||
patch.Flags().StringVar(&cfg.namespace, "namespace", "", "Namespace of the secret where certificate information will be read from")
|
||||
patch.Flags().StringVar(&cfg.webhookName, "webhook-name", "", "Name of validatingwebhookconfiguration and mutatingwebhookconfiguration that will be updated")
|
||||
patch.Flags().BoolVar(&cfg.patchValidating, "patch-validating", true, "If true, patch validatingwebhookconfiguration")
|
||||
patch.Flags().BoolVar(&cfg.patchMutating, "patch-mutating", true, "If true, patch mutatingwebhookconfiguration")
|
||||
patch.Flags().StringVar(&cfg.webhookName, "webhook-name", "", "Name of ValidatingWebhookConfiguration and MutatingWebhookConfiguration that will be updated")
|
||||
patch.Flags().StringVar(&cfg.apiServiceName, "apiservice-name", "", "Name of APIService that will be patched")
|
||||
patch.Flags().BoolVar(&cfg.patchValidating, "patch-validating", true, "If true, patch ValidatingWebhookConfiguration")
|
||||
patch.Flags().BoolVar(&cfg.patchMutating, "patch-mutating", true, "If true, patch MutatingWebhookConfiguration")
|
||||
patch.Flags().StringVar(&cfg.patchFailurePolicy, "patch-failure-policy", "", "If set, patch the webhooks with this failure policy. Valid options are Ignore or Fail")
|
||||
patch.MarkFlagRequired("secret-name")
|
||||
patch.MarkFlagRequired("namespace")
|
||||
patch.MarkFlagRequired("webhook-name")
|
||||
}
|
||||
|
|
|
|||
254
images/kube-webhook-certgen/rootfs/cmd/patch_test.go
Normal file
254
images/kube-webhook-certgen/rootfs/cmd/patch_test.go
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
package cmd_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/jet/kube-webhook-certgen/cmd"
|
||||
"github.com/jet/kube-webhook-certgen/pkg/k8s"
|
||||
)
|
||||
|
||||
func Test_Patch(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.TODO()
|
||||
|
||||
t.Run("patches_APIService_object_when_requested", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
config := testPatchConfig()
|
||||
config.APIServiceName = "bar"
|
||||
|
||||
patcher := testPatcher()
|
||||
patcher.patchObjects = func(_ context.Context, options k8s.PatchOptions) error {
|
||||
if options.APIServiceName != config.APIServiceName {
|
||||
return fmt.Errorf("unexpected APIService name %q, expected %q", options.APIServiceName, config.APIServiceName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
config.Patcher = patcher
|
||||
|
||||
if err := cmd.Patch(ctx, config); err != nil {
|
||||
t.Fatalf("Unexpected patching error: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("use_configured_webhook_name_for_patching", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
config := testPatchConfig()
|
||||
config.WebhookName = "foo"
|
||||
|
||||
patcher := testPatcher()
|
||||
patcher.patchObjects = func(_ context.Context, options k8s.PatchOptions) error {
|
||||
if options.ValidatingWebhookConfigurationName != config.WebhookName {
|
||||
return fmt.Errorf("unexpected object name %q, expected %q", options.ValidatingWebhookConfigurationName, config.WebhookName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
config.Patcher = patcher
|
||||
|
||||
if err := cmd.Patch(ctx, config); err != nil {
|
||||
t.Fatalf("Unexpected patching error: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("patches_only_validating_webhook_when_requested", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
config := testPatchConfig()
|
||||
config.PatchValidating = true
|
||||
config.PatchMutating = false
|
||||
|
||||
patcher := testPatcher()
|
||||
patcher.patchObjects = func(_ context.Context, options k8s.PatchOptions) error {
|
||||
if options.ValidatingWebhookConfigurationName == "" {
|
||||
t.Error("expected validating webhook to be patched")
|
||||
}
|
||||
|
||||
if options.MutatingWebhookConfigurationName != "" {
|
||||
t.Error("expected mutating webhook to not be patched")
|
||||
}
|
||||
|
||||
if options.APIServiceName != "" {
|
||||
t.Error("expected APIService to not be patched")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
config.Patcher = patcher
|
||||
|
||||
if err := cmd.Patch(ctx, config); err != nil {
|
||||
t.Fatalf("Unexpected patching error: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("patches_both_webhooks_when_requested", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
config := testPatchConfig()
|
||||
config.PatchValidating = true
|
||||
config.PatchMutating = true
|
||||
|
||||
patcher := testPatcher()
|
||||
patcher.patchObjects = func(_ context.Context, options k8s.PatchOptions) error {
|
||||
if options.ValidatingWebhookConfigurationName == "" {
|
||||
t.Error("expected validating webhook to be patched")
|
||||
}
|
||||
|
||||
if options.MutatingWebhookConfigurationName == "" {
|
||||
t.Error("expected mutating webhook to be patched")
|
||||
}
|
||||
|
||||
if options.APIServiceName != "" {
|
||||
t.Error("expected APIService to not be patched")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
config.Patcher = patcher
|
||||
|
||||
if err := cmd.Patch(ctx, config); err != nil {
|
||||
t.Fatalf("Unexpected patching error: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("use_empty_policy_when_ignore_is_requested", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
config := testPatchConfig()
|
||||
config.PatchFailurePolicy = "Ignore"
|
||||
|
||||
patcher := testPatcher()
|
||||
patcher.patchObjects = func(_ context.Context, options k8s.PatchOptions) error {
|
||||
if options.FailurePolicyType != "" {
|
||||
return fmt.Errorf("expected policy to be nil. got: %q", options.FailurePolicyType)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
config.Patcher = patcher
|
||||
|
||||
if err := cmd.Patch(ctx, config); err != nil {
|
||||
t.Fatalf("Unexpected patching error: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("use_fail_policy_when_fail_is_requested", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
config := testPatchConfig()
|
||||
config.PatchFailurePolicy = "Fail"
|
||||
|
||||
patcher := testPatcher()
|
||||
patcher.patchObjects = func(_ context.Context, options k8s.PatchOptions) error {
|
||||
if options.FailurePolicyType == "" || options.FailurePolicyType != "Fail" {
|
||||
return fmt.Errorf("unexpected policy: %q", options.FailurePolicyType)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
config.Patcher = patcher
|
||||
|
||||
if err := cmd.Patch(ctx, config); err != nil {
|
||||
t.Fatalf("Unexpected patching error: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("use_obtained_ca_certificate_for_patching", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
expectedCA := []byte("foo")
|
||||
|
||||
config := testPatchConfig()
|
||||
|
||||
patcher := testPatcher()
|
||||
patcher.patchObjects = func(_ context.Context, options k8s.PatchOptions) error {
|
||||
if !reflect.DeepEqual(options.CABundle, expectedCA) {
|
||||
return fmt.Errorf("unexpected CA, expected %q, got %q", string(expectedCA), string(options.CABundle))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
patcher.getCaFromSecret = func(context.Context, string, string) []byte {
|
||||
return expectedCA
|
||||
}
|
||||
config.Patcher = patcher
|
||||
|
||||
if err := cmd.Patch(ctx, config); err != nil {
|
||||
t.Fatalf("Unexpected patching error: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns_error_when", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for name, mutateF := range map[string]func(*cmd.PatchConfig){
|
||||
"no_patcher_is_defined": func(c *cmd.PatchConfig) {
|
||||
c.Patcher = nil
|
||||
},
|
||||
"no_webhooks_are_requested_for_patching": func(c *cmd.PatchConfig) {
|
||||
c.PatchValidating = false
|
||||
c.PatchMutating = false
|
||||
c.APIServiceName = ""
|
||||
},
|
||||
"unsupported_patch_failure_policy_is_defined": func(c *cmd.PatchConfig) {
|
||||
c.PatchFailurePolicy = "foo"
|
||||
},
|
||||
"ca_certificate_from_secret_is_empty": func(c *cmd.PatchConfig) {
|
||||
patcher := testPatcher()
|
||||
patcher.getCaFromSecret = func(_ context.Context, _, _ string) []byte {
|
||||
return nil
|
||||
}
|
||||
c.Patcher = patcher
|
||||
},
|
||||
} {
|
||||
mutateF := mutateF
|
||||
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
config := testPatchConfig()
|
||||
mutateF(config)
|
||||
|
||||
if err := cmd.Patch(ctx, config); err == nil {
|
||||
t.Fatalf("Expected error while patching")
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type patcher struct {
|
||||
patchObjects func(context.Context, k8s.PatchOptions) error
|
||||
getCaFromSecret func(context.Context, string, string) []byte
|
||||
}
|
||||
|
||||
func (p *patcher) PatchObjects(ctx context.Context, options k8s.PatchOptions) error {
|
||||
return p.patchObjects(ctx, options)
|
||||
}
|
||||
|
||||
func (p *patcher) GetCaFromSecret(ctx context.Context, secretName, namespace string) []byte {
|
||||
return p.getCaFromSecret(ctx, secretName, namespace)
|
||||
}
|
||||
|
||||
func testPatcher() *patcher {
|
||||
return &patcher{
|
||||
patchObjects: func(context.Context, k8s.PatchOptions) error {
|
||||
return nil
|
||||
},
|
||||
getCaFromSecret: func(context.Context, string, string) []byte { return []byte{} },
|
||||
}
|
||||
}
|
||||
|
||||
func testPatchConfig() *cmd.PatchConfig {
|
||||
return &cmd.PatchConfig{
|
||||
PatchValidating: true,
|
||||
WebhookName: "foo",
|
||||
Patcher: testPatcher(),
|
||||
}
|
||||
}
|
||||
|
|
@ -6,9 +6,9 @@ import (
|
|||
"github.com/onrik/logrus/filename"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
admissionv1 "k8s.io/api/admissionregistration/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
"k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -29,14 +29,13 @@ var (
|
|||
certName string
|
||||
keyName string
|
||||
host string
|
||||
apiServiceName string
|
||||
webhookName string
|
||||
patchValidating bool
|
||||
patchMutating bool
|
||||
patchFailurePolicy string
|
||||
kubeconfig string
|
||||
}{}
|
||||
|
||||
failurePolicy admissionv1.FailurePolicyType
|
||||
)
|
||||
|
||||
// Execute is the main entry point for the program
|
||||
|
|
@ -84,7 +83,7 @@ func getFormatter(logfmt string) log.Formatter {
|
|||
return nil
|
||||
}
|
||||
|
||||
func newKubernetesClient(kubeconfig string) kubernetes.Interface {
|
||||
func newKubernetesClients(kubeconfig string) (kubernetes.Interface, clientset.Interface) {
|
||||
config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
|
||||
if err != nil {
|
||||
log.WithError(err).Fatal("error building kubernetes config")
|
||||
|
|
@ -95,5 +94,10 @@ func newKubernetesClient(kubeconfig string) kubernetes.Interface {
|
|||
log.WithError(err).Fatal("error creating kubernetes client")
|
||||
}
|
||||
|
||||
return c
|
||||
aggregatorClientset, err := clientset.NewForConfig(config)
|
||||
if err != nil {
|
||||
log.WithError(err).Fatal("error creating kubernetes aggregator client")
|
||||
}
|
||||
|
||||
return c, aggregatorClientset
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue