1
0
Fork 0
mirror of https://github.com/external-secrets/external-secrets.git synced 2024-12-15 17:51:01 +00:00

Merge remote-tracking branch 'origin/main' into oracle-provider

This commit is contained in:
Kian 2021-08-18 14:41:30 +01:00
commit b030aed0a2
33 changed files with 1161 additions and 126 deletions

View file

@ -30,7 +30,7 @@ jobs:
steps:
- name: Detect No-op Changes
id: noop
uses: fkirc/skip-duplicate-actions@v3.4.0
uses: fkirc/skip-duplicate-actions@v3.4.1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
paths_ignore: '["**.md", "**.png", "**.jpg"]'
@ -176,7 +176,7 @@ jobs:
make test
- name: Publish Unit Test Coverage
uses: codecov/codecov-action@v1
uses: codecov/codecov-action@v2.0.2
with:
flags: unittests
file: ./cover.out

View file

@ -90,8 +90,8 @@ jobs:
runs-on: ubuntu-latest
if:
github.event_name == 'repository_dispatch' &&
github.event.client_payload.slash_command.sha != '' &&
contains(github.event.client_payload.pull_request.head.sha, github.event.client_payload.slash_command.sha)
github.event.client_payload.slash_command.args.named.sha != '' &&
contains(github.event.client_payload.pull_request.head.sha, github.event.client_payload.slash_command.args.named.sha)
steps:
# Check out merge commit

View file

@ -23,7 +23,7 @@ jobs:
private_key: ${{ secrets.PRIVATE_KEY }}
- name: Slash Command Dispatch
uses: peter-evans/slash-command-dispatch@v1
uses: peter-evans/slash-command-dispatch@v2.2.1
env:
TOKEN: ${{ steps.generate_token.outputs.token }}
with:

View file

@ -151,7 +151,8 @@ type ExternalSecretSpec struct {
type ExternalSecretConditionType string
const (
ExternalSecretReady ExternalSecretConditionType = "Ready"
ExternalSecretReady ExternalSecretConditionType = "Ready"
ExternalSecretDeleted ExternalSecretConditionType = "Deleted"
)
type ExternalSecretStatusCondition struct {
@ -173,6 +174,8 @@ const (
ConditionReasonSecretSynced = "SecretSynced"
// ConditionReasonSecretSyncedError indicates that there was an error syncing the secret.
ConditionReasonSecretSyncedError = "SecretSyncedError"
// ConditionReasonSecretDeleted indicates that the secret has been deleted.
ConditionReasonSecretDeleted = "SecretDeleted"
)
type ExternalSecretStatus struct {
@ -195,6 +198,7 @@ type ExternalSecretStatus struct {
// +kubebuilder:resource:scope=Namespaced,categories={externalsecrets},shortName=es
// +kubebuilder:printcolumn:name="Store",type=string,JSONPath=`.spec.secretStoreRef.name`
// +kubebuilder:printcolumn:name="Refresh Interval",type=string,JSONPath=`.spec.refreshInterval`
// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].reason`
type ExternalSecret struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

View file

@ -2,8 +2,8 @@ apiVersion: v2
name: external-secrets
description: External secret management for Kubernetes
type: application
version: "0.3.2"
appVersion: "v0.3.2"
version: "0.3.3"
appVersion: "v0.3.3"
kubeVersion: ">= 1.11.0-0"
keywords:
- kubernetes-external-secrets

View file

@ -4,7 +4,7 @@
[//]: # (README.md generated by gotmpl. DO NOT EDIT.)
![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![Version: 0.3.2](https://img.shields.io/badge/Version-0.3.2-informational?style=flat-square)
![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![Version: 0.3.3](https://img.shields.io/badge/Version-0.3.3-informational?style=flat-square)
External secret management for Kubernetes

View file

@ -25,6 +25,9 @@ spec:
- jsonPath: .spec.refreshInterval
name: Refresh Interval
type: string
- jsonPath: .status.conditions[?(@.type=="Ready")].reason
name: Status
type: string
name: v1alpha1
schema:
openAPIV3Schema:

View file

@ -31,7 +31,7 @@ You have created a key. Press the eyeball to show the key. Copy or save it becau
Create a secret containing your apiKey:
```shell
kubectl create secret generic ibm-secret --from-literal=apiKey='API_KEY_VALUE'
kubectl create secret generic ibm-secret --from-literal=apiKey='API_KEY_VALUE'
```
### Update secret store
@ -41,19 +41,39 @@ Be sure the `ibm` provider is listed in the `Kind=SecretStore`
{% include 'ibm-secret-store.yaml' %}
```
To find your serviceURL, under your Secrets Manager resource, go to "Endpoints" on the left:
To find your serviceURL, under your Secrets Manager resource, go to "Endpoints" on the left.
Note: Use the url without the `/api` suffix that is presented in the UI.
See here for a list of [publicly available endpoints](https://cloud.ibm.com/apidocs/secrets-manager#getting-started-endpoints).
![iam-create-success](./pictures/screenshot_service_url.png)
### Creating the secret inside the provider
### Secret Types
We support all secret types of [IBM Secrets Manager](https://cloud.ibm.com/apidocs/secrets-manager): `arbitrary`, `username_password`, `iam_credentials` and `imported_cert`. To define the type of secret you would like to sync you need to prefix the secret id with the desired type. If the secret type is not specified it is defaulted to `arbitrary`:
For now we only support secrets of type arbitrary. So you need to go to your Secrets Manager UI and, click 'Add Secret', and then choose 'Other Secret Type'. You can now enter your value as text or as a file. This will be the value synchronized with the secret directly.
```yaml
{% include 'ibm-es-types.yaml' %}
### Other types of secret
```
The behavior for the different secret types is as following:
#### arbitrary
* `remoteRef` retrieves a string from secrets manager and sets it for specified `secretKey`
* `dataFrom` retrieves a string from secrets manager and tries to parse it as JSON object setting the key:values pairs in resulting Kubernetes secret if successful
#### username_password
* `remoteRef` requires a `property` to be set for either `username` or `password` to retrieve respective fields from the secrets manager secret and set in specified `secretKey`
* `dataFrom` retrieves both `username` and `password` fields from the secrets manager secret and sets appropriate key:value pairs in the resulting Kubernetes secret
#### iam_credentials
* `remoteRef` retrieves an apikey from secrets manager and sets it for specified `secretKey`
* `dataFrom` retrieves an apikey from secrets manager and sets it for the `apikey` Kubernetes secret key
#### imported_cert
* `remoteRef` requires a `property` to be set for either `certificate`, `private_key` or `intermediate` to retrieve respective fields from the secrets manager secret and set in specified `secretKey`
* `dataFrom` retrieves all `certificate`, `private_key` and `intermediate` fields from the secrets manager secret and sets appropriate key:value pairs in the resulting Kubernetes secret
!!! note "Not implemented"
This is currently not yet implemented. See [#242](https://github.com/external-secrets/external-secrets/issues/242) for details. Feel free to contribute.
### Creating external secret

View file

@ -0,0 +1,20 @@
apiVersion: external-secrets.io/v1alpha1
kind: ExternalSecret
metadata:
name: ibm-sample
spec:
# [...]
data:
- secretKey: test
remoteRef:
# defaults to type=arbitrary
key: xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
- secretKey: foo
remoteRef:
key: username_password/yyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy
- secretKey: bar
remoteRef:
key: iam_credentials/zzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz
- secretKey: baz
remoteRef:
key: imported_cert/zzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz

View file

@ -5,9 +5,9 @@ metadata:
spec:
provider:
ibm:
serviceUrl: "https://SECRETS_MANAGER_ID.REGION.secrets-manager.appdomain.cloud"
auth:
secretRef:
secretApiKeySecretRef:
name: ibm-secret
key: apiKey
serviceUrl: "https://SECRETS_MANAGER_ID.REGION.secrets-manager.appdomain.cloud"

View file

@ -19,7 +19,7 @@ Resource Types:
<p>
<p>AWSAuth tells the controller how to do authentication with aws.
Only one of secretRef or jwt can be specified.
if none is specified the controller will load credentials using the aws sdk defaults</p>
if none is specified the controller will load credentials using the aws sdk defaults.</p>
</p>
<table>
<thead>
@ -106,7 +106,7 @@ github.com/external-secrets/external-secrets/apis/meta/v1.SecretKeySelector
<a href="#external-secrets.io/v1alpha1.AWSAuth">AWSAuth</a>)
</p>
<p>
<p>Authenticate against AWS using service account tokens</p>
<p>Authenticate against AWS using service account tokens.</p>
</p>
<table>
<thead>
@ -1843,7 +1843,7 @@ resource is used as the app role secret.</p>
</p>
<p>
<p>VaultAuth is the configuration used to authenticate with a Vault server.
Only one of <code>tokenSecretRef</code>, <code>appRole</code>, <code>kubernetes</code>, <code>ldap</code> or <code>jwt</code>
Only one of <code>tokenSecretRef</code>, <code>appRole</code>, <code>kubernetes</code>, <code>ldap</code>, <code>jwt</code> or <code>cert</code>
can be specified.</p>
</p>
<table>
@ -1926,6 +1926,66 @@ VaultJwtAuth
JWT/OIDC authentication method</p>
</td>
</tr>
<tr>
<td>
<code>cert</code></br>
<em>
<a href="#external-secrets.io/v1alpha1.VaultCertAuth">
VaultCertAuth
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>Cert authenticates with TLS Certificates by passing client certificate, private key and ca certificate
Cert authentication method</p>
</td>
</tr>
</tbody>
</table>
<h3 id="external-secrets.io/v1alpha1.VaultCertAuth">VaultCertAuth
</h3>
<p>
(<em>Appears on:</em>
<a href="#external-secrets.io/v1alpha1.VaultAuth">VaultAuth</a>)
</p>
<p>
<p>VaultJwtAuth authenticates with Vault using the JWT/OIDC authentication
method, with the role name and token stored in a Kubernetes Secret resource.</p>
</p>
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>clientCert</code></br>
<em>
github.com/external-secrets/external-secrets/apis/meta/v1.SecretKeySelector
</em>
</td>
<td>
<em>(Optional)</em>
<p>ClientCert is a certificate to authenticate using the Cert Vault
authentication method</p>
</td>
</tr>
<tr>
<td>
<code>secretRef</code></br>
<em>
github.com/external-secrets/external-secrets/apis/meta/v1.SecretKeySelector
</em>
</td>
<td>
<p>SecretRef to a key in a Secret resource containing client private key to
authenticate with Vault using the Cert authentication method</p>
</td>
</tr>
</tbody>
</table>
<h3 id="external-secrets.io/v1alpha1.VaultJwtAuth">VaultJwtAuth

View file

@ -4,7 +4,7 @@ SHELL := /bin/bash
IMG_TAG = test
IMG = local/external-secrets-e2e:$(IMG_TAG)
K8S_VERSION = "1.20.7"
KIND_IMG = "kindest/node:v1.20.7@sha256:cbeaf907fc78ac97ce7b625e4bf0de16e3ea725daf6b04f930bd14c67c671ff9"
BUILD_ARGS ?=
export FOCUS := $(FOCUS)
@ -13,7 +13,7 @@ start-kind: ## Start kind cluster
--name external-secrets \
--config kind.yaml \
--retain \
--image "kindest/node:v$(K8S_VERSION)"
--image "$(KIND_IMG)"
test: e2e-image ## Run e2e tests against current kube context
$(MAKE) -C ../ docker.build \

View file

@ -38,9 +38,6 @@ var _ = SynchronizedBeforeSuite(func() []byte {
err := util.WaitForURL("http://localstack.default/health")
Expect(err).ToNot(HaveOccurred())
By("installing vault")
addon.InstallGlobalAddon(addon.NewVault(), cfg)
By("installing eso")
addon.InstallGlobalAddon(addon.NewESO(), cfg)
return nil

View file

@ -13,9 +13,9 @@ limitations under the License.
package addon
import (
"bytes"
"context"
"fmt"
"os"
"os/exec"
corev1 "k8s.io/api/core/v1"
@ -79,22 +79,28 @@ func (c *HelmChart) Install() error {
args = append(args, "--set", fmt.Sprintf("%s=%s", s.Key, s.Value))
}
var sout, serr bytes.Buffer
log.Logf("installing chart %s", c.ReleaseName)
cmd := exec.Command("helm", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
cmd.Stdout = &sout
cmd.Stderr = &serr
err = cmd.Run()
if err != nil {
return fmt.Errorf("unable to run cmd: %w: %s %s", err, sout.String(), serr.String())
}
return nil
}
// Uninstall removes the chart aswell as the repo.
func (c *HelmChart) Uninstall() error {
var sout, serr bytes.Buffer
args := []string{"delete", "--namespace", c.Namespace, c.ReleaseName}
cmd := exec.Command("helm", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdout = &sout
cmd.Stderr = &serr
err := cmd.Run()
if err != nil {
return err
return fmt.Errorf("unable to delete helm release: %w: %s, %s", err, sout.String(), serr.String())
}
return c.removeRepo()
}
@ -103,23 +109,32 @@ func (c *HelmChart) addRepo() error {
if c.Repo.Name == "" || c.Repo.URL == "" {
return nil
}
log.Logf("adding repo %s", c.Repo.Name)
var sout, serr bytes.Buffer
args := []string{"repo", "add", c.Repo.Name, c.Repo.URL}
cmd := exec.Command("helm", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
cmd.Stdout = &sout
cmd.Stderr = &serr
err := cmd.Run()
if err != nil {
return fmt.Errorf("unable to add helm repo: %w: %s, %s", err, sout.String(), serr.String())
}
return nil
}
func (c *HelmChart) removeRepo() error {
if c.Repo.Name == "" || c.Repo.URL == "" {
return nil
}
var sout, serr bytes.Buffer
args := []string{"repo", "remove", c.Repo.Name}
cmd := exec.Command("helm", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
cmd.Stdout = &sout
cmd.Stderr = &serr
err := cmd.Run()
if err != nil {
return fmt.Errorf("unable to remove repo: %w: %s, %s", err, sout.String(), serr.String())
}
return nil
}
// Logs fetches the logs from all pods managed by this release

View file

@ -21,7 +21,7 @@ func NewESO() *ESO {
return &ESO{
&HelmChart{
Namespace: "default",
ReleaseName: "eso-aws-sm",
ReleaseName: "eso",
Chart: "/k8s/deploy/charts/external-secrets",
Values: []string{"/k8s/eso.values.yaml"},
},

View file

@ -13,41 +13,414 @@ limitations under the License.
*/
package addon
import "github.com/external-secrets/external-secrets/e2e/framework/util"
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/json"
"encoding/pem"
"fmt"
"math/big"
"net"
"net/http"
"os"
"time"
"github.com/golang-jwt/jwt"
vault "github.com/hashicorp/vault/api"
// nolint
. "github.com/onsi/ginkgo"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/external-secrets/external-secrets/e2e/framework/util"
)
type Vault struct {
Addon
chart *HelmChart
Namespace string
PodName string
VaultClient *vault.Client
VaultURL string
RootToken string
VaultServerCA []byte
ServerCert []byte
ServerKey []byte
VaultClientCA []byte
ClientCert []byte
ClientKey []byte
JWTPubkey []byte
JWTPrivKey []byte
JWTToken string
JWTRole string
KubernetesAuthPath string
KubernetesAuthRole string
AppRoleSecret string
AppRoleID string
AppRolePath string
}
func NewVault() *Vault {
func NewVault(namespace string) *Vault {
repo := "hashicorp-" + namespace
return &Vault{
&HelmChart{
Namespace: "default",
ReleaseName: "vault",
Chart: "hashicorp/vault",
chart: &HelmChart{
Namespace: namespace,
ReleaseName: fmt.Sprintf("vault-%s", namespace), // avoid cluster role collision
Chart: fmt.Sprintf("%s/vault", repo),
ChartVersion: "0.11.0",
Repo: ChartRepo{
Name: "hashicorp",
Name: repo,
URL: "https://helm.releases.hashicorp.com",
},
Vars: []StringTuple{
{
Key: "server.dev.enabled",
Value: "true",
},
{
Key: "injector.enabled",
Value: "false",
},
},
Values: []string{"/k8s/vault.values.yaml"},
},
Namespace: namespace,
}
}
type OperatorInitResponse struct {
UnsealKeysB64 []string `json:"unseal_keys_b64"`
RootToken string `json:"root_token"`
}
func (l *Vault) Install() error {
err := l.Addon.Install()
By("Installing vault in " + l.Namespace)
err := l.chart.Install()
if err != nil {
return err
}
return util.WaitForURL("http://vault.default:8200/ui/")
err = l.initVault()
if err != nil {
return err
}
err = l.configureVault()
if err != nil {
return err
}
return nil
}
func (l *Vault) initVault() error {
sec := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "vault-tls-config",
Namespace: l.Namespace,
},
Data: map[string][]byte{},
}
// vault-config contains vault init config and policies
files, err := os.ReadDir("/k8s/vault-config")
if err != nil {
return err
}
for _, f := range files {
name := f.Name()
data := mustReadFile(fmt.Sprintf("/k8s/vault-config/%s", name))
sec.Data[name] = data
}
// gen certificates and put them into the secret
serverRootPem, serverPem, serverKeyPem, clientRootPem, clientPem, clientKeyPem, err := genVaultCertificates(l.Namespace)
if err != nil {
return fmt.Errorf("unable to gen vault certs: %w", err)
}
jwtPrivkey, jwtPubkey, jwtToken, err := genVaultJWTKeys()
if err != nil {
return fmt.Errorf("unable to generate vault jwt keys: %w", err)
}
// pass certs to secret
sec.Data["vault-server-ca.pem"] = serverRootPem
sec.Data["server-cert.pem"] = serverPem
sec.Data["server-cert-key.pem"] = serverKeyPem
sec.Data["vault-client-ca.pem"] = clientRootPem
sec.Data["es-client.pem"] = clientPem
sec.Data["es-client-key.pem"] = clientKeyPem
sec.Data["jwt-pubkey.pem"] = jwtPubkey
// make certs available to the struct
// so it can be used by the provider
l.VaultServerCA = serverRootPem
l.ServerCert = serverPem
l.ServerKey = serverKeyPem
l.VaultClientCA = clientRootPem
l.ClientCert = clientPem
l.ClientKey = clientKeyPem
l.JWTPrivKey = jwtPrivkey
l.JWTPubkey = jwtPubkey
l.JWTToken = jwtToken
l.JWTRole = "external-secrets-operator" // see configure-vault.sh
l.KubernetesAuthPath = "mykubernetes" // see configure-vault.sh
l.KubernetesAuthRole = "external-secrets-operator" // see configure-vault.sh
By("Creating vault TLS secret")
err = l.chart.config.CRClient.Create(context.Background(), sec)
if err != nil {
return err
}
By("Waiting for vault pods to be running")
pl, err := util.WaitForPodsRunning(l.chart.config.KubeClientSet, 1, l.Namespace, metav1.ListOptions{
LabelSelector: "app.kubernetes.io/name=vault",
})
if err != nil {
return fmt.Errorf("error waiting for vault to be running: %w", err)
}
l.PodName = pl.Items[0].Name
By("Initializing vault")
out, err := util.ExecCmd(
l.chart.config.KubeClientSet,
l.chart.config.KubeConfig,
l.PodName, l.Namespace, "vault operator init --format=json")
if err != nil {
return fmt.Errorf("error initializing vault: %w", err)
}
By("Parsing init response")
var res OperatorInitResponse
err = json.Unmarshal([]byte(out), &res)
if err != nil {
return err
}
l.RootToken = res.RootToken
By("Unsealing vault")
for _, k := range res.UnsealKeysB64 {
_, err = util.ExecCmd(
l.chart.config.KubeClientSet,
l.chart.config.KubeConfig,
l.PodName, l.Namespace, "vault operator unseal "+k)
if err != nil {
return fmt.Errorf("unable to unseal vault: %w", err)
}
}
// vault becomes ready after it has been unsealed
err = util.WaitForPodsReady(l.chart.config.KubeClientSet, 1, l.Namespace, metav1.ListOptions{
LabelSelector: "app.kubernetes.io/name=vault",
})
if err != nil {
return fmt.Errorf("error waiting for vault to be ready: %w", err)
}
serverCA := l.VaultServerCA
caCertPool := x509.NewCertPool()
ok := caCertPool.AppendCertsFromPEM(serverCA)
if !ok {
panic("unable to append server ca cert")
}
cfg := vault.DefaultConfig()
l.VaultURL = fmt.Sprintf("https://vault-%s.%s.svc.cluster.local:8200", l.Namespace, l.Namespace)
cfg.Address = l.VaultURL
cfg.HttpClient.Transport.(*http.Transport).TLSClientConfig.RootCAs = caCertPool
l.VaultClient, err = vault.NewClient(cfg)
if err != nil {
return fmt.Errorf("unable to create vault client: %w", err)
}
l.VaultClient.SetToken(l.RootToken)
return nil
}
func (l *Vault) configureVault() error {
By("configuring vault")
cmd := `sh /etc/vault-config/configure-vault.sh %s`
_, err := util.ExecCmd(
l.chart.config.KubeClientSet,
l.chart.config.KubeConfig,
l.PodName, l.Namespace, fmt.Sprintf(cmd, l.RootToken))
if err != nil {
return fmt.Errorf("unable to configure vault: %w", err)
}
// configure appRole
l.AppRolePath = "myapprole"
req := l.VaultClient.NewRequest(http.MethodGet, fmt.Sprintf("/v1/auth/%s/role/eso-e2e-role/role-id", l.AppRolePath))
res, err := l.VaultClient.RawRequest(req)
if err != nil {
return err
}
defer res.Body.Close()
sec, err := vault.ParseSecret(res.Body)
if err != nil {
return err
}
l.AppRoleID = sec.Data["role_id"].(string)
// parse role id
req = l.VaultClient.NewRequest(http.MethodPost, fmt.Sprintf("/v1/auth/%s/role/eso-e2e-role/secret-id", l.AppRolePath))
res, err = l.VaultClient.RawRequest(req)
if err != nil {
return err
}
defer res.Body.Close()
sec, err = vault.ParseSecret(res.Body)
if err != nil {
return err
}
l.AppRoleSecret = sec.Data["secret_id"].(string)
return nil
}
func (l *Vault) Logs() error {
return l.chart.Logs()
}
func (l *Vault) Uninstall() error {
return l.chart.Uninstall()
}
func (l *Vault) Setup(cfg *Config) error {
return l.chart.Setup(cfg)
}
func genVaultCertificates(namespace string) ([]byte, []byte, []byte, []byte, []byte, []byte, error) {
// gen server ca + certs
serverRootCert, serverRootPem, serverRootKey, err := genCARoot()
if err != nil {
return nil, nil, nil, nil, nil, nil, fmt.Errorf("unable to generate ca cert: %w", err)
}
serverPem, serverKey, err := genPeerCert(serverRootCert, serverRootKey, "vault", []string{
"localhost",
"vault-" + namespace,
fmt.Sprintf("vault-%s.%s.svc.cluster.local", namespace, namespace)})
if err != nil {
return nil, nil, nil, nil, nil, nil, fmt.Errorf("unable to generate vault server cert")
}
serverKeyPem := pem.EncodeToMemory(&pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(serverKey)},
)
// gen client ca + certs
clientRootCert, clientRootPem, clientRootKey, err := genCARoot()
if err != nil {
return nil, nil, nil, nil, nil, nil, fmt.Errorf("unable to generate ca cert: %w", err)
}
clientPem, clientKey, err := genPeerCert(clientRootCert, clientRootKey, "vault-client", nil)
if err != nil {
return nil, nil, nil, nil, nil, nil, fmt.Errorf("unable to generate vault server cert")
}
clientKeyPem := pem.EncodeToMemory(&pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(clientKey)},
)
return serverRootPem, serverPem, serverKeyPem, clientRootPem, clientPem, clientKeyPem, err
}
func genVaultJWTKeys() ([]byte, []byte, string, error) {
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, nil, "", err
}
privPem := pem.EncodeToMemory(&pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(key),
})
pk, err := x509.MarshalPKIXPublicKey(&key.PublicKey)
if err != nil {
return nil, nil, "", err
}
pubPem := pem.EncodeToMemory(&pem.Block{
Type: "RSA PUBLIC KEY",
Bytes: pk,
})
token := jwt.NewWithClaims(jwt.SigningMethodPS256, jwt.MapClaims{
"aud": "vault.client",
"sub": "vault@example",
"iss": "example.iss",
"user": "eso",
"exp": time.Now().Add(time.Hour).Unix(),
"nbf": time.Date(2015, 10, 10, 12, 0, 0, 0, time.UTC).Unix(),
})
// Sign and get the complete encoded token as a string using the secret
tokenString, err := token.SignedString(key)
if err != nil {
return nil, nil, "", err
}
return privPem, pubPem, tokenString, nil
}
func genCARoot() (*x509.Certificate, []byte, *rsa.PrivateKey, error) {
tpl := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
Country: []string{"/dev/null"},
Organization: []string{"External Secrets ACME"},
CommonName: "External Secrets Vault CA",
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour),
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
BasicConstraintsValid: true,
IsCA: true,
MaxPathLen: 2,
}
pkey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, nil, nil, err
}
rootCert, rootPEM, err := genCert(&tpl, &tpl, &pkey.PublicKey, pkey)
return rootCert, rootPEM, pkey, err
}
func genCert(template, parent *x509.Certificate, publicKey *rsa.PublicKey, privateKey *rsa.PrivateKey) (*x509.Certificate, []byte, error) {
certBytes, err := x509.CreateCertificate(rand.Reader, template, parent, publicKey, privateKey)
if err != nil {
return nil, nil, fmt.Errorf("failed to create certificate: %w", err)
}
cert, err := x509.ParseCertificate(certBytes)
if err != nil {
return nil, nil, fmt.Errorf("failed to parse certificate: %w", err)
}
b := pem.Block{Type: "CERTIFICATE", Bytes: certBytes}
certPEM := pem.EncodeToMemory(&b)
return cert, certPEM, err
}
func genPeerCert(signingCert *x509.Certificate, signingKey *rsa.PrivateKey, cn string, dnsNames []string) ([]byte, *rsa.PrivateKey, error) {
pkey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, nil, err
}
tpl := x509.Certificate{
Subject: pkix.Name{
Country: []string{"/dev/null"},
Organization: []string{"External Secrets ACME"},
CommonName: cn,
},
SerialNumber: big.NewInt(1),
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour),
KeyUsage: x509.KeyUsageCRLSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
IsCA: false,
MaxPathLenZero: true,
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
DNSNames: dnsNames,
}
_, serverPEM, err := genCert(&tpl, signingCert, &pkey.PublicKey, signingKey)
return serverPEM, pkey, err
}
func mustReadFile(path string) []byte {
b, err := os.ReadFile(path)
if err != nil {
panic(err)
}
return b
}

View file

@ -18,6 +18,7 @@ import (
// nolint
. "github.com/onsi/ginkgo"
// nolint
. "github.com/onsi/gomega"
api "k8s.io/api/core/v1"
@ -83,11 +84,32 @@ func (f *Framework) BeforeEach() {
// AfterEach deletes the namespace and cleans up the registered addons.
func (f *Framework) AfterEach() {
for _, a := range f.Addons {
err := a.Uninstall()
Expect(err).ToNot(HaveOccurred())
}
// reset addons to default once the run is done
f.Addons = []addon.Addon{}
By("deleting test namespace")
err := util.DeleteKubeNamespace(f.Namespace.Name, f.KubeClientSet)
Expect(err).NotTo(HaveOccurred())
}
func (f *Framework) Install(a addon.Addon) {
f.Addons = append(f.Addons, a)
By("installing addon")
err := a.Setup(&addon.Config{
KubeConfig: f.KubeConfig,
KubeClientSet: f.KubeClientSet,
CRClient: f.CRClient,
})
Expect(err).NotTo(HaveOccurred())
err = a.Install()
Expect(err).NotTo(HaveOccurred())
}
// NewConfig loads and returns the kubernetes credentials from the environment.
// KUBECONFIG env var takes precedence and falls back to in-cluster config.
func NewConfig() (*rest.Config, *kubernetes.Clientset, crclient.Client) {

View file

@ -42,14 +42,16 @@ type SecretStoreProvider interface {
}
// TableFunc returns the main func that runs a TestCase in a table driven test.
func TableFunc(f *Framework, prov SecretStoreProvider) func(func(*TestCase)) {
return func(customize func(*TestCase)) {
func TableFunc(f *Framework, prov SecretStoreProvider) func(...func(*TestCase)) {
return func(tweaks ...func(*TestCase)) {
var err error
// make default test case
// and apply customization to it
tc := makeDefaultTestCase(f)
customize(tc)
for _, tweak := range tweaks {
tweak(tc)
}
// create secrets & defer delete
for k, v := range tc.Secrets {

View file

@ -14,6 +14,7 @@ limitations under the License.
package util
import (
"bytes"
"context"
"fmt"
"net/http"
@ -24,6 +25,9 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/scheme"
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/tools/remotecommand"
)
const (
@ -66,6 +70,123 @@ func namespaceNotExist(c kubernetes.Interface, namespace string) wait.ConditionF
}
}
// ExecCmd exec command on specific pod and wait the command's output.
func ExecCmd(client kubernetes.Interface, config *restclient.Config, podName, namespace string,
command string) (string, error) {
cmd := []string{
"sh",
"-c",
command,
}
req := client.CoreV1().RESTClient().Post().Resource("pods").Name(podName).
Namespace(namespace).SubResource("exec")
option := &v1.PodExecOptions{
Command: cmd,
Stdin: false,
Stdout: true,
Stderr: true,
TTY: false,
}
req.VersionedParams(
option,
scheme.ParameterCodec,
)
exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL())
if err != nil {
return "", err
}
var stdout, stderr bytes.Buffer
err = exec.Stream(remotecommand.StreamOptions{
Stdout: &stdout,
Stderr: &stderr,
Tty: false,
})
if err != nil {
return "", fmt.Errorf("unable to exec stream: %w: \n%s\n%s", err, stdout.String(), stderr.String())
}
return stdout.String() + stderr.String(), nil
}
// WaitForPodsRunning waits for a given amount of time until a group of Pods is running in the given namespace.
func WaitForPodsRunning(kubeClientSet kubernetes.Interface, expectedReplicas int, namespace string, opts metav1.ListOptions) (*v1.PodList, error) {
var pods *v1.PodList
err := wait.PollImmediate(1*time.Second, time.Minute*5, func() (bool, error) {
pl, err := kubeClientSet.CoreV1().Pods(namespace).List(context.TODO(), opts)
if err != nil {
return false, nil
}
r := 0
for i := range pl.Items {
if pl.Items[i].Status.Phase == v1.PodRunning {
r++
}
}
if r == expectedReplicas {
pods = pl
return true, nil
}
return false, nil
})
return pods, err
}
// WaitForPodsReady waits for a given amount of time until a group of Pods is running in the given namespace.
func WaitForPodsReady(kubeClientSet kubernetes.Interface, expectedReplicas int, namespace string, opts metav1.ListOptions) error {
return wait.PollImmediate(1*time.Second, time.Minute*5, func() (bool, error) {
pl, err := kubeClientSet.CoreV1().Pods(namespace).List(context.TODO(), opts)
if err != nil {
return false, nil
}
r := 0
for i := range pl.Items {
if isRunning, _ := podRunningReady(&pl.Items[i]); isRunning {
r++
}
}
if r == expectedReplicas {
return true, nil
}
return false, nil
})
}
// podRunningReady checks whether pod p's phase is running and it has a ready
// condition of status true.
func podRunningReady(p *v1.Pod) (bool, error) {
// Check the phase is running.
if p.Status.Phase != v1.PodRunning {
return false, fmt.Errorf("want pod '%s' on '%s' to be '%v' but was '%v'",
p.ObjectMeta.Name, p.Spec.NodeName, v1.PodRunning, p.Status.Phase)
}
// Check the ready condition is true.
if !isPodReady(p) {
return false, fmt.Errorf("pod '%s' on '%s' didn't have condition {%v %v}; conditions: %v",
p.ObjectMeta.Name, p.Spec.NodeName, v1.PodReady, v1.ConditionTrue, p.Status.Conditions)
}
return true, nil
}
func isPodReady(p *v1.Pod) bool {
for _, condition := range p.Status.Conditions {
if condition.Type != v1.ContainersReady {
continue
}
return condition.Status == v1.ConditionTrue
}
return false
}
// WaitForURL tests the provided url. Once a http 200 is returned the func returns with no error.
// Timeout is 5min.
func WaitForURL(url string) error {

View file

@ -0,0 +1,85 @@
#!/bin/sh
set -euxo pipefail;
export VAULT_TOKEN=${1}
# ------------------
# SECRET BACKENDS
# ------------------
vault secrets enable -path=secret -version=2 kv
vault secrets enable -path=secret_v1 -version=1 kv
# ------------------
# CERT AUTH
# https://www.vaultproject.io/docs/auth/cert
# ------------------
vault auth enable cert
vault policy write \
external-secrets-operator \
/etc/vault-config/vault-policy-es.hcl
vault write auth/cert/certs/external-secrets-operator \
display_name=external-secrets-operator \
policies=external-secrets-operator \
certificate=@/etc/vault-config/es-client.pem \
ttl=3600
# test certificate login
unset VAULT_TOKEN
vault login \
-client-cert=/etc/vault-config/es-client.pem \
-client-key=/etc/vault-config/es-client-key.pem \
-method=cert \
name=external-secrets-operator
vault kv put secret/foo/bar baz=bang
vault kv get secret/foo/bar
# ------------------
# App Role AUTH
# https://www.vaultproject.io/docs/auth/approle
# ------------------
export VAULT_TOKEN=${1}
vault auth enable -path=myapprole approle
vault write auth/myapprole/role/eso-e2e-role \
secret_id_ttl=10m \
token_num_uses=10 \
token_policies=external-secrets-operator \
token_ttl=1h \
token_max_ttl=4h \
secret_id_num_uses=40
# ------------------
# App Role AUTH
# https://www.vaultproject.io/docs/auth/jwt
# ------------------
vault auth enable jwt
vault write auth/jwt/config \
jwt_validation_pubkeys=@/etc/vault-config/jwt-pubkey.pem \
bound_issuer="example.iss" \
default_role="external-secrets-operator"
vault write auth/jwt/role/external-secrets-operator \
role_type="jwt" \
bound_subject="vault@example" \
bound_audiences="vault.client" \
user_claim="user" \
policies=external-secrets-operator \
ttl=1h
# ------------------
# Kubernetes AUTH
# https://www.vaultproject.io/docs/auth/kubernetes
# ------------------
vault auth enable -path=mykubernetes kubernetes
vault write auth/mykubernetes/config \
kubernetes_host=https://kubernetes.default.svc.cluster.local \
kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
vault write auth/mykubernetes/role/external-secrets-operator \
bound_service_account_names=* \
bound_service_account_namespaces=* \
policies=external-secrets-operator \
ttl=1h

View file

@ -0,0 +1,7 @@
path "secret/+/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
path "secret_v1/+/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}

27
e2e/k8s/vault.values.yaml Normal file
View file

@ -0,0 +1,27 @@
injector:
enabled: false
server:
extraEnvironmentVars:
VAULT_CACERT: /etc/vault-config/vault-server-ca.pem
VAULT_ADDR: https://127.0.0.1:8200
volumeMounts:
- name: tls-config
mountPath: /etc/vault-config
readOnly: true
volumes:
- name: tls-config
secret:
secretName: vault-tls-config
standalone:
config: |
ui = true
listener "tcp" {
address = "[::]:8200"
cluster_address = "[::]:8201"
tls_cert_file = "/etc/vault-config/server-cert.pem"
tls_key_file = "/etc/vault-config/server-cert-key.pem"
tls_client_ca_file = "/etc/vault-config/vault-client-ca.pem"
}
storage "file" {
path = "/vault/data"
}

View file

@ -9,15 +9,6 @@ kubeadmConfigPatches:
service-account-key-file: "/etc/kubernetes/pki/sa.pub"
service-account-signing-key-file: "/etc/kubernetes/pki/sa.key"
service-account-issuer: "https://s3-XXXXXXXXXX.amazonaws.com/XXXXXXXXXXXXXXXXXXXXX"
- |
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
metadata:
name: config
# this is only relevant for btrfs uses
# https://github.com/kubernetes/kubernetes/issues/80633#issuecomment-550994513
featureGates:
LocalStorageCapacityIsolation: false
nodes:
- role: control-plane
- role: worker

View file

@ -305,7 +305,7 @@ func DataPropertyDockerconfigJSON(f *framework.Framework) (string, func(*framewo
}
}
// This case adds an ssh private key secret and sybcs it.
// This case adds an ssh private key secret and synchronizes it.
// Not supported by: vault. Json parsing error.
func SSHKeySync(f *framework.Framework) (string, func(*framework.TestCase)) {
return "[common] should sync ssh key secret", func(tc *framework.TestCase) {
@ -379,7 +379,6 @@ func SSHKeySync(f *framework.Framework) (string, func(*framework.TestCase)) {
}
// This case adds an ssh private key secret and syncs it.
// Supported by vault. But does not work with any form of line breaks as standard ssh key.
func SSHKeySyncDataProperty(f *framework.Framework) (string, func(*framework.TestCase)) {
return "[common] should sync ssh key with provider.", func(tc *framework.TestCase) {
cloudSecretName := fmt.Sprintf("%s-%s", f.Namespace.Name, "docker-config-example")

View file

@ -31,81 +31,239 @@ import (
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
"github.com/external-secrets/external-secrets/e2e/framework"
"github.com/external-secrets/external-secrets/e2e/framework/addon"
)
type vaultProvider struct {
url string
token string
client *vault.Client
framework *framework.Framework
}
func newVaultProvider(f *framework.Framework, url, token string) *vaultProvider {
vc, err := vault.NewClient(&vault.Config{
Address: url,
})
Expect(err).ToNot(HaveOccurred())
vc.SetToken(token)
const (
certAuthProviderName = "cert-auth-provider"
appRoleAuthProviderName = "app-role-provider"
kvv1ProviderName = "kv-v1-provider"
jwtProviderName = "jwt-provider"
kubernetesProviderName = "kubernetes-provider"
)
func newVaultProvider(f *framework.Framework) *vaultProvider {
prov := &vaultProvider{
framework: f,
url: url,
token: token,
client: vc,
}
BeforeEach(prov.BeforeEach)
return prov
}
// CreateSecret creates a secret in both kv v1 and v2 provider.
func (s *vaultProvider) CreateSecret(key, val string) {
req := s.client.NewRequest(http.MethodPost, fmt.Sprintf("/v1/secret/data/%s", key))
req.BodyBytes = []byte(fmt.Sprintf(`{"data": %s}`, val))
_, err := s.client.RawRequestWithContext(context.Background(), req)
Expect(err).ToNot(HaveOccurred())
req = s.client.NewRequest(http.MethodPost, fmt.Sprintf("/v1/secret_v1/%s", key))
req.BodyBytes = []byte(val)
_, err = s.client.RawRequestWithContext(context.Background(), req)
Expect(err).ToNot(HaveOccurred())
}
func (s *vaultProvider) DeleteSecret(key string) {
req := s.client.NewRequest(http.MethodDelete, fmt.Sprintf("/v1/secret/data/%s", key))
_, err := s.client.RawRequestWithContext(context.Background(), req)
Expect(err).ToNot(HaveOccurred())
req = s.client.NewRequest(http.MethodDelete, fmt.Sprintf("/v1/secret_v1/%s", key))
_, err = s.client.RawRequestWithContext(context.Background(), req)
Expect(err).ToNot(HaveOccurred())
}
func (s *vaultProvider) BeforeEach() {
ns := s.framework.Namespace.Name
v := addon.NewVault(ns)
s.framework.Install(v)
s.client = v.VaultClient
s.url = v.VaultURL
s.CreateCertStore(v, ns)
s.CreateTokenStore(v, ns)
s.CreateAppRoleStore(v, ns)
s.CreateV1Store(v, ns)
s.CreateJWTStore(v, ns)
s.CreateKubernetesAuthStore(v, ns)
}
func makeStore(name, ns string, v *addon.Vault) *esv1alpha1.SecretStore {
return &esv1alpha1.SecretStore{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: ns,
},
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
Vault: &esv1alpha1.VaultProvider{
Version: esv1alpha1.VaultKVStoreV2,
Path: "secret",
Server: v.VaultURL,
CABundle: v.VaultServerCA,
},
},
},
}
}
func (s *vaultProvider) CreateCertStore(v *addon.Vault, ns string) {
By("creating a vault secret")
clientCert := v.ClientCert
clientKey := v.ClientKey
vaultCreds := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "provider-secret",
Namespace: s.framework.Namespace.Name,
Name: certAuthProviderName,
Namespace: ns,
},
StringData: map[string]string{
"token": s.token, // vault dev-mode default token
Data: map[string][]byte{
"token": []byte(v.RootToken),
"client_cert": clientCert,
"client_key": clientKey,
},
}
err := s.framework.CRClient.Create(context.Background(), vaultCreds)
Expect(err).ToNot(HaveOccurred())
By("creating an secret store for vault")
secretStore := &esv1alpha1.SecretStore{
ObjectMeta: metav1.ObjectMeta{
Name: s.framework.Namespace.Name,
Namespace: s.framework.Namespace.Name,
},
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
Vault: &esv1alpha1.VaultProvider{
Version: esv1alpha1.VaultKVStoreV2,
Path: "secret",
Server: s.url,
Auth: esv1alpha1.VaultAuth{
TokenSecretRef: &esmeta.SecretKeySelector{
Name: "provider-secret",
Key: "token",
},
},
},
secretStore := makeStore(certAuthProviderName, ns, v)
secretStore.Spec.Provider.Vault.Auth = esv1alpha1.VaultAuth{
Cert: &esv1alpha1.VaultCertAuth{
ClientCert: esmeta.SecretKeySelector{
Name: certAuthProviderName,
Key: "client_cert",
},
SecretRef: esmeta.SecretKeySelector{
Name: certAuthProviderName,
Key: "client_key",
},
},
}
err = s.framework.CRClient.Create(context.Background(), secretStore)
Expect(err).ToNot(HaveOccurred())
}
func (s vaultProvider) CreateTokenStore(v *addon.Vault, ns string) {
vaultCreds := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "token-provider",
Namespace: ns,
},
Data: map[string][]byte{
"token": []byte(v.RootToken),
},
}
err := s.framework.CRClient.Create(context.Background(), vaultCreds)
Expect(err).ToNot(HaveOccurred())
secretStore := makeStore(s.framework.Namespace.Name, ns, v)
secretStore.Spec.Provider.Vault.Auth = esv1alpha1.VaultAuth{
TokenSecretRef: &esmeta.SecretKeySelector{
Name: "token-provider",
Key: "token",
},
}
err = s.framework.CRClient.Create(context.Background(), secretStore)
Expect(err).ToNot(HaveOccurred())
}
func (s vaultProvider) CreateAppRoleStore(v *addon.Vault, ns string) {
By("creating a vault secret")
vaultCreds := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: appRoleAuthProviderName,
Namespace: ns,
},
Data: map[string][]byte{
"approle_secret": []byte(v.AppRoleSecret),
},
}
err := s.framework.CRClient.Create(context.Background(), vaultCreds)
Expect(err).ToNot(HaveOccurred())
By("creating an secret store for vault")
secretStore := makeStore(appRoleAuthProviderName, ns, v)
secretStore.Spec.Provider.Vault.Auth = esv1alpha1.VaultAuth{
AppRole: &esv1alpha1.VaultAppRole{
Path: v.AppRolePath,
RoleID: v.AppRoleID,
SecretRef: esmeta.SecretKeySelector{
Name: appRoleAuthProviderName,
Key: "approle_secret",
},
},
}
err = s.framework.CRClient.Create(context.Background(), secretStore)
Expect(err).ToNot(HaveOccurred())
}
func (s vaultProvider) CreateV1Store(v *addon.Vault, ns string) {
vaultCreds := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "v1-provider",
Namespace: ns,
},
Data: map[string][]byte{
"token": []byte(v.RootToken),
},
}
err := s.framework.CRClient.Create(context.Background(), vaultCreds)
Expect(err).ToNot(HaveOccurred())
secretStore := makeStore(kvv1ProviderName, ns, v)
secretStore.Spec.Provider.Vault.Version = esv1alpha1.VaultKVStoreV1
secretStore.Spec.Provider.Vault.Path = "secret_v1"
secretStore.Spec.Provider.Vault.Auth = esv1alpha1.VaultAuth{
TokenSecretRef: &esmeta.SecretKeySelector{
Name: "v1-provider",
Key: "token",
},
}
err = s.framework.CRClient.Create(context.Background(), secretStore)
Expect(err).ToNot(HaveOccurred())
}
func (s vaultProvider) CreateJWTStore(v *addon.Vault, ns string) {
vaultCreds := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "jwt-provider",
Namespace: ns,
},
Data: map[string][]byte{
"jwt": []byte(v.JWTToken),
},
}
err := s.framework.CRClient.Create(context.Background(), vaultCreds)
Expect(err).ToNot(HaveOccurred())
secretStore := makeStore(jwtProviderName, ns, v)
secretStore.Spec.Provider.Vault.Auth = esv1alpha1.VaultAuth{
Jwt: &esv1alpha1.VaultJwtAuth{
Role: v.JWTRole,
SecretRef: esmeta.SecretKeySelector{
Name: "jwt-provider",
Key: "jwt",
},
},
}
err = s.framework.CRClient.Create(context.Background(), secretStore)
Expect(err).ToNot(HaveOccurred())
}
func (s vaultProvider) CreateKubernetesAuthStore(v *addon.Vault, ns string) {
secretStore := makeStore(kubernetesProviderName, ns, v)
secretStore.Spec.Provider.Vault.Auth = esv1alpha1.VaultAuth{
Kubernetes: &esv1alpha1.VaultKubernetesAuth{
Path: v.KubernetesAuthPath,
Role: v.KubernetesAuthRole,
ServiceAccountRef: &esmeta.ServiceAccountSelector{
Name: "default",
},
},
}
err := s.framework.CRClient.Create(context.Background(), secretStore)
Expect(err).ToNot(HaveOccurred())
}

View file

@ -28,10 +28,74 @@ var _ = Describe("[vault] ", func() {
DescribeTable("sync secrets",
framework.TableFunc(f,
newVaultProvider(f, "http://vault.default:8200", "root")),
Entry(common.JSONDataFromSync(f)),
Entry(common.JSONDataWithProperty(f)),
Entry(common.JSONDataWithTemplate(f)),
Entry(common.DataPropertyDockerconfigJSON(f)),
newVaultProvider(f)),
// uses token auth
compose("with token auth", f, common.JSONDataFromSync, useTokenAuth),
compose("with token auth", f, common.JSONDataWithProperty, useTokenAuth),
compose("with token auth", f, common.JSONDataWithTemplate, useTokenAuth),
compose("with token auth", f, common.DataPropertyDockerconfigJSON, useTokenAuth),
// use cert auth
compose("with cert auth", f, common.JSONDataFromSync, useCertAuth),
compose("with cert auth", f, common.JSONDataWithProperty, useCertAuth),
compose("with cert auth", f, common.JSONDataWithTemplate, useCertAuth),
compose("with cert auth", f, common.DataPropertyDockerconfigJSON, useCertAuth),
// use approle auth
compose("with appRole auth", f, common.JSONDataFromSync, useApproleAuth),
compose("with appRole auth", f, common.JSONDataWithProperty, useApproleAuth),
compose("with appRole auth", f, common.JSONDataWithTemplate, useApproleAuth),
compose("with appRole auth", f, common.DataPropertyDockerconfigJSON, useApproleAuth),
// use v1 provider
compose("with v1 kv provider", f, common.JSONDataFromSync, useV1Provider),
compose("with v1 kv provider", f, common.JSONDataWithProperty, useV1Provider),
compose("with v1 kv provider", f, common.JSONDataWithTemplate, useV1Provider),
compose("with v1 kv provider", f, common.DataPropertyDockerconfigJSON, useV1Provider),
// use jwt provider
compose("with jwt provider", f, common.JSONDataFromSync, useJWTProvider),
compose("with jwt provider", f, common.JSONDataWithProperty, useJWTProvider),
compose("with jwt provider", f, common.JSONDataWithTemplate, useJWTProvider),
compose("with jwt provider", f, common.DataPropertyDockerconfigJSON, useJWTProvider),
// use kubernetes provider
compose("with kubernetes provider", f, common.JSONDataFromSync, useKubernetesProvider),
compose("with kubernetes provider", f, common.JSONDataWithProperty, useKubernetesProvider),
compose("with kubernetes provider", f, common.JSONDataWithTemplate, useKubernetesProvider),
compose("with kubernetes provider", f, common.DataPropertyDockerconfigJSON, useKubernetesProvider),
)
})
func compose(descAppend string, f *framework.Framework, fn func(f *framework.Framework) (string, func(*framework.TestCase)), tweaks ...func(*framework.TestCase)) TableEntry {
desc, tfn := fn(f)
tweaks = append(tweaks, tfn)
te := Entry(desc + " " + descAppend)
// need to convert []func to []interface{}
ifs := make([]interface{}, len(tweaks))
for i := 0; i < len(tweaks); i++ {
ifs[i] = tweaks[i]
}
te.Parameters = ifs
return te
}
func useTokenAuth(tc *framework.TestCase) {
tc.ExternalSecret.Spec.SecretStoreRef.Name = tc.Framework.Namespace.Name
}
func useCertAuth(tc *framework.TestCase) {
tc.ExternalSecret.Spec.SecretStoreRef.Name = certAuthProviderName
}
func useApproleAuth(tc *framework.TestCase) {
tc.ExternalSecret.Spec.SecretStoreRef.Name = appRoleAuthProviderName
}
func useV1Provider(tc *framework.TestCase) {
tc.ExternalSecret.Spec.SecretStoreRef.Name = kvv1ProviderName
}
func useJWTProvider(tc *framework.TestCase) {
tc.ExternalSecret.Spec.SecretStoreRef.Name = jwtProviderName
}
func useKubernetesProvider(tc *framework.TestCase) {
tc.ExternalSecret.Spec.SecretStoreRef.Name = kubernetesProviderName
}

1
go.mod
View file

@ -44,6 +44,7 @@ require (
github.com/fatih/color v1.10.0 // indirect
github.com/frankban/quicktest v1.10.0 // indirect
github.com/go-logr/logr v0.4.0
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/google/go-cmp v0.5.5
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.2.0

4
go.sum
View file

@ -143,6 +143,7 @@ github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc=
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
@ -241,6 +242,8 @@ github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@ -486,6 +489,7 @@ github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR
github.com/mitchellh/mapstructure v1.3.3 h1:SzB1nHZ2Xi+17FP0zVQBHIZqvwRN9408fJO8h+eeNA8=
github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8=
github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=

View file

@ -17,6 +17,7 @@ package main
import (
"flag"
"os"
"time"
"go.uber.org/zap/zapcore"
"k8s.io/apimachinery/pkg/runtime"
@ -88,6 +89,7 @@ func main() {
Log: ctrl.Log.WithName("controllers").WithName("ExternalSecret"),
Scheme: mgr.GetScheme(),
ControllerClass: controllerClass,
RequeueInterval: time.Hour,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "ExternalSecret")
os.Exit(1)

View file

@ -74,6 +74,7 @@ type Reconciler struct {
Log logr.Logger
Scheme *runtime.Scheme
ControllerClass string
RequeueInterval time.Duration
}
// Reconcile implements the main reconciliation loop
@ -89,6 +90,13 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
err := r.Get(ctx, req.NamespacedName, &externalSecret)
if apierrors.IsNotFound(err) {
syncCallsTotal.With(syncCallsMetricLabels).Inc()
conditionSynced := NewExternalSecretCondition(esv1alpha1.ExternalSecretDeleted, v1.ConditionFalse, esv1alpha1.ConditionReasonSecretDeleted, "Secret was deleted")
SetExternalSecretCondition(&esv1alpha1.ExternalSecret{
ObjectMeta: metav1.ObjectMeta{
Name: req.Name,
Namespace: req.Namespace,
},
}, *conditionSynced)
return ctrl.Result{}, nil
} else if err != nil {
log.Error(err, errGetES)
@ -145,7 +153,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
}
}()
refreshInt := time.Hour
refreshInt := r.RequeueInterval
if externalSecret.Spec.RefreshInterval != nil {
refreshInt = externalSecret.Spec.RefreshInterval.Duration
}
@ -306,7 +314,7 @@ func shouldRefresh(es esv1alpha1.ExternalSecret) bool {
return true
}
// skip refresh if refresh interval is 0
if es.Spec.RefreshInterval == nil && es.Status.SyncedResourceVersion != "" {
if es.Spec.RefreshInterval.Duration == 0 && es.Status.SyncedResourceVersion != "" {
return false
}
if es.Status.RefreshTime.IsZero() {

View file

@ -488,6 +488,41 @@ var _ = Describe("ExternalSecret controller", func() {
}
}
refreshintervalZero := func(tc *testCase) {
const targetProp = "targetProperty"
const secretVal = "someValue"
fakeProvider.WithGetSecret([]byte(secretVal), nil)
tc.externalSecret.Spec.RefreshInterval = &metav1.Duration{Duration: 0}
tc.checkSecret = func(es *esv1alpha1.ExternalSecret, secret *v1.Secret) {
Expect(externalSecretConditionShouldBe(ExternalSecretName, ExternalSecretNamespace, esv1alpha1.ExternalSecretReady, v1.ConditionFalse, 0.0)).To(BeTrue())
Expect(externalSecretConditionShouldBe(ExternalSecretName, ExternalSecretNamespace, esv1alpha1.ExternalSecretReady, v1.ConditionTrue, 1.0)).To(BeTrue())
Eventually(func() bool {
Expect(syncCallsTotal.WithLabelValues(ExternalSecretName, ExternalSecretNamespace).Write(&metric)).To(Succeed())
return metric.GetCounter().GetValue() == 1.0
}, timeout, interval).Should(BeTrue())
// check values
Expect(string(secret.Data[targetProp])).To(Equal(secretVal))
// update provider secret
newValue := "NEW VALUE"
sec := &v1.Secret{}
fakeProvider.WithGetSecret([]byte(newValue), nil)
secretLookupKey := types.NamespacedName{
Name: ExternalSecretTargetSecretName,
Namespace: ExternalSecretNamespace,
}
Consistently(func() bool {
err := k8sClient.Get(context.Background(), secretLookupKey, sec)
if err != nil {
return false
}
v := sec.Data[targetProp]
return string(v) == secretVal
}, time.Second*10, time.Second).Should(BeTrue())
}
}
// with dataFrom all properties from the specified secret
// should be put into the secret
syncWithDataFrom := func(tc *testCase) {
@ -669,6 +704,7 @@ var _ = Describe("ExternalSecret controller", func() {
Entry("should sync template with correct value precedence", syncWithTemplatePrecedence),
Entry("should refresh secret from template", refreshWithTemplate),
Entry("should refresh secret value when provider secret changes", refreshSecretValue),
Entry("should not refresh secret value when provider secret changes but refreshInterval is zero", refreshintervalZero),
Entry("should fetch secret using dataFrom", syncWithDataFrom),
Entry("should set error condition when provider errors", providerErrCondition),
Entry("should set an error condition when store does not exist", storeMissingErrCondition),
@ -695,7 +731,12 @@ var _ = Describe("ExternalSecret refresh logic", func() {
"foo": "bar",
},
},
Status: esv1alpha1.ExternalSecretStatus{},
Spec: esv1alpha1.ExternalSecretSpec{
RefreshInterval: &metav1.Duration{Duration: time.Minute},
},
Status: esv1alpha1.ExternalSecretStatus{
RefreshTime: metav1.Now(),
},
}
es.Status.SyncedResourceVersion = getResourceVersion(es)
// this should not refresh, rv matches object
@ -714,7 +755,12 @@ var _ = Describe("ExternalSecret refresh logic", func() {
"foo": "bar",
},
},
Status: esv1alpha1.ExternalSecretStatus{},
Spec: esv1alpha1.ExternalSecretSpec{
RefreshInterval: &metav1.Duration{Duration: time.Minute},
},
Status: esv1alpha1.ExternalSecretStatus{
RefreshTime: metav1.Now(),
},
}
es.Status.SyncedResourceVersion = getResourceVersion(es)
// this should not refresh, rv matches object
@ -730,7 +776,12 @@ var _ = Describe("ExternalSecret refresh logic", func() {
ObjectMeta: metav1.ObjectMeta{
Generation: 1,
},
Status: esv1alpha1.ExternalSecretStatus{},
Spec: esv1alpha1.ExternalSecretSpec{
RefreshInterval: &metav1.Duration{Duration: 0},
},
Status: esv1alpha1.ExternalSecretStatus{
RefreshTime: metav1.Now(),
},
}
es.Status.SyncedResourceVersion = getResourceVersion(es)
Expect(shouldRefresh(es)).To(BeFalse())
@ -740,13 +791,13 @@ var _ = Describe("ExternalSecret refresh logic", func() {
Expect(shouldRefresh(es)).To(BeTrue())
})
It("should skip refresh when refreshInterval is nil", func() {
It("should skip refresh when refreshInterval is 0", func() {
es := esv1alpha1.ExternalSecret{
ObjectMeta: metav1.ObjectMeta{
Generation: 1,
},
Spec: esv1alpha1.ExternalSecretSpec{
RefreshInterval: nil,
RefreshInterval: &metav1.Duration{Duration: 0},
},
Status: esv1alpha1.ExternalSecretStatus{},
}

View file

@ -17,6 +17,7 @@ package externalsecret
import (
"path/filepath"
"testing"
"time"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
@ -74,9 +75,10 @@ var _ = BeforeSuite(func() {
Expect(err).ToNot(HaveOccurred())
err = (&Reconciler{
Client: k8sClient,
Scheme: k8sManager.GetScheme(),
Log: ctrl.Log.WithName("controllers").WithName("ExternalSecrets"),
Client: k8sClient,
Scheme: k8sManager.GetScheme(),
Log: ctrl.Log.WithName("controllers").WithName("ExternalSecrets"),
RequeueInterval: time.Second,
}).SetupWithManager(k8sManager)
Expect(err).ToNot(HaveOccurred())

View file

@ -51,9 +51,9 @@ const (
errVaultCert = "cannot set Vault CA certificate: %w"
errReadSecret = "cannot read secret data from Vault: %w"
errAuthFormat = "cannot initialize Vault client: no valid auth method specified: %w"
errDataField = "failed to find data field: %v"
errJSONUnmarshall = "failed to unmarshall JSON: %v"
errSecretFormat = "secret data not in expected format: %v"
errDataField = "failed to find data field"
errJSONUnmarshall = "failed to unmarshall JSON"
errSecretFormat = "secret data not in expected format"
errVaultToken = "cannot parse Vault authentication token: %w"
errVaultReqParams = "cannot set Vault request parameters: %w"
errVaultRequest = "error from Vault request: %w"
@ -118,7 +118,6 @@ func (c *connector) NewClient(ctx context.Context, store esv1alpha1.GenericStore
}
cfg, err := vStore.newConfig()
if err != nil {
return nil, err
}