/
groupsuffix.go
194 lines (161 loc) · 6.51 KB
/
groupsuffix.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
// 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)
}