diff --git a/docs/resources/realm_translation.md b/docs/resources/realm_translation.md new file mode 100644 index 00000000..c251a2a1 --- /dev/null +++ b/docs/resources/realm_translation.md @@ -0,0 +1,38 @@ +--- +page_title: "keycloak_realm_translation Resource" +--- + +# keycloak_realm_tranlsation Resource + +Allows for managing Realm Translations overrides within Keycloak. + +A translation defines a schema for representing a locale with a map of key/value pairs and how they are managed within a realm. + +Note: whilst you can provide translations for unsupported locales, they will not take effect until they are defined within the realm resource. + +## Example Usage + +```hcl +resource "keycloak_realm" "realm" { + realm = "my-realm" +} + +resource "keycloak_realm_translation" "realm_translation" { + realm_id = keycloak_realm.my_realm.id + locale = "de" + translations = { + "Hello" : "Hallo" + } +} +``` + +## Argument Reference + +- `realm_id` - (Required) The ID of the realm the user profile applies to. +- `locale` - (Required) The locale (language code) the translations apply to. +- `translations` - (Optional) A map of translation keys to values. + + +## Import + +This resource does not currently support importing. diff --git a/example/main.tf b/example/main.tf index 2575ab4c..8e75e88c 100644 --- a/example/main.tf +++ b/example/main.tf @@ -103,7 +103,7 @@ resource "keycloak_realm" "test" { resource "keycloak_realm_translation" "test_translation" { realm_id = keycloak_realm.test.id - language = "en" + locale = "en" translations = { "test" : "translation" } diff --git a/keycloak/realm_translation.go b/keycloak/realm_translation.go index f5dbf6c4..f7cff1e9 100644 --- a/keycloak/realm_translation.go +++ b/keycloak/realm_translation.go @@ -8,14 +8,15 @@ import ( "net/http" ) -type RealmLanguageTranslation struct { - Language string `json:"language"` +type RealmLocaleTranslation struct { + Locale string `json:"locale"` Translations map[string]string `json:"translations"` } -func (keycloakClient *KeycloakClient) UpdateRealmTranslations(ctx context.Context, realmId string, language string, translations map[string]string) error { +func (keycloakClient *KeycloakClient) UpdateRealmTranslations(ctx context.Context, realmId string, locale string, translations map[string]string) error { var existingTranslations map[string]string - data, _ := keycloakClient.getRaw(ctx, fmt.Sprintf("/realms/%s/localization/%s", realmId, language), nil) + + data, _ := keycloakClient.getRaw(ctx, fmt.Sprintf("/realms/%s/localization/%s", realmId, locale), nil) err := json.Unmarshal(data, &existingTranslations) if err != nil { return nil @@ -27,13 +28,13 @@ func (keycloakClient *KeycloakClient) UpdateRealmTranslations(ctx context.Contex } } for _, key := range translationsToDelete { - err := keycloakClient.delete(ctx, fmt.Sprintf("/realms/%s/localization/%s/%s", realmId, language, key), nil) + err := keycloakClient.delete(ctx, fmt.Sprintf("/realms/%s/localization/%s/%s", realmId, locale, key), nil) if err != nil { return err } } for key, value := range translations { - err := keycloakClient.putPlain(ctx, fmt.Sprintf("/realms/%s/localization/%s/%s", realmId, language, key), value) + err := keycloakClient.putPlain(ctx, fmt.Sprintf("/realms/%s/localization/%s/%s", realmId, locale, key), value) if err != nil { return err } @@ -52,18 +53,18 @@ func (keycloakClient *KeycloakClient) putPlain(ctx context.Context, path string, return err } -func (keycloakClient *KeycloakClient) GetRealmTranslations(ctx context.Context, realmId string, language string) (*map[string]string, error) { +func (keycloakClient *KeycloakClient) GetRealmTranslations(ctx context.Context, realmId string, locale string) (*map[string]string, error) { keyValues := make(map[string]string) - err := keycloakClient.get(ctx, fmt.Sprintf("/realms/%s/localization/%s", realmId, language), &keyValues, nil) + err := keycloakClient.get(ctx, fmt.Sprintf("/realms/%s/localization/%s", realmId, locale), &keyValues, nil) if err != nil { return nil, err } return &keyValues, nil } -func (keycloakClient *KeycloakClient) DeleteRealmTranslations(ctx context.Context, realmId string, language string, translations map[string]string) error { +func (keycloakClient *KeycloakClient) DeleteRealmTranslations(ctx context.Context, realmId string, locale string, translations map[string]string) error { for key := range translations { - err := keycloakClient.delete(ctx, fmt.Sprintf("/realms/%s/localization/%s/%s", realmId, language, key), nil) + err := keycloakClient.delete(ctx, fmt.Sprintf("/realms/%s/localization/%s/%s", realmId, locale, key), nil) if err != nil { return err } diff --git a/provider/resource_keycloak_realm_translation.go b/provider/resource_keycloak_realm_translation.go index d4eea25d..f875291b 100644 --- a/provider/resource_keycloak_realm_translation.go +++ b/provider/resource_keycloak_realm_translation.go @@ -15,16 +15,19 @@ func resourceKeycloakRealmTranslation() *schema.Resource { ReadContext: resourceKeycloakRealmTranslationRead, DeleteContext: resourceKeycloakRealmTranslationDelete, UpdateContext: resourceKeycloakRealmTranslationUpdate, + Description: "Manage realm-level translations.", Schema: map[string]*schema.Schema{ "realm_id": { - Type: schema.TypeString, - Required: true, - ForceNew: true, + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The realm in which the translation exists.", }, - "language": { - Type: schema.TypeString, - Required: true, - ForceNew: true, + "locale": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The locale for the translations.", }, "translations": { Optional: true, @@ -32,6 +35,7 @@ func resourceKeycloakRealmTranslation() *schema.Resource { Elem: &schema.Schema{ Type: schema.TypeString, }, + Description: "The mapping of translation keys to values.", }, }, } @@ -40,39 +44,39 @@ func resourceKeycloakRealmTranslation() *schema.Resource { func resourceKeycloakRealmTranslationRead(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { keycloakClient := meta.(*keycloak.KeycloakClient) realmId := data.Get("realm_id").(string) - language := data.Get("language").(string) - realmLanguageTranslations, err := keycloakClient.GetRealmTranslations(ctx, realmId, language) + locale := data.Get("locale").(string) + realmLocaleTranslations, err := keycloakClient.GetRealmTranslations(ctx, realmId, locale) if err != nil { return diag.FromErr(err) } - data.Set("translations", realmLanguageTranslations) + data.Set("translations", realmLocaleTranslations) return nil } func resourceKeycloakRealmTranslationUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*keycloak.KeycloakClient) realm := d.Get("realm_id").(string) - language := d.Get("language").(string) + locale := d.Get("locale").(string) translations := d.Get("translations").(map[string]interface{}) translationsConverted := convertTranslations(translations) - err := client.UpdateRealmTranslations(ctx, realm, language, translationsConverted) + err := client.UpdateRealmTranslations(ctx, realm, locale, translationsConverted) if err != nil { return diag.FromErr(err) } - d.SetId(fmt.Sprintf("%s/%s", realm, language)) // Set resource ID as "realm/language" + d.SetId(fmt.Sprintf("%s/%s", realm, locale)) // Set resource ID as "realm/locale" return resourceKeycloakRealmTranslationRead(ctx, d, meta) } func resourceKeycloakRealmTranslationDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*keycloak.KeycloakClient) realm := d.Get("realm_id").(string) - language := d.Get("language").(string) + locale := d.Get("locale").(string) translations := d.Get("translations").(map[string]interface{}) translationsConverted := convertTranslations(translations) - err := client.DeleteRealmTranslations(ctx, realm, language, translationsConverted) + err := client.DeleteRealmTranslations(ctx, realm, locale, translationsConverted) if err != nil { return diag.FromErr(err) } diff --git a/provider/resource_keycloak_realm_translation_test.go b/provider/resource_keycloak_realm_translation_test.go index ed59d098..ff0d8094 100644 --- a/provider/resource_keycloak_realm_translation_test.go +++ b/provider/resource_keycloak_realm_translation_test.go @@ -2,6 +2,7 @@ package provider import ( "fmt" + "reflect" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" @@ -10,6 +11,62 @@ import ( "github.com/keycloak/terraform-provider-keycloak/keycloak" ) +func TestAccKeycloakRealmTranslation_basic(t *testing.T) { + skipIfVersionIsLessThanOrEqualTo(testCtx, t, keycloakClient, keycloak.Version_14) + + realmName := acctest.RandomWithPrefix("tf-acc") + + resource.Test(t, resource.TestCase{ + ProviderFactories: testAccProviderFactories, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckKeycloakRealmTranslationsDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakRealmTranslation_basic(realmName), + Check: testAccCheckKeycloakRealmTranslationsExist("keycloak_realm_translation.realm_translation", "en", map[string]string{"k": "v"}), + }, + }, + }) +} + +func TestAccKeycloakRealmTranslation_empty(t *testing.T) { + skipIfVersionIsLessThanOrEqualTo(testCtx, t, keycloakClient, keycloak.Version_14) + + realmName := acctest.RandomWithPrefix("tf-acc") + + resource.Test(t, resource.TestCase{ + ProviderFactories: testAccProviderFactories, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckKeycloakRealmTranslationsDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakRealmTranslation_empty(realmName), + Check: testAccCheckKeycloakRealmTranslationsExist("keycloak_realm_translation.realm_translation", "en", map[string]string{}), + }, + }, + }) +} + +// Tests creating a realm translation in a realm without localization in a non-default locale +// The translation should exist, but it won't take effect. +func TestAccKeycloakRealmTranslation_noLocalization(t *testing.T) { + skipIfVersionIsLessThanOrEqualTo(testCtx, t, keycloakClient, keycloak.Version_14) + + realmName := acctest.RandomWithPrefix("tf-acc") + + resource.Test(t, resource.TestCase{ + ProviderFactories: testAccProviderFactories, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckKeycloakRealmTranslationsDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakRealmTranslation_noInternationalization(realmName), + Check: testAccCheckKeycloakRealmTranslationsExist("keycloak_realm_translation.realm_translation", "de", map[string]string{"k": "v"}), + }, + }, + }) +} + func testAccCheckKeycloakRealmTranslationsDestroy() resource.TestCheckFunc { return func(s *terraform.State) error { for _, rs := range s.RootModule().Resources { @@ -18,9 +75,9 @@ func testAccCheckKeycloakRealmTranslationsDestroy() resource.TestCheckFunc { } realm := rs.Primary.Attributes["realm_id"] - language := rs.Primary.Attributes["language"] + locale := rs.Primary.Attributes["locale"] - realmTranslation, _ := keycloakClient.GetRealmTranslations(testCtx, realm, language) + realmTranslation, _ := keycloakClient.GetRealmTranslations(testCtx, realm, locale) if realmTranslation != nil { return fmt.Errorf("translation for realm %s", realm) } @@ -30,25 +87,7 @@ func testAccCheckKeycloakRealmTranslationsDestroy() resource.TestCheckFunc { } } -// func TestAccKeycloakRealmTranslations_Create(t *testing.T) { -// skipIfVersionIsGreaterThanOrEqualTo(testCtx, t, keycloakClient, keycloak.Version_24) - -// realmName := acctest.RandomWithPrefix("tf-acc") - -// resource.Test(t, resource.TestCase{ -// ProviderFactories: testAccProviderFactories, -// PreCheck: func() { testAccPreCheck(t) }, -// CheckDestroy: testAccCheckKeycloakRealmTranslationsDestroy(), -// Steps: []resource.TestStep{ -// { -// Config: testKeycloakRealmUserProfile_userProfileEnabledNotSet(realmName), -// ExpectError: regexp.MustCompile("User Profile is disabled"), -// }, -// }, -// }) -// } - -func testKeycloakRealmTranslation_template(realm string) string { +func testKeycloakRealmTranslation_basic(realm string) string { return fmt.Sprintf(` resource "keycloak_realm" "realm" { realm = "%s" @@ -62,7 +101,7 @@ func testKeycloakRealmTranslation_template(realm string) string { resource "keycloak_realm_translation" "realm_translation" { realm_id = keycloak_realm.realm.id - language = "en" + locale = "en" translations = { "k": "v" } @@ -70,50 +109,72 @@ func testKeycloakRealmTranslation_template(realm string) string { `, realm) } -func getRealmTranslationFromState(s *terraform.State, resourceName string) (map[string]string, error) { +func testKeycloakRealmTranslation_empty(realm string) string { + return fmt.Sprintf(` + resource "keycloak_realm" "realm" { + realm = "%s" + internationalization { + supported_locales = [ + "en" + ] + default_locale = "en" + } + } + + resource "keycloak_realm_translation" "realm_translation" { + realm_id = keycloak_realm.realm.id + locale = "en" + translations = { + } + } + `, realm) +} + +func testKeycloakRealmTranslation_noInternationalization(realm string) string { + return fmt.Sprintf(` + resource "keycloak_realm" "realm" { + realm = "%s" + } + + resource "keycloak_realm_translation" "realm_translation" { + realm_id = keycloak_realm.realm.id + locale = "de" + translations = { + "k": "v" + } + } + `, realm) +} + +func getRealmTranslationFromState(s *terraform.State, resourceName string) (map[string]string, string, error) { rs, ok := s.RootModule().Resources[resourceName] if !ok { - return nil, fmt.Errorf("resource not found: %s", resourceName) + return nil, "", fmt.Errorf("resource not found: %s", resourceName) } realm := rs.Primary.Attributes["realm_id"] - language := rs.Primary.Attributes["language"] + locale := rs.Primary.Attributes["locale"] - realmTranslations, err := keycloakClient.GetRealmTranslations(testCtx, realm, language) + realmTranslations, err := keycloakClient.GetRealmTranslations(testCtx, realm, locale) if err != nil { - return nil, fmt.Errorf("error getting realm user profile: %s", err) + return nil, "", fmt.Errorf("error getting realm user profile: %s", err) } - fmt.Println("GETTING REALM TRANSLATION FROM STATE") - fmt.Printf("Translations: %s", realmTranslations) - return *realmTranslations, nil + return *realmTranslations, locale, nil } -func testAccCheckKeycloakRealmTranslationeExists(resourceName string) resource.TestCheckFunc { +func testAccCheckKeycloakRealmTranslationsExist(resourceName string, expectedLocale string, expectedTranslations map[string]string) resource.TestCheckFunc { return func(s *terraform.State) error { - _, err := getRealmTranslationFromState(s, resourceName) + translations, locale, err := getRealmTranslationFromState(s, resourceName) if err != nil { - fmt.Println("Error!!!") return err } + if expectedLocale != locale { + return fmt.Errorf("assigned and expected translations locale do not match %v != %v", locale, expectedLocale) + } + if !reflect.DeepEqual(translations, expectedTranslations) { + return fmt.Errorf("assigned and expected realm translations do not match %v != %v", translations, expectedTranslations) + } return nil } } - -func TestAccKeycloakRealmTranslation_basicEmpty(t *testing.T) { - skipIfVersionIsLessThanOrEqualTo(testCtx, t, keycloakClient, keycloak.Version_14) - - realmName := acctest.RandomWithPrefix("tf-acc") - - resource.Test(t, resource.TestCase{ - ProviderFactories: testAccProviderFactories, - PreCheck: func() { testAccPreCheck(t) }, - CheckDestroy: testAccCheckKeycloakRealmTranslationsDestroy(), - Steps: []resource.TestStep{ - { - Config: testKeycloakRealmTranslation_template(realmName), - Check: testAccCheckKeycloakRealmTranslationeExists("keycloak_realm_translation.realm_translation"), - }, - }, - }) -}