Upon pod startup, update the Status of CredentialIssuerConfig

- Indicate the success or failure of the cluster signing key strategy
- Also introduce the concept of "capabilities" of an integration test
  cluster to allow the integration tests to be run against clusters
  that do or don't allow the borrowing of the cluster signing key
- Tests that are not expected to pass on clusters that lack the
  borrowing of the signing key capability are now ignored by
  calling the new library.SkipUnlessClusterHasCapability test helper
- Rename library.Getenv to library.GetEnv
- Add copyrights where they were missing
This commit is contained in:
Ryan Richard 2020-08-24 18:07:34 -07:00
parent 399e1d2eb8
commit 6e59596285
26 changed files with 376 additions and 127 deletions

View File

@ -1,4 +1,5 @@
#@ load("@ytt:data", "data") #! Copyright 2020 VMware, Inc.
#! SPDX-License-Identifier: Apache-2.0
#! Example of valid CredentialIssuerConfig object: #! Example of valid CredentialIssuerConfig object:
#! --- #! ---
@ -8,15 +9,17 @@
#! name: credential-issuer-config #! name: credential-issuer-config
#! namespace: integration #! namespace: integration
#! status: #! status:
#! strategies:
#! - type: KubeClusterSigningCertificate
#! lastUpdateTime: 2020-08-21T20:08:18Z
#! status: Error
#! reason: CouldNotFetchKey
#! message: "There was an error getting the signing cert"
#! kubeConfigInfo: #! kubeConfigInfo:
#! server: https://foo #! server: https://foo
#! certificateAuthorityData: bar #! certificateAuthorityData: bar
#! strategies:
#! - type: KubeClusterSigningCertificate
#! status: Error
#! reason: CouldNotFetchKey
#! message: "There was an error getting the signing cert"
#! lastUpdateTime: 2020-08-21T20:08:18Z
#@ load("@ytt:data", "data")
--- ---
apiVersion: apiextensions.k8s.io/v1 apiVersion: apiextensions.k8s.io/v1

View File

@ -1,3 +1,6 @@
#! Copyright 2020 VMware, Inc.
#! SPDX-License-Identifier: Apache-2.0
#@ load("@ytt:data", "data") #@ load("@ytt:data", "data")
--- ---

View File

@ -1,3 +1,6 @@
#! Copyright 2020 VMware, Inc.
#! SPDX-License-Identifier: Apache-2.0
#@ load("@ytt:data", "data") #@ load("@ytt:data", "data")
#! Give permission to various cluster-scoped objects #! Give permission to various cluster-scoped objects

View File

@ -1,3 +1,6 @@
#! Copyright 2020 VMware, Inc.
#! SPDX-License-Identifier: Apache-2.0
#@data/values #@data/values
--- ---

View File

@ -1,5 +1,10 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Copyright 2020 VMware, Inc.
# SPDX-License-Identifier: Apache-2.0
set -euo pipefail set -euo pipefail
ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )" ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )"
KUBE_ROOT="${ROOT}" # required by `hack/lib/version.sh` KUBE_ROOT="${ROOT}" # required by `hack/lib/version.sh`

View File

@ -1,5 +1,8 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Copyright 2020 VMware, Inc.
# SPDX-License-Identifier: Apache-2.0
set -euo pipefail set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"

View File

@ -2,7 +2,9 @@
# Copyright 2020 VMware, Inc. # Copyright 2020 VMware, Inc.
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
set -euo pipefail set -euo pipefail
ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )" ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )"
"$ROOT/hack/module.sh" unittest "$ROOT/hack/module.sh" unittest

View File

@ -2,7 +2,9 @@
# Copyright 2020 VMware, Inc. # Copyright 2020 VMware, Inc.
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
set -euo pipefail set -euo pipefail
ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )" ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )"
"$ROOT/hack/module.sh" tidy "$ROOT/hack/module.sh" tidy

View File

@ -2,7 +2,9 @@
# Copyright 2020 VMware, Inc. # Copyright 2020 VMware, Inc.
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
set -euo pipefail set -euo pipefail
ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )" ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )"
"$ROOT/hack/module.sh" lint "$ROOT/hack/module.sh" lint

View File

@ -9,6 +9,7 @@ import (
"context" "context"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"reflect"
k8serrors "k8s.io/apimachinery/pkg/api/errors" k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -109,61 +110,90 @@ func (c *publisherController) Sync(ctx controller.Context) error {
server = *c.serverOverride server = *c.serverOverride
} }
credentialIssuerConfig := crdpinnipedv1alpha1.CredentialIssuerConfig{ existingCredentialIssuerConfigFromInformerCache, err := c.credentialIssuerConfigInformer.
TypeMeta: metav1.TypeMeta{}, Lister().
ObjectMeta: metav1.ObjectMeta{ CredentialIssuerConfigs(c.namespace).
Name: configName, Get(configName)
Namespace: c.namespace, notFound = k8serrors.IsNotFound(err)
}, if err != nil && !notFound {
Status: crdpinnipedv1alpha1.CredentialIssuerConfigStatus{ return fmt.Errorf("could not get credentialissuerconfig: %w", err)
Strategies: []crdpinnipedv1alpha1.CredentialIssuerConfigStrategy{}, }
KubeConfigInfo: &crdpinnipedv1alpha1.CredentialIssuerConfigKubeConfigInfo{
updateServerAndCAFunc := func(c *crdpinnipedv1alpha1.CredentialIssuerConfig) {
c.Status.KubeConfigInfo = &crdpinnipedv1alpha1.CredentialIssuerConfigKubeConfigInfo{
Server: server, Server: server,
CertificateAuthorityData: certificateAuthorityData, CertificateAuthorityData: certificateAuthorityData,
},
},
} }
if err := c.createOrUpdateCredentialIssuerConfig(ctx.Context, &credentialIssuerConfig); err != nil { }
err = createOrUpdateCredentialIssuerConfig(
ctx.Context,
existingCredentialIssuerConfigFromInformerCache,
notFound,
configName,
c.namespace,
c.pinnipedClient,
updateServerAndCAFunc)
return err return err
} }
return nil func CreateOrUpdateCredentialIssuerConfig(
}
func (c *publisherController) createOrUpdateCredentialIssuerConfig(
ctx context.Context, ctx context.Context,
newCredentialIssuerConfig *crdpinnipedv1alpha1.CredentialIssuerConfig, credentialIssuerConfigNamespace string,
pinnipedClient pinnipedclientset.Interface,
applyUpdatesToCredentialIssuerConfigFunc func(configToUpdate *crdpinnipedv1alpha1.CredentialIssuerConfig),
) error { ) error {
existingCredentialIssuerConfig, err := c.credentialIssuerConfigInformer. credentialIssuerConfigName := configName
Lister().
CredentialIssuerConfigs(c.namespace). existingCredentialIssuerConfig, err := pinnipedClient.
Get(newCredentialIssuerConfig.Name) CrdV1alpha1().
CredentialIssuerConfigs(credentialIssuerConfigNamespace).
Get(ctx, credentialIssuerConfigName, metav1.GetOptions{})
notFound := k8serrors.IsNotFound(err) notFound := k8serrors.IsNotFound(err)
if err != nil && !notFound { if err != nil && !notFound {
return fmt.Errorf("could not get credentialissuerconfig: %w", err) return fmt.Errorf("could not get credentialissuerconfig: %w", err)
} }
credentialIssuerConfigsClient := c.pinnipedClient.CrdV1alpha1().CredentialIssuerConfigs(c.namespace) return createOrUpdateCredentialIssuerConfig(
if notFound {
if _, err := credentialIssuerConfigsClient.Create(
ctx,
newCredentialIssuerConfig,
metav1.CreateOptions{},
); err != nil {
return fmt.Errorf("could not create credentialissuerconfig: %w", err)
}
} else if !equal(existingCredentialIssuerConfig, newCredentialIssuerConfig) {
// Update just the fields we care about.
newServer := newCredentialIssuerConfig.Status.KubeConfigInfo.Server
newCA := newCredentialIssuerConfig.Status.KubeConfigInfo.CertificateAuthorityData
existingCredentialIssuerConfig.Status.KubeConfigInfo.Server = newServer
existingCredentialIssuerConfig.Status.KubeConfigInfo.CertificateAuthorityData = newCA
if _, err := credentialIssuerConfigsClient.Update(
ctx, ctx,
existingCredentialIssuerConfig, existingCredentialIssuerConfig,
metav1.UpdateOptions{}, notFound,
); err != nil { credentialIssuerConfigName,
credentialIssuerConfigNamespace,
pinnipedClient,
applyUpdatesToCredentialIssuerConfigFunc)
}
func createOrUpdateCredentialIssuerConfig(
ctx context.Context,
existingCredentialIssuerConfig *crdpinnipedv1alpha1.CredentialIssuerConfig,
notFound bool,
credentialIssuerConfigName string,
credentialIssuerConfigNamespace string,
pinnipedClient pinnipedclientset.Interface,
applyUpdatesToCredentialIssuerConfigFunc func(configToUpdate *crdpinnipedv1alpha1.CredentialIssuerConfig),
) error {
credentialIssuerConfigsClient := pinnipedClient.CrdV1alpha1().CredentialIssuerConfigs(credentialIssuerConfigNamespace)
if notFound {
// Create it
credentialIssuerConfig := minimalValidCredentialIssuerConfig(credentialIssuerConfigName, credentialIssuerConfigNamespace)
applyUpdatesToCredentialIssuerConfigFunc(credentialIssuerConfig)
if _, err := credentialIssuerConfigsClient.Create(ctx, credentialIssuerConfig, metav1.CreateOptions{}); err != nil {
return fmt.Errorf("could not create credentialissuerconfig: %w", err)
}
} else {
// Already exists, so check to see if we need to update it
credentialIssuerConfig := existingCredentialIssuerConfig.DeepCopy()
applyUpdatesToCredentialIssuerConfigFunc(credentialIssuerConfig)
if reflect.DeepEqual(existingCredentialIssuerConfig.Status, credentialIssuerConfig.Status) {
// Nothing interesting would change as a result of this update, so skip it
return nil
}
if _, err := credentialIssuerConfigsClient.Update(ctx, credentialIssuerConfig, metav1.UpdateOptions{}); err != nil {
return fmt.Errorf("could not update credentialissuerconfig: %w", err) return fmt.Errorf("could not update credentialissuerconfig: %w", err)
} }
} }
@ -171,7 +201,19 @@ func (c *publisherController) createOrUpdateCredentialIssuerConfig(
return nil return nil
} }
func equal(a, b *crdpinnipedv1alpha1.CredentialIssuerConfig) bool { func minimalValidCredentialIssuerConfig(
return a.Status.KubeConfigInfo.Server == b.Status.KubeConfigInfo.Server && credentialIssuerConfigName string,
a.Status.KubeConfigInfo.CertificateAuthorityData == b.Status.KubeConfigInfo.CertificateAuthorityData credentialIssuerConfigNamespace string,
) *crdpinnipedv1alpha1.CredentialIssuerConfig {
return &crdpinnipedv1alpha1.CredentialIssuerConfig{
TypeMeta: metav1.TypeMeta{},
ObjectMeta: metav1.ObjectMeta{
Name: credentialIssuerConfigName,
Namespace: credentialIssuerConfigNamespace,
},
Status: crdpinnipedv1alpha1.CredentialIssuerConfigStatus{
Strategies: []crdpinnipedv1alpha1.CredentialIssuerConfigStrategy{},
KubeConfigInfo: nil,
},
}
} }

View File

@ -13,19 +13,24 @@ import (
"time" "time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
genericapiserver "k8s.io/apiserver/pkg/server" genericapiserver "k8s.io/apiserver/pkg/server"
genericoptions "k8s.io/apiserver/pkg/server/options" genericoptions "k8s.io/apiserver/pkg/server/options"
"k8s.io/apiserver/plugin/pkg/authenticator/token/webhook" "k8s.io/apiserver/plugin/pkg/authenticator/token/webhook"
"k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes"
restclient "k8s.io/client-go/rest" restclient "k8s.io/client-go/rest"
"k8s.io/klog/v2"
"github.com/suzerain-io/pinniped/internal/apiserver" "github.com/suzerain-io/pinniped/internal/apiserver"
"github.com/suzerain-io/pinniped/internal/certauthority/kubecertauthority" "github.com/suzerain-io/pinniped/internal/certauthority/kubecertauthority"
"github.com/suzerain-io/pinniped/internal/controller/issuerconfig"
"github.com/suzerain-io/pinniped/internal/controllermanager" "github.com/suzerain-io/pinniped/internal/controllermanager"
"github.com/suzerain-io/pinniped/internal/downward" "github.com/suzerain-io/pinniped/internal/downward"
"github.com/suzerain-io/pinniped/internal/provider" "github.com/suzerain-io/pinniped/internal/provider"
"github.com/suzerain-io/pinniped/internal/registry/credentialrequest" "github.com/suzerain-io/pinniped/internal/registry/credentialrequest"
crdpinnipedv1alpha1 "github.com/suzerain-io/pinniped/kubernetes/1.19/api/apis/crdpinniped/v1alpha1"
pinnipedv1alpha1 "github.com/suzerain-io/pinniped/kubernetes/1.19/api/apis/pinniped/v1alpha1" pinnipedv1alpha1 "github.com/suzerain-io/pinniped/kubernetes/1.19/api/apis/pinniped/v1alpha1"
pinnipedclientset "github.com/suzerain-io/pinniped/kubernetes/1.19/client-go/clientset/versioned"
"github.com/suzerain-io/pinniped/pkg/config" "github.com/suzerain-io/pinniped/pkg/config"
) )
@ -99,8 +104,15 @@ func (a *App) runServer(ctx context.Context) error {
return fmt.Errorf("could not load config: %w", err) return fmt.Errorf("could not load config: %w", err)
} }
// Discover in which namespace we are installed.
podInfo, err := downward.Load(a.downwardAPIPath)
if err != nil {
return fmt.Errorf("could not read pod metadata: %w", err)
}
serverInstallationNamespace := podInfo.Namespace
// Load the Kubernetes cluster signing CA. // Load the Kubernetes cluster signing CA.
k8sClusterCA, shutdownCA, err := getClusterCASigner() k8sClusterCA, shutdownCA, err := getClusterCASigner(ctx, serverInstallationNamespace)
if err != nil { if err != nil {
return err return err
} }
@ -112,13 +124,6 @@ func (a *App) runServer(ctx context.Context) error {
return fmt.Errorf("could not create webhook client: %w", err) return fmt.Errorf("could not create webhook client: %w", err)
} }
// Discover in which namespace we are installed.
podInfo, err := downward.Load(a.downwardAPIPath)
if err != nil {
return fmt.Errorf("could not read pod metadata: %w", err)
}
serverInstallationNamespace := podInfo.Namespace
// This cert provider will provide certs to the API server and will // This cert provider will provide certs to the API server and will
// be mutated by a controller to keep the certs up to date with what // be mutated by a controller to keep the certs up to date with what
// is stored in a k8s Secret. Therefore it also effectively acting as // is stored in a k8s Secret. Therefore it also effectively acting as
@ -160,7 +165,7 @@ func (a *App) runServer(ctx context.Context) error {
return server.GenericAPIServer.PrepareRun().Run(ctx.Done()) return server.GenericAPIServer.PrepareRun().Run(ctx.Done())
} }
func getClusterCASigner() (*kubecertauthority.CA, kubecertauthority.ShutdownFunc, error) { func getClusterCASigner(ctx context.Context, serverInstallationNamespace string) (*kubecertauthority.CA, kubecertauthority.ShutdownFunc, error) {
// Load the Kubernetes client configuration. // Load the Kubernetes client configuration.
kubeConfig, err := restclient.InClusterConfig() kubeConfig, err := restclient.InClusterConfig()
if err != nil { if err != nil {
@ -173,6 +178,12 @@ func getClusterCASigner() (*kubecertauthority.CA, kubecertauthority.ShutdownFunc
return nil, nil, fmt.Errorf("could not initialize Kubernetes client: %w", err) return nil, nil, fmt.Errorf("could not initialize Kubernetes client: %w", err)
} }
// Connect to the pinniped API.
pinnipedClient, err := pinnipedclientset.NewForConfig(kubeConfig)
if err != nil {
return nil, nil, fmt.Errorf("could not initialize pinniped client: %w", err)
}
// Make a clock tick that triggers a periodic refresh. // Make a clock tick that triggers a periodic refresh.
ticker := time.NewTicker(5 * time.Minute) ticker := time.NewTicker(5 * time.Minute)
@ -182,10 +193,51 @@ func getClusterCASigner() (*kubecertauthority.CA, kubecertauthority.ShutdownFunc
kubecertauthority.NewPodCommandExecutor(kubeConfig, kubeClient), kubecertauthority.NewPodCommandExecutor(kubeConfig, kubeClient),
ticker.C, ticker.C,
) )
if err != nil { if err != nil {
ticker.Stop() ticker.Stop()
updateErr := issuerconfig.CreateOrUpdateCredentialIssuerConfig(
ctx,
serverInstallationNamespace,
pinnipedClient,
func(configToUpdate *crdpinnipedv1alpha1.CredentialIssuerConfig) {
configToUpdate.Status.Strategies = []crdpinnipedv1alpha1.CredentialIssuerConfigStrategy{
{
Type: crdpinnipedv1alpha1.KubeClusterSigningCertificateStrategyType,
Status: crdpinnipedv1alpha1.ErrorStrategyStatus,
Reason: crdpinnipedv1alpha1.CouldNotFetchKeyStrategyReason,
Message: err.Error(),
LastUpdateTime: metav1.Now(),
},
}
},
)
klog.Errorf("error performing create or update on CredentialIssuerConfig to add strategy error: %w", updateErr)
return nil, nil, fmt.Errorf("could not load cluster signing CA: %w", err) return nil, nil, fmt.Errorf("could not load cluster signing CA: %w", err)
} }
updateErr := issuerconfig.CreateOrUpdateCredentialIssuerConfig(
ctx,
serverInstallationNamespace,
pinnipedClient,
func(configToUpdate *crdpinnipedv1alpha1.CredentialIssuerConfig) {
configToUpdate.Status.Strategies = []crdpinnipedv1alpha1.CredentialIssuerConfigStrategy{
{
Type: crdpinnipedv1alpha1.KubeClusterSigningCertificateStrategyType,
Status: crdpinnipedv1alpha1.SuccessStrategyStatus,
Reason: crdpinnipedv1alpha1.FetchedKeyStrategyReason,
Message: "Key was fetched successfully",
LastUpdateTime: metav1.Now(),
},
}
},
)
if updateErr != nil {
return nil, nil, fmt.Errorf("error performing create or update on CredentialIssuerConfig to add strategy success: %w", updateErr)
}
return k8sClusterCA, func() { shutdownCA(); ticker.Stop() }, nil return k8sClusterCA, func() { shutdownCA(); ticker.Stop() }, nil
} }

View File

@ -7,6 +7,20 @@ package v1alpha1
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
type StrategyType string
type StrategyStatus string
type StrategyReason string
const (
KubeClusterSigningCertificateStrategyType = StrategyType("KubeClusterSigningCertificate")
SuccessStrategyStatus = StrategyStatus("Success")
ErrorStrategyStatus = StrategyStatus("Error")
CouldNotFetchKeyStrategyReason = StrategyReason("CouldNotFetchKey")
FetchedKeyStrategyReason = StrategyReason("FetchedKey")
)
type CredentialIssuerConfigStatus struct { type CredentialIssuerConfigStatus struct {
Strategies []CredentialIssuerConfigStrategy `json:"strategies"` Strategies []CredentialIssuerConfigStrategy `json:"strategies"`
@ -23,9 +37,9 @@ type CredentialIssuerConfigKubeConfigInfo struct {
} }
type CredentialIssuerConfigStrategy struct { type CredentialIssuerConfigStrategy struct {
Type string `json:"type,omitempty"` Type StrategyType `json:"type,omitempty"`
Status string `json:"status,omitempty"` Status StrategyStatus `json:"status,omitempty"`
Reason string `json:"reason,omitempty"` Reason StrategyReason `json:"reason,omitempty"`
Message string `json:"message,omitempty"` Message string `json:"message,omitempty"`
LastUpdateTime metav1.Time `json:"lastUpdateTime"` LastUpdateTime metav1.Time `json:"lastUpdateTime"`
} }

View File

@ -0,0 +1,8 @@
# Copyright 2020 VMware, Inc.
# SPDX-License-Identifier: Apache-2.0
# Describe the capabilities of the cluster against which the integration tests will run.
capabilities:
# Is it possible to borrow the cluster's signing key from the kube API server?
clusterSigningKeyIsAvailable: false

View File

@ -0,0 +1,8 @@
# Copyright 2020 VMware, Inc.
# SPDX-License-Identifier: Apache-2.0
# Describe the capabilities of the cluster against which the integration tests will run.
capabilities:
# Is it possible to borrow the cluster's signing key from the kube API server?
clusterSigningKeyIsAvailable: true

View File

@ -0,0 +1,8 @@
# Copyright 2020 VMware, Inc.
# SPDX-License-Identifier: Apache-2.0
# Describe the capabilities of the cluster against which the integration tests will run.
capabilities:
# Is it possible to borrow the cluster's signing key from the kube API server?
clusterSigningKeyIsAvailable: true

View File

@ -4,6 +4,7 @@ go 1.14
require ( require (
github.com/davecgh/go-spew v1.1.1 github.com/davecgh/go-spew v1.1.1
github.com/ghodss/yaml v1.0.0
github.com/stretchr/testify v1.6.1 github.com/stretchr/testify v1.6.1
github.com/suzerain-io/pinniped v0.0.0-20200819182107-1b9a70d089f4 github.com/suzerain-io/pinniped v0.0.0-20200819182107-1b9a70d089f4
github.com/suzerain-io/pinniped/kubernetes/1.19/api v0.0.0-00010101000000-000000000000 github.com/suzerain-io/pinniped/kubernetes/1.19/api v0.0.0-00010101000000-000000000000

View File

@ -103,6 +103,7 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-critic/go-critic v0.5.0 h1:Ic2p5UCl5fX/2WX2w8nroPpPhxRNsNTMlJzsu/uqwnM= github.com/go-critic/go-critic v0.5.0 h1:Ic2p5UCl5fX/2WX2w8nroPpPhxRNsNTMlJzsu/uqwnM=
github.com/go-critic/go-critic v0.5.0/go.mod h1:4jeRh3ZAVnRYhuWdOEvwzVqLUpxMSoAT0xZ74JsTPlo= github.com/go-critic/go-critic v0.5.0/go.mod h1:4jeRh3ZAVnRYhuWdOEvwzVqLUpxMSoAT0xZ74JsTPlo=

View File

@ -16,6 +16,7 @@ import (
func TestGetAPIResourceList(t *testing.T) { func TestGetAPIResourceList(t *testing.T) {
library.SkipUnlessIntegration(t) library.SkipUnlessIntegration(t)
library.SkipUnlessClusterHasCapability(t, library.ClusterSigningKeyIsAvailable)
client := library.NewPinnipedClientset(t) client := library.NewPinnipedClientset(t)
@ -60,7 +61,7 @@ func TestGetAPIResourceList(t *testing.T) {
SingularName: "", SingularName: "",
} }
expectedLDCAPIResource := metav1.APIResource{ expectedCredentialIssuerConfigResource := metav1.APIResource{
Name: "credentialissuerconfigs", Name: "credentialissuerconfigs",
SingularName: "credentialissuerconfig", SingularName: "credentialissuerconfig",
Namespaced: true, Namespaced: true,
@ -79,8 +80,8 @@ func TestGetAPIResourceList(t *testing.T) {
actualAPIResource := actualCrdPinnipedResources.APIResources[0] actualAPIResource := actualCrdPinnipedResources.APIResources[0]
// workaround because its hard to predict the storage version hash (e.g. "t/+v41y+3e4=") // workaround because its hard to predict the storage version hash (e.g. "t/+v41y+3e4=")
// so just don't worry about comparing that field // so just don't worry about comparing that field
expectedLDCAPIResource.StorageVersionHash = actualAPIResource.StorageVersionHash expectedCredentialIssuerConfigResource.StorageVersionHash = actualAPIResource.StorageVersionHash
require.Equal(t, expectedLDCAPIResource, actualAPIResource) require.Equal(t, expectedCredentialIssuerConfigResource, actualAPIResource)
} }
func findGroup(name string, groups []*metav1.APIGroup) *metav1.APIGroup { func findGroup(name string, groups []*metav1.APIGroup) *metav1.APIGroup {

View File

@ -22,6 +22,7 @@ import (
func TestAPIServingCertificateAutoCreationAndRotation(t *testing.T) { func TestAPIServingCertificateAutoCreationAndRotation(t *testing.T) {
library.SkipUnlessIntegration(t) library.SkipUnlessIntegration(t)
library.SkipUnlessClusterHasCapability(t, library.ClusterSigningKeyIsAvailable)
tests := []struct { tests := []struct {
name string name string
@ -74,7 +75,7 @@ func TestAPIServingCertificateAutoCreationAndRotation(t *testing.T) {
for _, test := range tests { for _, test := range tests {
test := test test := test
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
namespaceName := library.Getenv(t, "PINNIPED_NAMESPACE") namespaceName := library.GetEnv(t, "PINNIPED_NAMESPACE")
kubeClient := library.NewClientset(t) kubeClient := library.NewClientset(t)
aggregatedClient := library.NewAggregatedClientset(t) aggregatedClient := library.NewAggregatedClientset(t)
@ -108,7 +109,7 @@ func TestAPIServingCertificateAutoCreationAndRotation(t *testing.T) {
return err == nil return err == nil
} }
assert.Eventually(t, secretIsRegenerated, 10*time.Second, 250*time.Millisecond) assert.Eventually(t, secretIsRegenerated, 10*time.Second, 250*time.Millisecond)
require.NoError(t, err) // prints out the error in case of failure require.NoError(t, err) // prints out the error and stops the test in case of failure
regeneratedCACert := secret.Data["caCertificate"] regeneratedCACert := secret.Data["caCertificate"]
regeneratedPrivateKey := secret.Data["tlsPrivateKey"] regeneratedPrivateKey := secret.Data["tlsPrivateKey"]
regeneratedCertChain := secret.Data["tlsCertificateChain"] regeneratedCertChain := secret.Data["tlsCertificateChain"]
@ -125,7 +126,7 @@ func TestAPIServingCertificateAutoCreationAndRotation(t *testing.T) {
return err == nil return err == nil
} }
assert.Eventually(t, aggregatedAPIUpdated, 10*time.Second, 250*time.Millisecond) assert.Eventually(t, aggregatedAPIUpdated, 10*time.Second, 250*time.Millisecond)
require.NoError(t, err) // prints out the error in case of failure require.NoError(t, err) // prints out the error and stops the test in case of failure
require.Equal(t, regeneratedCACert, apiService.Spec.CABundle) require.Equal(t, regeneratedCACert, apiService.Spec.CABundle)
// Check that we can still make requests to the aggregated API through the kube API server, // Check that we can still make requests to the aggregated API through the kube API server,
@ -147,7 +148,7 @@ func TestAPIServingCertificateAutoCreationAndRotation(t *testing.T) {
// Unfortunately, although our code changes all the certs immediately, it seems to take ~1 minute for // Unfortunately, although our code changes all the certs immediately, it seems to take ~1 minute for
// the API machinery to notice that we updated our serving cert, causing 1 minute of downtime for our endpoint. // the API machinery to notice that we updated our serving cert, causing 1 minute of downtime for our endpoint.
assert.Eventually(t, aggregatedAPIWorking, 2*time.Minute, 250*time.Millisecond) assert.Eventually(t, aggregatedAPIWorking, 2*time.Minute, 250*time.Millisecond)
require.NoError(t, err) // prints out the error in case of failure require.NoError(t, err) // prints out the error and stops the test in case of failure
}) })
} }
} }

View File

@ -20,8 +20,9 @@ import (
func TestGetDeployment(t *testing.T) { func TestGetDeployment(t *testing.T) {
library.SkipUnlessIntegration(t) library.SkipUnlessIntegration(t)
namespaceName := library.Getenv(t, "PINNIPED_NAMESPACE") library.SkipUnlessClusterHasCapability(t, library.ClusterSigningKeyIsAvailable)
deploymentName := library.Getenv(t, "PINNIPED_APP_NAME") namespaceName := library.GetEnv(t, "PINNIPED_NAMESPACE")
deploymentName := library.GetEnv(t, "PINNIPED_APP_NAME")
client := library.NewClientset(t) client := library.NewClientset(t)

View File

@ -56,7 +56,8 @@ var maskKey = func(s string) string { return strings.ReplaceAll(s, "TESTING KEY"
func TestClient(t *testing.T) { func TestClient(t *testing.T) {
library.SkipUnlessIntegration(t) library.SkipUnlessIntegration(t)
tmcClusterToken := library.Getenv(t, "PINNIPED_TMC_CLUSTER_TOKEN") library.SkipUnlessClusterHasCapability(t, library.ClusterSigningKeyIsAvailable)
tmcClusterToken := library.GetEnv(t, "PINNIPED_TMC_CLUSTER_TOKEN")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel() defer cancel()

View File

@ -11,6 +11,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
@ -19,65 +20,91 @@ import (
"github.com/suzerain-io/pinniped/test/library" "github.com/suzerain-io/pinniped/test/library"
) )
func TestSuccessfulCredentialIssuerConfig(t *testing.T) { func TestCredentialIssuerConfig(t *testing.T) {
library.SkipUnlessIntegration(t) library.SkipUnlessIntegration(t)
namespaceName := library.Getenv(t, "PINNIPED_NAMESPACE") namespaceName := library.GetEnv(t, "PINNIPED_NAMESPACE")
config := library.NewClientConfig(t)
client := library.NewPinnipedClientset(t) client := library.NewPinnipedClientset(t)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel() defer cancel()
config := library.NewClientConfig(t) t.Run("test successful CredentialIssuerConfig", func(t *testing.T) {
expectedLDCStatus := expectedLDCStatus(config) actualConfigList, err := client.
configList, err := client.
CrdV1alpha1(). CrdV1alpha1().
CredentialIssuerConfigs(namespaceName). CredentialIssuerConfigs(namespaceName).
List(ctx, metav1.ListOptions{}) List(ctx, metav1.ListOptions{})
require.NoError(t, err) require.NoError(t, err)
require.Len(t, configList.Items, 1) require.Len(t, actualConfigList.Items, 1)
require.Equal(t, expectedLDCStatus, &configList.Items[0].Status)
// Verify the published kube config info.
actualStatusKubeConfigInfo := actualConfigList.Items[0].Status.KubeConfigInfo
require.Equal(t, expectedStatusKubeConfigInfo(config), actualStatusKubeConfigInfo)
// Verify the cluster strategy status based on what's expected of the test cluster's ability to share signing keys.
actualStatusStrategies := actualConfigList.Items[0].Status.Strategies
require.Len(t, actualStatusStrategies, 1)
actualStatusStrategy := actualStatusStrategies[0]
require.Equal(t, crdpinnipedv1alpha1.KubeClusterSigningCertificateStrategyType, actualStatusStrategy.Type)
if library.ClusterHasCapability(t, library.ClusterSigningKeyIsAvailable) {
require.Equal(t, crdpinnipedv1alpha1.SuccessStrategyStatus, actualStatusStrategy.Status)
require.Equal(t, crdpinnipedv1alpha1.FetchedKeyStrategyReason, actualStatusStrategy.Reason)
require.Equal(t, "Key was fetched successfully", actualStatusStrategy.Message)
} else {
require.Equal(t, crdpinnipedv1alpha1.ErrorStrategyStatus, actualStatusStrategy.Status)
require.Equal(t, crdpinnipedv1alpha1.CouldNotFetchKeyStrategyReason, actualStatusStrategy.Reason)
require.Contains(t, actualStatusStrategy.Message, "some part of the error message")
} }
func TestReconcilingCredentialIssuerConfig(t *testing.T) { require.WithinDuration(t, time.Now(), actualStatusStrategy.LastUpdateTime.Local(), 10*time.Minute)
library.SkipUnlessIntegration(t) })
namespaceName := library.Getenv(t, "PINNIPED_NAMESPACE")
client := library.NewPinnipedClientset(t) t.Run("reconciling CredentialIssuerConfig", func(t *testing.T) {
library.SkipUnlessClusterHasCapability(t, library.ClusterSigningKeyIsAvailable)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) existingConfig, err := client.
defer cancel()
err := client.
CrdV1alpha1().
CredentialIssuerConfigs(namespaceName).
Delete(ctx, "pinniped-config", metav1.DeleteOptions{})
require.NoError(t, err)
config := library.NewClientConfig(t)
expectedLDCStatus := expectedLDCStatus(config)
var actualLDC *crdpinnipedv1alpha1.CredentialIssuerConfig
for i := 0; i < 10; i++ {
actualLDC, err = client.
CrdV1alpha1(). CrdV1alpha1().
CredentialIssuerConfigs(namespaceName). CredentialIssuerConfigs(namespaceName).
Get(ctx, "pinniped-config", metav1.GetOptions{}) Get(ctx, "pinniped-config", metav1.GetOptions{})
if err == nil {
break
}
time.Sleep(time.Millisecond * 750)
}
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, expectedLDCStatus, &actualLDC.Status) require.Len(t, existingConfig.Status.Strategies, 1)
initialStrategy := existingConfig.Status.Strategies[0]
// Mutate the existing object. Don't delete it because that would mess up its `Status.Strategies` array,
// since the reconciling controller is not currently responsible for that field.
existingConfig.Status.KubeConfigInfo.Server = "https://junk"
updatedConfig, err := client.
CrdV1alpha1().
CredentialIssuerConfigs(namespaceName).
Update(ctx, existingConfig, metav1.UpdateOptions{})
require.NoError(t, err)
require.Equal(t, "https://junk", updatedConfig.Status.KubeConfigInfo.Server)
// Expect that the object's mutated field is set back to what matches its source of truth.
var actualCredentialIssuerConfig *crdpinnipedv1alpha1.CredentialIssuerConfig
var getConfig = func() bool {
actualCredentialIssuerConfig, err = client.
CrdV1alpha1().
CredentialIssuerConfigs(namespaceName).
Get(ctx, "pinniped-config", metav1.GetOptions{})
return err == nil
}
assert.Eventually(t, getConfig, 5*time.Second, 100*time.Millisecond)
require.NoError(t, err) // prints out the error and stops the test in case of failure
actualStatusKubeConfigInfo := actualCredentialIssuerConfig.Status.KubeConfigInfo
require.Equal(t, expectedStatusKubeConfigInfo(config), actualStatusKubeConfigInfo)
// The strategies should not have changed during reconciliation.
require.Len(t, actualCredentialIssuerConfig.Status.Strategies, 1)
require.Equal(t, initialStrategy, actualCredentialIssuerConfig.Status.Strategies[0])
})
} }
func expectedLDCStatus(config *rest.Config) *crdpinnipedv1alpha1.CredentialIssuerConfigStatus { func expectedStatusKubeConfigInfo(config *rest.Config) *crdpinnipedv1alpha1.CredentialIssuerConfigKubeConfigInfo {
return &crdpinnipedv1alpha1.CredentialIssuerConfigStatus{ return &crdpinnipedv1alpha1.CredentialIssuerConfigKubeConfigInfo{
Strategies: []crdpinnipedv1alpha1.CredentialIssuerConfigStrategy{},
KubeConfigInfo: &crdpinnipedv1alpha1.CredentialIssuerConfigKubeConfigInfo{
Server: config.Host, Server: config.Host,
CertificateAuthorityData: base64.StdEncoding.EncodeToString(config.TLSClientConfig.CAData), CertificateAuthorityData: base64.StdEncoding.EncodeToString(config.TLSClientConfig.CAData),
},
} }
} }

View File

@ -14,9 +14,8 @@ import (
"time" "time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
v1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1" rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -63,7 +62,8 @@ func addTestClusterRoleBinding(ctx context.Context, t *testing.T, adminClient ku
func TestSuccessfulCredentialRequest(t *testing.T) { func TestSuccessfulCredentialRequest(t *testing.T) {
library.SkipUnlessIntegration(t) library.SkipUnlessIntegration(t)
tmcClusterToken := library.Getenv(t, "PINNIPED_TMC_CLUSTER_TOKEN") library.SkipUnlessClusterHasCapability(t, library.ClusterSigningKeyIsAvailable)
tmcClusterToken := library.GetEnv(t, "PINNIPED_TMC_CLUSTER_TOKEN")
response, err := makeRequest(t, v1alpha1.CredentialRequestSpec{ response, err := makeRequest(t, v1alpha1.CredentialRequestSpec{
Type: v1alpha1.TokenCredentialType, Type: v1alpha1.TokenCredentialType,
@ -121,7 +121,7 @@ func TestSuccessfulCredentialRequest(t *testing.T) {
return err == nil return err == nil
} }
assert.Eventually(t, canListNamespaces, 3*time.Second, 250*time.Millisecond) assert.Eventually(t, canListNamespaces, 3*time.Second, 250*time.Millisecond)
require.NoError(t, err) // prints out the error in case of failure require.NoError(t, err) // prints out the error and stops the test in case of failure
require.NotEmpty(t, listNamespaceResponse.Items) require.NotEmpty(t, listNamespaceResponse.Items)
}) })
@ -150,13 +150,15 @@ func TestSuccessfulCredentialRequest(t *testing.T) {
return err == nil return err == nil
} }
assert.Eventually(t, canListNamespaces, 3*time.Second, 250*time.Millisecond) assert.Eventually(t, canListNamespaces, 3*time.Second, 250*time.Millisecond)
require.NoError(t, err) // prints out the error in case of failure require.NoError(t, err) // prints out the error and stops the test in case of failure
require.NotEmpty(t, listNamespaceResponse.Items) require.NotEmpty(t, listNamespaceResponse.Items)
}) })
} }
func TestFailedCredentialRequestWhenTheRequestIsValidButTheTokenDoesNotAuthenticateTheUser(t *testing.T) { func TestFailedCredentialRequestWhenTheRequestIsValidButTheTokenDoesNotAuthenticateTheUser(t *testing.T) {
library.SkipUnlessIntegration(t) library.SkipUnlessIntegration(t)
library.SkipUnlessClusterHasCapability(t, library.ClusterSigningKeyIsAvailable)
response, err := makeRequest(t, v1alpha1.CredentialRequestSpec{ response, err := makeRequest(t, v1alpha1.CredentialRequestSpec{
Type: v1alpha1.TokenCredentialType, Type: v1alpha1.TokenCredentialType,
Token: &v1alpha1.CredentialRequestTokenCredential{Value: "not a good token"}, Token: &v1alpha1.CredentialRequestTokenCredential{Value: "not a good token"},
@ -171,6 +173,8 @@ func TestFailedCredentialRequestWhenTheRequestIsValidButTheTokenDoesNotAuthentic
func TestCredentialRequest_ShouldFailWhenRequestDoesNotIncludeToken(t *testing.T) { func TestCredentialRequest_ShouldFailWhenRequestDoesNotIncludeToken(t *testing.T) {
library.SkipUnlessIntegration(t) library.SkipUnlessIntegration(t)
library.SkipUnlessClusterHasCapability(t, library.ClusterSigningKeyIsAvailable)
response, err := makeRequest(t, v1alpha1.CredentialRequestSpec{ response, err := makeRequest(t, v1alpha1.CredentialRequestSpec{
Type: v1alpha1.TokenCredentialType, Type: v1alpha1.TokenCredentialType,
Token: nil, Token: nil,

View File

@ -15,6 +15,7 @@ import (
"github.com/suzerain-io/pinniped/test/library" "github.com/suzerain-io/pinniped/test/library"
) )
// Smoke test to see if the kubeconfig works and the cluster is reachable.
func TestGetNodes(t *testing.T) { func TestGetNodes(t *testing.T) {
library.SkipUnlessIntegration(t) library.SkipUnlessIntegration(t)
cmd := exec.Command("kubectl", "get", "nodes") cmd := exec.Command("kubectl", "get", "nodes")

View File

@ -0,0 +1,53 @@
package library
import (
"io/ioutil"
"os"
"testing"
"github.com/ghodss/yaml"
"github.com/stretchr/testify/require"
)
type TestClusterCapability string
const (
ClusterSigningKeyIsAvailable = TestClusterCapability("clusterSigningKeyIsAvailable")
)
type capabilitiesConfig struct {
Capabilities map[TestClusterCapability]bool `yaml:"capabilities,omitempty"`
}
func ClusterHasCapability(t *testing.T, capability TestClusterCapability) bool {
t.Helper()
capabilitiesDescriptionYAML := os.Getenv("PINNIPED_CLUSTER_CAPABILITY_YAML")
capabilitiesDescriptionFile := os.Getenv("PINNIPED_CLUSTER_CAPABILITY_FILE")
require.NotEmptyf(t,
capabilitiesDescriptionYAML+capabilitiesDescriptionFile,
"must specify either PINNIPED_CLUSTER_CAPABILITY_YAML or PINNIPED_CLUSTER_CAPABILITY_FILE env var for integration tests",
)
if capabilitiesDescriptionYAML == "" {
bytes, err := ioutil.ReadFile(capabilitiesDescriptionFile)
capabilitiesDescriptionYAML = string(bytes)
require.NoError(t, err)
}
var capabilities capabilitiesConfig
err := yaml.Unmarshal([]byte(capabilitiesDescriptionYAML), &capabilities)
require.NoError(t, err)
isCapable, capabilityWasDescribed := capabilities.Capabilities[capability]
require.True(t, capabilityWasDescribed, `the cluster's "%s" capability was not described`, capability)
return isCapable
}
func SkipUnlessClusterHasCapability(t *testing.T, capability TestClusterCapability) {
t.Helper()
if !ClusterHasCapability(t, capability) {
t.Skipf(`skipping integration test because cluster lacks the "%s" capability`, capability)
}
}

View File

@ -12,9 +12,9 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
// Getenv gets the environment variable with key and asserts that it is not // GetEnv gets the environment variable with key and asserts that it is not
// empty. It returns the value of the environment variable. // empty. It returns the value of the environment variable.
func Getenv(t *testing.T, key string) string { func GetEnv(t *testing.T, key string) string {
t.Helper() t.Helper()
value := os.Getenv(key) value := os.Getenv(key)
require.NotEmptyf(t, value, "must specify %s env var for integration tests", key) require.NotEmptyf(t, value, "must specify %s env var for integration tests", key)