Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support credentials from plain k8s Secret #176

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,26 @@ stringData:

The secret `keycloak-credentials` contains the keycloak API server URL, credentials, and other configuration details that are required to connect to the keycloak API server. **It supports the same fields as the [terraform provider configuration](https://registry.terraform.io/providers/mrparkers/keycloak/latest/docs#argument-reference)**

As an alternative to using the embedded JSON format shown above, you can also place settings in a plain Kubernetes secret like this:

```yaml
apiVersion: v1
kind: Secret
metadata:
name: keycloak-credentials
namespace: crossplane-system
labels:
type: provider-credentials
type: Opaque
stringData:
client_id: "admin-cli"
username: "admin"
password: "admin"
url: "https://keycloak.example.com"
base_path: "/auth"
realm: "master"
```


### Custom Resource Definitions

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ require (
github.com/hashicorp/terraform-plugin-sdk/v2 v2.30.0
github.com/mrparkers/terraform-provider-keycloak v0.0.0-20240108222732-3f6b75b79ada
github.com/pkg/errors v0.9.1
k8s.io/api v0.29.3
k8s.io/apimachinery v0.29.3
k8s.io/client-go v0.29.3
sigs.k8s.io/controller-runtime v0.17.3
Expand Down Expand Up @@ -130,7 +131,6 @@ require (
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.29.3 // indirect
k8s.io/apiextensions-apiserver v0.29.2 // indirect
k8s.io/component-base v0.29.2 // indirect
k8s.io/klog/v2 v2.110.1 // indirect
Expand Down
40 changes: 35 additions & 5 deletions internal/clients/keycloak.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
"encoding/json"
"fmt"

xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
"github.com/crossplane/crossplane-runtime/pkg/resource"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"

Expand All @@ -28,6 +30,8 @@
errTrackUsage = "cannot track ProviderConfig usage"
errExtractCredentials = "cannot extract credentials"
errUnmarshalCredentials = "cannot unmarshal keycloak credentials as JSON"
errExtractSecretKey = "cannot extract from secret key when none specified"
errGetCredentialsSecret = "cannot get credentials secret"
)

// Password and client secret auth parameters + general config parameters
Expand Down Expand Up @@ -74,14 +78,10 @@
return ps, errors.Wrap(err, errTrackUsage)
}

data, err := resource.CommonCredentialExtractor(ctx, pc.Spec.Credentials.Source, client, pc.Spec.Credentials.CommonCredentialSelectors)
creds, err := ExtractCredentials(ctx, pc.Spec.Credentials.Source, client, pc.Spec.Credentials.CommonCredentialSelectors)
if err != nil {
return ps, errors.Wrap(err, errExtractCredentials)
}
creds := map[string]string{}
if err := json.Unmarshal(data, &creds); err != nil {
return ps, errors.Wrap(err, errUnmarshalCredentials)
}

// set provider configuration
ps.Configuration = map[string]any{}
Expand Down Expand Up @@ -119,3 +119,33 @@
ps.Meta = cb.Meta()
return nil
}

func ExtractCredentials(ctx context.Context, source xpv1.CredentialsSource, client client.Client, selector xpv1.CommonCredentialSelectors) (map[string]string, error) {

Check failure on line 123 in internal/clients/keycloak.go

View workflow job for this annotation

GitHub Actions / lint

exported function `ExtractCredentials` should have comment or be unexported (golint)
creds := make(map[string]string)

// first try to see if the secret contains a proper key-value map
if selector.SecretRef == nil {
return nil, errors.New(errExtractSecretKey)
}
secret := &corev1.Secret{}
if err := client.Get(ctx, types.NamespacedName{Namespace: selector.SecretRef.Namespace, Name: selector.SecretRef.Name}, secret); err != nil {
return nil, errors.Wrap(err, errGetCredentialsSecret)
}
if _, ok := secret.Data[selector.SecretRef.Key]; !ok {
for k, v := range secret.Data {
creds[k] = string(v)
}
return creds, nil
}

// if that fails, use Crossplane's way of extracting a JSON document
rawData, err := resource.CommonCredentialExtractor(ctx, source, client, selector)
if err != nil {
return nil, err
}
if err := json.Unmarshal(rawData, &creds); err != nil {
return nil, errors.Wrap(err, errUnmarshalCredentials)
}

return creds, nil
}
116 changes: 116 additions & 0 deletions internal/clients/keycloak_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package clients

import (
"context"
"reflect"
"testing"

v1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)

func TestExtractCredentials(t *testing.T) {
type args struct {
ctx context.Context
source v1.CredentialsSource
client client.Client
selector v1.CommonCredentialSelectors
}
tests := []struct {
name string
args args
want map[string]string
wantErr bool
}{
{
name: "extracting credentials from JSON blob secret works",
args: args{
ctx: context.Background(),
source: v1.CredentialsSourceSecret,
client: fake.NewClientBuilder().
WithObjects(&corev1.Secret{
TypeMeta: metav1.TypeMeta{},
ObjectMeta: metav1.ObjectMeta{
Name: "provider-keycloak-config",
Namespace: "crossplane-system",
},
Data: map[string][]byte{
"someCredentialsField": []byte(`{
"client_id": "test-client",
"username": "tester",
"password": "53cr37",
"url": "my-keycloak.nmspc.svc.cluster.local"
}`)},
}).
Build(),
selector: v1.CommonCredentialSelectors{
SecretRef: &v1.SecretKeySelector{
Key: "someCredentialsField",
SecretReference: v1.SecretReference{
Name: "provider-keycloak-config",
Namespace: "crossplane-system",
},
},
},
},
want: map[string]string{
"client_id": "test-client",
"username": "tester",
"password": "53cr37",
"url": "my-keycloak.nmspc.svc.cluster.local",
},
},
{
name: "extracting credentials from plain k8s secret works",
args: args{
ctx: context.Background(),
source: v1.CredentialsSourceSecret,
client: fake.NewClientBuilder().
WithObjects(&corev1.Secret{
TypeMeta: metav1.TypeMeta{},
ObjectMeta: metav1.ObjectMeta{
Name: "provider-keycloak-config-plain",
Namespace: "crossplane-system",
},
Data: map[string][]byte{
"client_id": []byte("test-client"),
"username": []byte("tester"),
"password": []byte("53cr37"),
"url": []byte("my-keycloak.nmspc.svc.cluster.local"),
},
}).
Build(),
selector: v1.CommonCredentialSelectors{
SecretRef: &v1.SecretKeySelector{
Key: "someCredentialsField",
SecretReference: v1.SecretReference{
Name: "provider-keycloak-config-plain",
Namespace: "crossplane-system",
},
},
},
},
want: map[string]string{
"client_id": "test-client",
"username": "tester",
"password": "53cr37",
"url": "my-keycloak.nmspc.svc.cluster.local",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ExtractCredentials(tt.args.ctx, tt.args.source, tt.args.client, tt.args.selector)
if (err != nil) != tt.wantErr {
t.Errorf("ExtractCredentials() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("ExtractCredentials() got = %v, want %v", got, tt.want)
}
})
}
}
Loading