supervisor-oidc: checkpoint: controller watches OIDCProviderConfig

Signed-off-by: Andrew Keesler <akeesler@vmware.com>
This commit is contained in:
Andrew Keesler 2020-10-07 10:53:05 -04:00
parent 8a772793b8
commit 019f44982c
No known key found for this signature in database
GPG Key ID: 27CE0444346F9413
8 changed files with 249 additions and 109 deletions

View File

@ -6,22 +6,20 @@ package main
import ( import (
"context" "context"
"fmt" "fmt"
"io/ioutil"
"net" "net"
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
"time" "time"
kubeinformers "k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/pkg/version" "k8s.io/client-go/pkg/version"
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
restclient "k8s.io/client-go/rest" restclient "k8s.io/client-go/rest"
"k8s.io/component-base/logs" "k8s.io/component-base/logs"
"k8s.io/klog/v2" "k8s.io/klog/v2"
"sigs.k8s.io/yaml"
pinnipedclientset "go.pinniped.dev/generated/1.19/client/clientset/versioned"
pinnipedinformers "go.pinniped.dev/generated/1.19/client/informers/externalversions"
"go.pinniped.dev/internal/controller/supervisorconfig" "go.pinniped.dev/internal/controller/supervisorconfig"
"go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/controllerlib"
"go.pinniped.dev/internal/downward" "go.pinniped.dev/internal/downward"
@ -69,63 +67,57 @@ func waitForSignal() os.Signal {
func startControllers( func startControllers(
ctx context.Context, ctx context.Context,
issuerProvider *issuerprovider.Provider, issuerProvider *issuerprovider.Provider,
kubeClient kubernetes.Interface, pinnipedInformers pinnipedinformers.SharedInformerFactory,
kubeInformers kubeinformers.SharedInformerFactory,
serverInstallationNamespace string,
staticConfig StaticConfig,
) { ) {
// Create controller manager. // Create controller manager.
controllerManager := controllerlib. controllerManager := controllerlib.
NewManager(). NewManager().
WithController( WithController(
supervisorconfig.NewDynamicConfigWatcherController( supervisorconfig.NewDynamicConfigWatcherController(
serverInstallationNamespace,
staticConfig.NamesConfig.DynamicConfigMap,
issuerProvider, issuerProvider,
kubeClient, pinnipedInformers.Config().V1alpha1().OIDCProviderConfigs(),
kubeInformers.Core().V1().ConfigMaps(),
controllerlib.WithInformer, controllerlib.WithInformer,
), ),
singletonWorker, singletonWorker,
) )
kubeInformers.Start(ctx.Done()) pinnipedInformers.Start(ctx.Done())
go controllerManager.Start(ctx) go controllerManager.Start(ctx)
} }
func newK8sClient() (kubernetes.Interface, error) { func newPinnipedClient() (pinnipedclientset.Interface, error) {
kubeConfig, err := restclient.InClusterConfig() kubeConfig, err := restclient.InClusterConfig()
if err != nil { if err != nil {
return nil, fmt.Errorf("could not load in-cluster configuration: %w", err) return nil, fmt.Errorf("could not load in-cluster configuration: %w", err)
} }
// Connect to the core Kubernetes API. // Connect to the core Kubernetes API.
kubeClient, err := kubernetes.NewForConfig(kubeConfig) pinnipedClient, err := pinnipedclientset.NewForConfig(kubeConfig)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not load in-cluster configuration: %w", err) return nil, fmt.Errorf("could not load in-cluster configuration: %w", err)
} }
return kubeClient, nil return pinnipedClient, nil
} }
func run(serverInstallationNamespace string, staticConfig StaticConfig) error { func run(serverInstallationNamespace string) error {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
kubeClient, err := newK8sClient() pinnipedClient, err := newPinnipedClient()
if err != nil { if err != nil {
return fmt.Errorf("cannot create k8s client: %w", err) return fmt.Errorf("cannot create k8s client: %w", err)
} }
kubeInformers := kubeinformers.NewSharedInformerFactoryWithOptions( pinnipedInformers := pinnipedinformers.NewSharedInformerFactoryWithOptions(
kubeClient, pinnipedClient,
defaultResyncInterval, defaultResyncInterval,
kubeinformers.WithNamespace(serverInstallationNamespace), pinnipedinformers.WithNamespace(serverInstallationNamespace),
) )
issuerProvider := issuerprovider.New() issuerProvider := issuerprovider.New()
startControllers(ctx, issuerProvider, kubeClient, kubeInformers, serverInstallationNamespace, staticConfig) startControllers(ctx, issuerProvider, pinnipedInformers)
//nolint: gosec // Intentionally binding to all network interfaces. //nolint: gosec // Intentionally binding to all network interfaces.
l, err := net.Listen("tcp", ":80") l, err := net.Listen("tcp", ":80")
@ -143,14 +135,6 @@ func run(serverInstallationNamespace string, staticConfig StaticConfig) error {
return nil return nil
} }
type StaticConfig struct {
NamesConfig NamesConfigSpec `json:"names"`
}
type NamesConfigSpec struct {
DynamicConfigMap string `json:"dynamicConfigMap"`
}
func main() { func main() {
logs.InitLogs() logs.InitLogs()
defer logs.FlushLogs() defer logs.FlushLogs()
@ -164,17 +148,7 @@ func main() {
klog.Fatal(fmt.Errorf("could not read pod metadata: %w", err)) klog.Fatal(fmt.Errorf("could not read pod metadata: %w", err))
} }
// Read static config. if err := run(podInfo.Namespace); err != nil {
data, err := ioutil.ReadFile(os.Args[2])
if err != nil {
klog.Fatal(fmt.Errorf("read file: %w", err))
}
var staticConfig StaticConfig
if err := yaml.Unmarshal(data, &staticConfig); err != nil {
klog.Fatal(fmt.Errorf("decode yaml: %w", err))
}
if err := run(podInfo.Namespace, staticConfig); err != nil {
klog.Fatal(err) klog.Fatal(err)
} }
} }

View File

@ -13,8 +13,8 @@ metadata:
labels: labels:
app: #@ data.values.app_name app: #@ data.values.app_name
rules: rules:
- apiGroups: [""] - apiGroups: [config.pinniped.dev]
resources: [configmaps] resources: [oidcproviderconfigs]
verbs: [get, list, watch] verbs: [get, list, watch]
--- ---
kind: RoleBinding kind: RoleBinding

View File

@ -5,12 +5,12 @@ package supervisorconfig
import ( import (
"fmt" "fmt"
"net/url"
k8serrors "k8s.io/apimachinery/pkg/api/errors" k8serrors "k8s.io/apimachinery/pkg/api/errors"
corev1informers "k8s.io/client-go/informers/core/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/klog/v2" "k8s.io/klog/v2"
configinformers "go.pinniped.dev/generated/1.19/client/informers/externalversions/config/v1alpha1"
pinnipedcontroller "go.pinniped.dev/internal/controller" pinnipedcontroller "go.pinniped.dev/internal/controller"
"go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/controllerlib"
) )
@ -22,41 +22,36 @@ const (
// IssuerSetter can be notified of a valid issuer with its SetIssuer function. If there is no // IssuerSetter can be notified of a valid issuer with its SetIssuer function. If there is no
// longer any valid issuer, then nil can be passed to this interface. // longer any valid issuer, then nil can be passed to this interface.
// //
// If the IssuerSetter doesn't like the provided issuer, it can return an error.
//
// Implementations of this type should be thread-safe to support calls from multiple goroutines. // Implementations of this type should be thread-safe to support calls from multiple goroutines.
type IssuerSetter interface { type IssuerSetter interface {
SetIssuer(issuer *string) SetIssuer(issuer *url.URL) error
} }
type dynamicConfigWatcherController struct { type dynamicConfigWatcherController struct {
configMapName string
configMapNamespace string
issuerSetter IssuerSetter issuerSetter IssuerSetter
k8sClient kubernetes.Interface opcInformer configinformers.OIDCProviderConfigInformer
configMapInformer corev1informers.ConfigMapInformer
} }
// NewDynamicConfigWatcherController creates a controllerlib.Controller that watches
// OIDCProviderConfig objects and notifies a callback object of their creation or deletion.
func NewDynamicConfigWatcherController( func NewDynamicConfigWatcherController(
serverInstallationNamespace string,
configMapName string,
issuerObserver IssuerSetter, issuerObserver IssuerSetter,
k8sClient kubernetes.Interface, opcInformer configinformers.OIDCProviderConfigInformer,
configMapInformer corev1informers.ConfigMapInformer,
withInformer pinnipedcontroller.WithInformerOptionFunc, withInformer pinnipedcontroller.WithInformerOptionFunc,
) controllerlib.Controller { ) controllerlib.Controller {
return controllerlib.New( return controllerlib.New(
controllerlib.Config{ controllerlib.Config{
Name: "DynamicConfigWatcherController", Name: "DynamicConfigWatcherController",
Syncer: &dynamicConfigWatcherController{ Syncer: &dynamicConfigWatcherController{
configMapNamespace: serverInstallationNamespace,
configMapName: configMapName,
issuerSetter: issuerObserver, issuerSetter: issuerObserver,
k8sClient: k8sClient, opcInformer: opcInformer,
configMapInformer: configMapInformer,
}, },
}, },
withInformer( withInformer(
configMapInformer, opcInformer,
pinnipedcontroller.NameAndNamespaceExactMatchFilterFactory(configMapName, serverInstallationNamespace), pinnipedcontroller.NoOpFilter(),
controllerlib.InformerOption{}, controllerlib.InformerOption{},
), ),
) )
@ -69,44 +64,49 @@ func (c *dynamicConfigWatcherController) Sync(ctx controllerlib.Context) error {
// TODO The discovery endpoint would return an error until all missing configuration options are // TODO The discovery endpoint would return an error until all missing configuration options are
// filled in. // filled in.
configMap, err := c.configMapInformer. opc, err := c.opcInformer.
Lister(). Lister().
ConfigMaps(c.configMapNamespace). OIDCProviderConfigs(ctx.Key.Namespace).
Get(c.configMapName) Get(ctx.Key.Name)
notFound := k8serrors.IsNotFound(err) notFound := k8serrors.IsNotFound(err)
if err != nil && !notFound { if err != nil && !notFound {
return fmt.Errorf("failed to get %s/%s secret: %w", c.configMapNamespace, c.configMapName, err) return fmt.Errorf("failed to get %s/%s oidcproviderconfig: %w", ctx.Key.Namespace, ctx.Key.Name, err)
} }
if notFound { if notFound {
klog.InfoS( klog.InfoS(
"dynamicConfigWatcherController Sync found no configmap", "dynamicConfigWatcherController Sync found no oidcproviderconfig",
"configmap", "oidcproviderconfig",
klog.KRef(c.configMapNamespace, c.configMapName), klog.KRef(ctx.Key.Namespace, ctx.Key.Name),
) )
c.issuerSetter.SetIssuer(nil) c.issuerSetter.SetIssuer(nil)
return nil return nil
} }
issuer, ok := configMap.Data[issuerConfigMapKey] url, err := url.Parse(opc.Spec.Issuer)
if !ok { if err != nil {
klog.InfoS( klog.InfoS(
"dynamicConfigWatcherController Sync found no issuer", "dynamicConfigWatcherController Sync failed to parse issuer",
"configmap", "err",
klog.KObj(configMap), err,
) )
c.issuerSetter.SetIssuer(nil)
return nil return nil
} }
klog.InfoS( klog.InfoS(
"dynamicConfigWatcherController Sync issuer", "dynamicConfigWatcherController Sync issuer",
"configmap", "oidcproviderconfig",
klog.KObj(configMap), klog.KObj(opc),
"issuer", "issuer",
issuer, url,
) )
c.issuerSetter.SetIssuer(&issuer) if err := c.issuerSetter.SetIssuer(url); err != nil {
klog.InfoS(
"dynamicConfigWatcherController Sync failed to set issuer",
"err",
err,
)
}
return nil return nil
} }

View File

@ -8,6 +8,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"net/url"
) )
// Metadata holds all fields (that we care about) from the OpenID Provider Metadata section in the // Metadata holds all fields (that we care about) from the OpenID Provider Metadata section in the
@ -30,7 +31,7 @@ type Metadata struct {
// //
// Implementations of this type should be thread-safe to support calls from multiple goroutines. // Implementations of this type should be thread-safe to support calls from multiple goroutines.
type IssuerGetter interface { type IssuerGetter interface {
GetIssuer() *string GetIssuer() *url.URL
} }
// New returns an http.Handler that will use information from the provided IssuerGetter to serve an // New returns an http.Handler that will use information from the provided IssuerGetter to serve an
@ -39,22 +40,23 @@ func New(ig IssuerGetter) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
issuer := ig.GetIssuer()
if issuer == nil {
http.Error(w, `{"error": "OIDC discovery not available (unknown issuer)"}`, http.StatusNotFound)
return
}
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
http.Error(w, `{"error": "Method not allowed (try GET)"}`, http.StatusMethodNotAllowed) http.Error(w, `{"error": "Method not allowed (try GET)"}`, http.StatusMethodNotAllowed)
return return
} }
issuer := ig.GetIssuer() issuerURL := issuer.String()
if issuer == nil {
http.Error(w, `{"error": "OIDC discovery not available (unknown issuer)"}`, http.StatusServiceUnavailable)
return
}
oidcConfig := Metadata{ oidcConfig := Metadata{
Issuer: *issuer, Issuer: issuerURL,
AuthorizationEndpoint: fmt.Sprintf("%s/oauth2/v0/auth", *issuer), AuthorizationEndpoint: fmt.Sprintf("%s/oauth2/v0/auth", issuerURL),
TokenEndpoint: fmt.Sprintf("%s/oauth2/v0/token", *issuer), TokenEndpoint: fmt.Sprintf("%s/oauth2/v0/token", issuerURL),
JWKSURL: fmt.Sprintf("%s/oauth2/v0/keys", *issuer), JWKSURL: fmt.Sprintf("%s/oauth2/v0/keys", issuerURL),
ResponseTypesSupported: []string{}, ResponseTypesSupported: []string{},
SubjectTypesSupported: []string{}, SubjectTypesSupported: []string{},
IDTokenSigningAlgValuesSupported: []string{}, IDTokenSigningAlgValuesSupported: []string{},

View File

@ -7,6 +7,7 @@ import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url"
"testing" "testing"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -18,7 +19,7 @@ func TestDiscovery(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
issuer string issuer *url.URL
method string method string
wantStatus int wantStatus int
@ -26,16 +27,16 @@ func TestDiscovery(t *testing.T) {
wantBody interface{} wantBody interface{}
}{ }{
{ {
name: "issuer returns nil issuer", name: "nil issuer",
method: http.MethodGet, method: http.MethodGet,
wantStatus: http.StatusServiceUnavailable, wantStatus: http.StatusNotFound,
wantBody: map[string]string{ wantBody: map[string]string{
"error": "OIDC discovery not available (unknown issuer)", "error": "OIDC discovery not available (unknown issuer)",
}, },
}, },
{ {
name: "issuer returns non-nil issuer", name: "issuer without path",
issuer: "https://some-issuer.com", issuer: must(url.Parse("https://some-issuer.com")),
method: http.MethodGet, method: http.MethodGet,
wantStatus: http.StatusOK, wantStatus: http.StatusOK,
wantContentType: "application/json", wantContentType: "application/json",
@ -49,9 +50,25 @@ func TestDiscovery(t *testing.T) {
IDTokenSigningAlgValuesSupported: []string{}, IDTokenSigningAlgValuesSupported: []string{},
}, },
}, },
{
name: "issuer with path",
issuer: must(url.Parse("https://some-issuer.com/some/path")),
method: http.MethodGet,
wantStatus: http.StatusOK,
wantContentType: "application/json",
wantBody: &Metadata{
Issuer: "https://some-issuer.com/some/path",
AuthorizationEndpoint: "https://some-issuer.com/some/path/oauth2/v0/auth",
TokenEndpoint: "https://some-issuer.com/some/path/oauth2/v0/token",
JWKSURL: "https://some-issuer.com/some/path/oauth2/v0/keys",
ResponseTypesSupported: []string{},
SubjectTypesSupported: []string{},
IDTokenSigningAlgValuesSupported: []string{},
},
},
{ {
name: "bad method", name: "bad method",
issuer: "https://some-issuer.com", issuer: must(url.Parse("https://some-issuer.com")),
method: http.MethodPost, method: http.MethodPost,
wantStatus: http.StatusMethodNotAllowed, wantStatus: http.StatusMethodNotAllowed,
wantBody: map[string]string{ wantBody: map[string]string{
@ -63,11 +80,7 @@ func TestDiscovery(t *testing.T) {
test := test test := test
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
p := issuerprovider.New() p := issuerprovider.New()
if test.issuer != "" { p.SetIssuer(test.issuer)
p.SetIssuer(&test.issuer)
} else {
p.SetIssuer(nil)
}
handler := New(p) handler := New(p)
req := httptest.NewRequest(test.method, "/this/path/shouldnt/matter", nil) req := httptest.NewRequest(test.method, "/this/path/shouldnt/matter", nil)
@ -88,3 +101,10 @@ func TestDiscovery(t *testing.T) {
}) })
} }
} }
func must(u *url.URL, err error) *url.URL {
if err != nil {
panic(err)
}
return u
}

View File

@ -4,14 +4,20 @@
// Package issuerprovider provides a thread-safe type that can hold on to an OIDC issuer name. // Package issuerprovider provides a thread-safe type that can hold on to an OIDC issuer name.
package issuerprovider package issuerprovider
import "sync" import (
"net/url"
"strings"
"sync"
"go.pinniped.dev/internal/constable"
)
// Provider is a type that can hold onto an issuer value, which may be nil. // Provider is a type that can hold onto an issuer value, which may be nil.
// //
// It is thread-safe. // It is thread-safe.
type Provider struct { type Provider struct {
mu sync.RWMutex mu sync.RWMutex
issuer *string issuer *url.URL
} }
// New returns an empty Provider, i.e., one that holds a nil issuer. // New returns an empty Provider, i.e., one that holds a nil issuer.
@ -19,14 +25,56 @@ func New() *Provider {
return &Provider{} return &Provider{}
} }
func (p *Provider) SetIssuer(issuer *string) { // SetIssuer validates and sets the provided issuer. If validation fails, SetIssuer will return
// an error.
func (p *Provider) SetIssuer(issuer *url.URL) error {
if err := p.validateIssuer(issuer); err != nil {
return err
}
p.setIssuer(issuer)
return nil
}
func (p *Provider) validateIssuer(issuer *url.URL) error {
if issuer == nil {
return nil
}
if issuer.Scheme != "https" && removeMeAfterWeNoLongerNeedHTTPIssuerSupport(issuer.Scheme) {
return constable.Error(`issuer must have "https" scheme`)
}
if issuer.User != nil {
return constable.Error(`issuer must not have username or password`)
}
if strings.HasSuffix(issuer.Path, "/") {
return constable.Error(`issuer must not have trailing slash in path`)
}
if issuer.RawQuery != "" {
return constable.Error(`issuer must not have query`)
}
if issuer.Fragment != "" {
return constable.Error(`issuer must not have fragment`)
}
return nil
}
func (p *Provider) setIssuer(issuer *url.URL) {
p.mu.Lock() p.mu.Lock()
defer p.mu.Unlock() defer p.mu.Unlock()
p.issuer = issuer p.issuer = issuer
} }
func (p *Provider) GetIssuer() *string { func (p *Provider) GetIssuer() *url.URL {
p.mu.RLock() p.mu.RLock()
defer p.mu.RUnlock() defer p.mu.RUnlock()
return p.issuer return p.issuer
} }
func removeMeAfterWeNoLongerNeedHTTPIssuerSupport(scheme string) bool {
return scheme != "http"
}

View File

@ -0,0 +1,84 @@
package issuerprovider
import (
"net/url"
"testing"
"github.com/stretchr/testify/require"
)
func TestProvider(t *testing.T) {
tests := []struct {
name string
issuer *url.URL
wantError string
}{
{
name: "nil issuer",
issuer: nil,
},
{
name: "no scheme",
issuer: must(url.Parse("tuna.com")),
wantError: `issuer must have "https" scheme`,
},
{
name: "bad scheme",
issuer: must(url.Parse("ftp://tuna.com")),
wantError: `issuer must have "https" scheme`,
},
{
name: "fragment",
issuer: must(url.Parse("https://tuna.com/fish#some-frag")),
wantError: `issuer must not have fragment`,
},
{
name: "query",
issuer: must(url.Parse("https://tuna.com?some=query")),
wantError: `issuer must not have query`,
},
{
name: "username",
issuer: must(url.Parse("https://username@tuna.com")),
wantError: `issuer must not have username or password`,
},
{
name: "password",
issuer: must(url.Parse("https://username:password@tuna.com")),
wantError: `issuer must not have username or password`,
},
{
name: "without path",
issuer: must(url.Parse("https://tuna.com")),
},
{
name: "with path",
issuer: must(url.Parse("https://tuna.com/fish/marlin")),
},
{
name: "trailing slash in path",
issuer: must(url.Parse("https://tuna.com/")),
wantError: `issuer must not have trailing slash in path`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := New()
err := p.SetIssuer(tt.issuer)
if tt.wantError != "" {
require.EqualError(t, err, tt.wantError)
require.Nil(t, p.GetIssuer())
} else {
require.NoError(t, err)
require.Equal(t, tt.issuer, p.GetIssuer())
}
})
}
}
func must(u *url.URL, err error) *url.URL {
if err != nil {
panic(err)
}
return u
}

View File

@ -40,9 +40,13 @@ func TestSupervisorOIDCDiscovery(t *testing.T) {
// When this test has finished, recreate any OIDCProviderConfigs that had existed on the cluster before this test. // When this test has finished, recreate any OIDCProviderConfigs that had existed on the cluster before this test.
t.Cleanup(func() { t.Cleanup(func() {
cleanupCtx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
for _, config := range originalConfigList.Items { for _, config := range originalConfigList.Items {
thisConfig := config thisConfig := config
_, err := client.ConfigV1alpha1().OIDCProviderConfigs(ns).Create(ctx, &thisConfig, metav1.CreateOptions{}) thisConfig.ResourceVersion = "" // Get rid of resource version since we can't create an object with one.
_, err := client.ConfigV1alpha1().OIDCProviderConfigs(ns).Create(cleanupCtx, &thisConfig, metav1.CreateOptions{})
require.NoError(t, err) require.NoError(t, err)
} }
}) })
@ -64,6 +68,10 @@ func TestSupervisorOIDCDiscovery(t *testing.T) {
// Create a new OIDCProviderConfig with a known issuer. // Create a new OIDCProviderConfig with a known issuer.
issuer := fmt.Sprintf("http://%s/nested/issuer", env.SupervisorAddress) issuer := fmt.Sprintf("http://%s/nested/issuer", env.SupervisorAddress)
newOIDCProviderConfig := v1alpha1.OIDCProviderConfig{ newOIDCProviderConfig := v1alpha1.OIDCProviderConfig{
TypeMeta: metav1.TypeMeta{
Kind: "OIDCProviderConfig",
APIVersion: v1alpha1.SchemeGroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: "nested-issuser-config-from-integration-test", Name: "nested-issuser-config-from-integration-test",
Namespace: ns, Namespace: ns,
@ -77,7 +85,10 @@ func TestSupervisorOIDCDiscovery(t *testing.T) {
// When this test has finished, clean up the new OIDCProviderConfig. // When this test has finished, clean up the new OIDCProviderConfig.
t.Cleanup(func() { t.Cleanup(func() {
err = client.ConfigV1alpha1().OIDCProviderConfigs(ns).Delete(ctx, newOIDCProviderConfig.Name, metav1.DeleteOptions{}) cleanupCtx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
err = client.ConfigV1alpha1().OIDCProviderConfigs(ns).Delete(cleanupCtx, newOIDCProviderConfig.Name, metav1.DeleteOptions{})
require.NoError(t, err) require.NoError(t, err)
}) })
@ -94,9 +105,11 @@ func TestSupervisorOIDCDiscovery(t *testing.T) {
var response *http.Response var response *http.Response
assert.Eventually(t, func() bool { assert.Eventually(t, func() bool {
response, err = httpClient.Do(requestDiscoveryEndpoint) //nolint:bodyclose // the body is closed below after it is read response, err = httpClient.Do(requestDiscoveryEndpoint) //nolint:bodyclose // the body is closed below after it is read
return err == nil return err == nil && response.StatusCode == http.StatusOK
}, 10*time.Second, 200*time.Millisecond) }, 10*time.Second, 200*time.Millisecond)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, http.StatusOK, response.StatusCode)
responseBody, err := ioutil.ReadAll(response.Body) responseBody, err := ioutil.ReadAll(response.Body)
require.NoError(t, err) require.NoError(t, err)
err = response.Body.Close() err = response.Body.Close()
@ -116,7 +129,6 @@ func TestSupervisorOIDCDiscovery(t *testing.T) {
}`) }`)
expectedJSON := fmt.Sprintf(expectedResultTemplate, issuer, issuer, issuer, issuer) expectedJSON := fmt.Sprintf(expectedResultTemplate, issuer, issuer, issuer, issuer)
require.Equal(t, 200, response.StatusCode)
require.Equal(t, "application/json", response.Header.Get("content-type")) require.Equal(t, "application/json", response.Header.Get("content-type"))
require.JSONEq(t, expectedJSON, string(responseBody)) require.JSONEq(t, expectedJSON, string(responseBody))
} }