ContainerImage.Pinniped/internal/groupsuffix/groupsuffix.go
Ryan Richard ba98c8cc14 Enhance Kube middleware to rewrite API group of ownerRefs on update verb
When oidcclientsecretstorage.Set() wants to update the contents of the
storage Secret, it also wants to keep the original ownerRef of the
storage Secret, so it needs the middleware to rewrite the API group
of the ownerRef again during the update (just like it had initially done
during the create of the Secret).
2022-09-21 21:30:44 -07:00

195 lines
6.5 KiB
Go

// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package groupsuffix
import (
"context"
"fmt"
"strings"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/validation"
loginv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/login/v1alpha1"
"go.pinniped.dev/internal/constable"
"go.pinniped.dev/internal/kubeclient"
)
const (
PinnipedDefaultSuffix = "pinniped.dev"
pinnipedDefaultSuffixWithDot = ".pinniped.dev"
)
func New(apiGroupSuffix string) kubeclient.Middleware {
// return a no-op middleware by default
if len(apiGroupSuffix) == 0 || apiGroupSuffix == PinnipedDefaultSuffix {
return nil
}
return kubeclient.Middlewares{
kubeclient.MiddlewareFunc(func(_ context.Context, rt kubeclient.RoundTrip) {
group := rt.Resource().Group
newGroup, ok := Replace(group, apiGroupSuffix)
if !ok {
return // ignore APIs that do not have our group
}
rt.MutateRequest(func(obj kubeclient.Object) error {
typeMeta := obj.GetObjectKind()
origGVK := typeMeta.GroupVersionKind()
newGVK := schema.GroupVersionKind{
Group: newGroup,
Version: origGVK.Version,
Kind: origGVK.Kind,
}
typeMeta.SetGroupVersionKind(newGVK)
return nil
})
}),
kubeclient.MiddlewareFunc(func(_ context.Context, rt kubeclient.RoundTrip) {
// Only mess with ownerRefs on requests to perform edits.
// Not needed on deletes since the object is getting deleted anyway.
// WARNING: This code might need to be enhanced to handle the patch verb
// if we start using patches for objects that have ownerRefs.
if rt.Verb() != kubeclient.VerbCreate && rt.Verb() != kubeclient.VerbUpdate {
return
}
// we probably do not want mess with an owner ref on a subresource
if len(rt.Subresource()) != 0 {
return
}
rt.MutateRequest(mutateOwnerRefs(Replace, apiGroupSuffix))
}),
kubeclient.MiddlewareFunc(func(_ context.Context, rt kubeclient.RoundTrip) {
// we only care if this is a create on a TokenCredentialRequest without a subresource
if rt.Resource() != loginv1alpha1.SchemeGroupVersion.WithResource("tokencredentialrequests") ||
rt.Verb() != kubeclient.VerbCreate ||
rt.Subresource() != "" {
return
}
// we only do this on the way out, since on the way back in we don't set a spec in our
// TokenCredentialRequest
rt.MutateRequest(func(obj kubeclient.Object) error {
tokenCredentialRequest, ok := obj.(*loginv1alpha1.TokenCredentialRequest)
if !ok {
return fmt.Errorf("cannot cast obj of type %T to *loginv1alpha1.TokenCredentialRequest", obj)
}
if tokenCredentialRequest.Spec.Authenticator.APIGroup == nil {
// technically, the APIGroup field is optional, so clients are free to do this, but we
// want our middleware to be opinionated so that it can be really good at a specific task
// and give us specific feedback when it can't do that specific task
return fmt.Errorf(
"cannot replace token credential request %s/%s without authenticator API group",
obj.GetNamespace(), obj.GetName(),
)
}
mutatedAuthenticatorAPIGroup, ok := Replace(*tokenCredentialRequest.Spec.Authenticator.APIGroup, apiGroupSuffix)
if !ok {
// see comment above about specificity of middleware
return fmt.Errorf(
"cannot replace token credential request %s/%s authenticator API group %q with group suffix %q",
obj.GetNamespace(), obj.GetName(),
*tokenCredentialRequest.Spec.Authenticator.APIGroup,
apiGroupSuffix,
)
}
tokenCredentialRequest.Spec.Authenticator.APIGroup = &mutatedAuthenticatorAPIGroup
return nil
})
}),
kubeclient.MiddlewareFunc(func(_ context.Context, rt kubeclient.RoundTrip) {
// always unreplace owner refs with apiGroupSuffix because we can consume those objects across all verbs
rt.MutateResponse(mutateOwnerRefs(Unreplace, apiGroupSuffix))
}),
}
}
func mutateOwnerRefs(replaceFunc func(baseAPIGroup, apiGroupSuffix string) (string, bool), apiGroupSuffix string) func(kubeclient.Object) error {
return func(obj kubeclient.Object) error {
// fix up owner refs because they are consumed by external and internal actors
oldRefs := obj.GetOwnerReferences()
if len(oldRefs) == 0 {
return nil
}
var changedGroup bool
newRefs := make([]metav1.OwnerReference, 0, len(oldRefs))
for _, ref := range oldRefs {
ref := *ref.DeepCopy()
gv, _ := schema.ParseGroupVersion(ref.APIVersion) // error is safe to ignore, empty gv is fine
if newGroup, ok := replaceFunc(gv.Group, apiGroupSuffix); ok {
changedGroup = true
gv.Group = newGroup
ref.APIVersion = gv.String()
}
newRefs = append(newRefs, ref)
}
if !changedGroup {
return nil
}
obj.SetOwnerReferences(newRefs)
return nil
}
}
// Replace constructs an API group from a baseAPIGroup and a parameterized apiGroupSuffix.
//
// We assume that all baseAPIGroup's will end in "pinniped.dev", and therefore we can safely replace
// the reference to "pinniped.dev" with the provided apiGroupSuffix. If the provided baseAPIGroup
// does not end in "pinniped.dev", then this function will return an empty string and false.
//
// See ExampleReplace_loginv1alpha1 and ExampleReplace_string for more information on input/output pairs.
func Replace(baseAPIGroup, apiGroupSuffix string) (string, bool) {
if !strings.HasSuffix(baseAPIGroup, pinnipedDefaultSuffixWithDot) {
return "", false
}
return strings.TrimSuffix(baseAPIGroup, PinnipedDefaultSuffix) + apiGroupSuffix, true
}
// Unreplace is like performing an undo of Replace().
func Unreplace(baseAPIGroup, apiGroupSuffix string) (string, bool) {
if !strings.HasSuffix(baseAPIGroup, "."+apiGroupSuffix) {
return "", false
}
return strings.TrimSuffix(baseAPIGroup, apiGroupSuffix) + PinnipedDefaultSuffix, true
}
// Validate validates the provided apiGroupSuffix is usable as an API group suffix. Specifically, it
// makes sure that the provided apiGroupSuffix is a valid DNS-1123 subdomain with at least one dot,
// to match Kubernetes behavior.
func Validate(apiGroupSuffix string) error {
var errs []error //nolint:prealloc
if len(strings.Split(apiGroupSuffix, ".")) < 2 {
errs = append(errs, constable.Error("must contain '.'"))
}
errorStrings := validation.IsDNS1123Subdomain(apiGroupSuffix)
for _, errorString := range errorStrings {
errorString := errorString
errs = append(errs, constable.Error(errorString))
}
return errors.NewAggregate(errs)
}