diff --git a/README.md b/README.md index d1e7402..e02fd9a 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/go.mod b/go.mod index d588a6c..97cf78a 100644 --- a/go.mod +++ b/go.mod @@ -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 @@ -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 diff --git a/internal/clients/keycloak.go b/internal/clients/keycloak.go index 21e8d37..c3142af 100644 --- a/internal/clients/keycloak.go +++ b/internal/clients/keycloak.go @@ -9,8 +9,10 @@ import ( "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" @@ -28,6 +30,8 @@ const ( 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 @@ -74,14 +78,10 @@ func TerraformSetupBuilder() terraform.SetupFn { // nolint: gocyclo 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{} @@ -119,3 +119,33 @@ func configureNoForkKeycloakClient(ctx context.Context, ps *terraform.Setup) err ps.Meta = cb.Meta() return nil } + +func ExtractCredentials(ctx context.Context, source xpv1.CredentialsSource, client client.Client, selector xpv1.CommonCredentialSelectors) (map[string]string, error) { + 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 +} diff --git a/internal/clients/keycloak_test.go b/internal/clients/keycloak_test.go new file mode 100644 index 0000000..ed9c0a0 --- /dev/null +++ b/internal/clients/keycloak_test.go @@ -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) + } + }) + } +}