Skip to content
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
34 changes: 34 additions & 0 deletions docs/resources/airflow_user_roles.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
layout: "airflow"
page_title: "Airflow: airflow_user_roles"
sidebar_current: "docs-airflow-resource-user-roles"
description: |-
Provides an Airflow user roles
---

# airflow_user roles

Provides an Airflow user roles management.

## Example Usage

```hcl
resource "airflow_user_roles" "example" {
username = "example"
roles = [airflow_role.example.name]
}
```

## Argument Reference

The following arguments are supported:
* `username` - (Required) The username
* `roles` - (Required) A set of User roles to attach to the User.

## Import

User's roles can be imported using the username.

```terraform
terraform import airflow_user_roles.example example
```
1 change: 1 addition & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ func AirflowProvider() *schema.Provider {
"airflow_pool": resourcePool(),
"airflow_role": resourceRole(),
"airflow_user": resourceUser(),
"airflow_user_roles": resourceUserRoles(),
},
// ConfigureContextFunc: providerConfigure,
}
Expand Down
125 changes: 125 additions & 0 deletions internal/provider/resource_user_roles.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package provider

import (
"context"

"github.com/apache/airflow-client-go/airflow"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

func resourceUserRoles() *schema.Resource {
return &schema.Resource{
CreateWithoutTimeout: resourceUserRolesCreate,
ReadWithoutTimeout: resourceUserRolesRead,
UpdateWithoutTimeout: resourceUserRolesUpdate,
DeleteWithoutTimeout: resourceUserRolesDelete,
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},
Schema: map[string]*schema.Schema{
"roles": {
Type: schema.TypeSet,
Required: true,
MinItems: 1,
Elem: &schema.Schema{Type: schema.TypeString},
},
"username": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
},
}
}

func resourceUserRolesCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
pcfg := m.(ProviderConfig)
client := pcfg.ApiClient

username := d.Get("username").(string)
roles := expandAirflowUserRoles(d.Get("roles").(*schema.Set))

userApi := client.UserApi

_, _, err := userApi.PatchUser(pcfg.AuthContext, username).UpdateMask([]string{"roles"}).User(airflow.User{
Roles: &roles,
Username: &username,
FirstName: &username,
LastName: &username,
Email: &username,
}).Execute()
if err != nil {
return diag.Errorf("failed to create user `%s` from Airflow: %s", username, err)
}
d.SetId(username)

return resourceUserRolesRead(ctx, d, m)
}

func resourceUserRolesRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
pcfg := m.(ProviderConfig)
client := pcfg.ApiClient

user, resp, err := client.UserApi.GetUser(pcfg.AuthContext, d.Id()).Execute()
if resp != nil && resp.StatusCode == 404 {
d.SetId("")
return nil
}
if err != nil {
return diag.Errorf("failed to get user `%s` from Airflow: %s", d.Id(), err)
}

d.Set("username", user.Username)
d.Set("roles", flattenAirflowUserRoles(*user.Roles))

return nil
}

func resourceUserRolesUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
pcfg := m.(ProviderConfig)
client := pcfg.ApiClient

roles := expandAirflowUserRoles(d.Get("roles").(*schema.Set))
username := d.Id()

_, _, err := client.UserApi.PatchUser(pcfg.AuthContext, username).UpdateMask([]string{"roles"}).User(airflow.User{
Roles: &roles,
Username: &username,
FirstName: &username,
LastName: &username,
Email: &username,
}).Execute()
if err != nil {
return diag.Errorf("failed to update user `%s` from Airflow: %s", username, err)
}

return resourceUserRolesRead(ctx, d, m)
}

func resourceUserRolesDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
pcfg := m.(ProviderConfig)
client := pcfg.ApiClient

roles := make([]airflow.UserCollectionItemRoles, 0)
username := d.Id()

_, _, err := client.UserApi.PatchUser(pcfg.AuthContext, username).UpdateMask([]string{"roles"}).User(airflow.User{
Roles: &roles,
Username: &username,
FirstName: &username,
LastName: &username,
Email: &username,
}).Execute()

resp, err := client.UserApi.DeleteUser(pcfg.AuthContext, d.Id()).Execute()
if err != nil {
return diag.Errorf("failed to delete user `%s` from Airflow: %s", d.Id(), err)
}

if resp != nil && resp.StatusCode == 404 {
return nil
}

return nil
}
177 changes: 177 additions & 0 deletions internal/provider/resource_user_roles_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package provider

import (
"context"
"fmt"
"net/http"
"net/url"
"os"
"strings"
"testing"

"github.com/apache/airflow-client-go/airflow"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/logging"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
)

const (
accName = "tf-acc-test-user-roles"
)

func TestAccAirflowUserRoles_basic(t *testing.T) {
rName := acctest.RandomWithPrefix("tf-role-test")
r2Name := acctest.RandomWithPrefix("tf-role-test2")

resourceName := "airflow_user_roles.test"
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheckCreateUser(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAirflowUserRolesCheckDestroy,
Steps: []resource.TestStep{
{
Config: testAccAirflowUserRolesConfigBasic(accName, rName),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "username", accName),
resource.TestCheckResourceAttr(resourceName, "roles.#", "1"),
resource.TestCheckTypeSetElemAttrPair(resourceName, "roles.*", "airflow_role.test", "name"),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
{
Config: testAccAirflowUserAddRolesConfigBasic(accName, rName, r2Name),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "username", accName),
resource.TestCheckResourceAttr(resourceName, "roles.#", "2"),
resource.TestCheckTypeSetElemAttrPair(resourceName, "roles.*", "airflow_role.test", "name"),
resource.TestCheckTypeSetElemAttrPair(resourceName, "roles.*", "airflow_role.test2", "name"),
),
},
},
})
}

func testAccCheckAirflowUserRolesCheckDestroy(s *terraform.State) error {
client := testAccProvider.Meta().(ProviderConfig)

for _, rs := range s.RootModule().Resources {
if rs.Type != "airflow_user_roles" {
continue
}

user, res, err := client.ApiClient.UserApi.GetUser(client.AuthContext, rs.Primary.ID).Execute()
if err == nil {
if len(*user.Roles) != 0 {
return fmt.Errorf("Airflow User (%s) still have some roles.", rs.Primary.ID)
}
}

if res != nil && res.StatusCode == 404 {
continue
}
}
client.ApiClient.UserApi.DeleteUser(client.AuthContext, accName).Execute()

return nil
}

func testAccAirflowUserRolesConfigBasic(accName, rName string) string {
return fmt.Sprintf(`
resource "airflow_role" "test" {
name = %[1]q

action {
action = "can_read"
resource = "Audit Logs"
}
}

resource "airflow_user_roles" "test" {
username = %[2]q
roles = [airflow_role.test.name]
}
`, rName, accName)
}

func testAccAirflowUserAddRolesConfigBasic(accName, rName, r2Name string) string {
return fmt.Sprintf(`
resource "airflow_role" "test" {
name = %[1]q

action {
action = "can_read"
resource = "Audit Logs"
}
}

resource "airflow_role" "test2" {
name = %[2]q

action {
action = "menu_access"
resource = "Audit Logs"
}
}

resource "airflow_user_roles" "test" {
username = %[3]q
roles = [airflow_role.test.name, airflow_role.test2.name]
}
`, rName, r2Name, accName)
}

func testAccPreCheckCreateUser(t *testing.T) {
testAccPreCheck(t)
endpoint := os.Getenv("AIRFLOW_BASE_ENDPOINT")
u, err := url.Parse(endpoint)
if err != nil {
t.Fatalf("failed to initialise Airflow at `%s`: %s", endpoint, err)
}

client := &http.Client{
Transport: logging.NewLoggingHTTPTransport(http.DefaultTransport),
}
path := strings.TrimSuffix(u.Path, "/")
apiClient := airflow.NewAPIClient(&airflow.Configuration{
Scheme: u.Scheme,
Host: u.Host,
Debug: true,
HTTPClient: client,
Servers: airflow.ServerConfigurations{
{
URL: fmt.Sprint(path, "/api/v1"),
Description: "Apache Airflow Stable API.",
},
},
})
authContext := context.Background()
cred := airflow.BasicAuth{
UserName: os.Getenv("AIRFLOW_API_USERNAME"),
Password: os.Getenv("AIRFLOW_API_PASSWORD"),
}
authContext = context.WithValue(authContext, airflow.ContextBasicAuth, cred)

email := acctest.RandomWithPrefix("tf-role-email-test")
firstName := acctest.RandomWithPrefix("tf-role-first-name-test")
lastName := acctest.RandomWithPrefix("tf-role-last-name-test")
password := acctest.RandomWithPrefix("tf-role-password-test")
publicRoleName := "Public"
roles := []airflow.UserCollectionItemRoles{{Name: &publicRoleName}}
username := accName
_, _, err = apiClient.UserApi.PostUser(authContext).User(airflow.User{
Email: &email,
FirstName: &firstName,
LastName: &lastName,
Username: &username,
Password: &password,
Roles: &roles,
}).Execute()
if err != nil {
t.Fatalf("failed to create user `%s` from Airflow: %s", username, err)
}
}