Skip to content

Commit

Permalink
Merge pull request #11 from DrFaust92/user-auth
Browse files Browse the repository at this point in the history
support basic auth properly
  • Loading branch information
DrFaust92 authored Jan 18, 2022
2 parents 441bff7 + 0168e9d commit e50b0ce
Show file tree
Hide file tree
Showing 11 changed files with 463 additions and 58 deletions.
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
Terraform Provider Airflow
==========================

[![terraformregistry](https://img.shields.io/badge/terraform-registry-blueviolet)](https://registry.terraform.io/providers/houqp/airflow)
![build](https://github.com/houqp/terraform-provider-airflow/workflows/build/badge.svg)

[![terraformregistry](https://img.shields.io/badge/terraform-registry-blueviolet)](https://registry.terraform.io/providers/drfaust92/airflow)
![build](https://github.com/drfaust92/terraform-provider-airflow/workflows/build/badge.svg)

Terraform provider for managing Airflow resources. See `examples` folder for
documentations on how to configure and use this provider.
50 changes: 50 additions & 0 deletions docs/resources/airflow_role.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
layout: "airflow"
page_title: "Airflow: airflow_role"
sidebar_current: "docs-airflow-resource-role"
description: |-
Provides an Airflow role
---

# airflow_role

Provides an Airflow role.

## Example Usage

```hcl
resource "airflow_role" "example" {
name = "example"
action {
action = "can_read"
resource = "Audit Logs"
}
}
```

## Argument Reference

The following arguments are supported:

* `name` - (Required) The name of the role
* `action` - (Required) The action struct that defines the role. See [Action](#action).

### Action

* `action` - (Required) The name of the permission.
* `resource` - (Required) The name of the resource.

## Attributes Reference

This resource exports the following attributes:

* `id` - The role name.

## Import

Content can be imported using the role key.

```terraform
terraform import airflow_role.default example
```
52 changes: 52 additions & 0 deletions docs/resources/airflow_user.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
layout: "airflow"
page_title: "Airflow: airflow_user"
sidebar_current: "docs-airflow-resource-user"
description: |-
Provides an Airflow user
---

# airflow_user

Provides an Airflow user.

## Example Usage

```hcl
resource "airflow_user" "example" {
email = "example"
first_name = "example"
last_name = "example"
username = "example"
password = "example"
roles = [airflow_role.example.name]
}
```

## Argument Reference

The following arguments are supported:

* `email` - (Required) The user's email
* `first_name` - (Required) The user firstname
* `last_name` - (Required) The user lastname
* `username` - (Required) The username
* `password` - (Required) The user password.
* `roles` - (Required) A set of User roles to attach to the User.

## Attributes Reference

This resource exports the following attributes:

* `active` - Whether the user is active.
* `id` - The username.
* `failed_login_count` - The number of times the login failed.
* `login_count` -The login count.

## Import

Content can be imported using the user key.

```terraform
terraform import airflow_user.example example
```
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/apache/terraform-provider-airflow
go 1.16

require (
github.com/apache/airflow-client-go/airflow v0.0.0-20210731181422-11b5c2528269
github.com/apache/airflow-client-go/airflow v0.0.0-20211202191715-b125451e80fd
github.com/golang/protobuf v1.5.2 // indirect
github.com/hashicorp/terraform-plugin-sdk/v2 v2.10.1
golang.org/x/net v0.0.0-20211020060615-d418f374d309 // indirect
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXva
github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/andybalholm/crlf v0.0.0-20171020200849-670099aa064f/go.mod h1:k8feO4+kXDxro6ErPXBRTJ/ro2mf0SsFG8s7doP9kJE=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/apache/airflow-client-go/airflow v0.0.0-20210731181422-11b5c2528269 h1:bGAJwbwQVkXxnE1oxhREFCjuYQyiJmH2JTd8tqYyEvs=
github.com/apache/airflow-client-go/airflow v0.0.0-20210731181422-11b5c2528269/go.mod h1:x2yDpHvQTpMyFzvwqnroMtzVgG9qFp/eJWA6kw5KTMM=
github.com/apache/airflow-client-go/airflow v0.0.0-20211202191715-b125451e80fd h1:mrEN57zVRd8QAJkBh+MvCpbykk1eMvUe+I62+tysImc=
github.com/apache/airflow-client-go/airflow v0.0.0-20211202191715-b125451e80fd/go.mod h1:x2yDpHvQTpMyFzvwqnroMtzVgG9qFp/eJWA6kw5KTMM=
github.com/apparentlymart/go-cidr v1.0.1 h1:NmIwLZ/KdsjIUlhf+/Np40atNXm/+lZ5txfTJ/SpF+U=
github.com/apparentlymart/go-cidr v1.0.1/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc=
github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM=
Expand Down
62 changes: 36 additions & 26 deletions provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

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

type ProviderConfig struct {
Expand All @@ -19,34 +20,41 @@ func AirflowProvider() *schema.Provider {
return &schema.Provider{
Schema: map[string]*schema.Schema{
"base_endpoint": {
Type: schema.TypeString,
Required: true,
DefaultFunc: schema.EnvDefaultFunc("AIRFLOW_BASE_ENDPOINT", nil),
Type: schema.TypeString,
Required: true,
DefaultFunc: schema.EnvDefaultFunc("AIRFLOW_BASE_ENDPOINT", nil),
ValidateFunc: validation.IsURLWithHTTPorHTTPS,
},
"oauth2_token": {
Type: schema.TypeString,
Optional: true,
Description: "The oauth to use for API authentication",
DefaultFunc: schema.EnvDefaultFunc("AIRFLOW_OAUTH2_TOKEN", nil),
Type: schema.TypeString,
Optional: true,
Description: "The oauth to use for API authentication",
DefaultFunc: schema.EnvDefaultFunc("AIRFLOW_OAUTH2_TOKEN", nil),
ConflictsWith: []string{"username", "password"},
},
"username": {
Type: schema.TypeString,
DefaultFunc: schema.EnvDefaultFunc("AIRFLOW_API_USERNAME", nil),
Optional: true,
Description: "The username to use for API basic authentication",
Type: schema.TypeString,
DefaultFunc: schema.EnvDefaultFunc("AIRFLOW_API_USERNAME", nil),
Optional: true,
Description: "The username to use for API basic authentication",
RequiredWith: []string{"password"},
ConflictsWith: []string{"oauth2_token"},
},
"password": {
Type: schema.TypeString,
DefaultFunc: schema.EnvDefaultFunc("AIRFLOW_API_PASSWORD", nil),
Optional: true,
Description: "The password to use for API basic authentication",
Type: schema.TypeString,
DefaultFunc: schema.EnvDefaultFunc("AIRFLOW_API_PASSWORD", nil),
Optional: true,
Description: "The password to use for API basic authentication",
RequiredWith: []string{"username"},
ConflictsWith: []string{"oauth2_token"},
},
},
ResourcesMap: map[string]*schema.Resource{
"airflow_connection": resourceConnection(),
"airflow_variable": resourceVariable(),
"airflow_pool": resourcePool(),
"airflow_role": resourceRole(),
"airflow_user": resourceUser(),
},
ConfigureFunc: providerConfigure,
}
Expand Down Expand Up @@ -78,18 +86,20 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) {
authCtx = context.WithValue(authCtx, airflow.ContextBasicAuth, cred)
}

return ProviderConfig{
ApiClient: airflow.NewAPIClient(&airflow.Configuration{
Scheme: u.Scheme,
Host: u.Host,
Debug: true,
Servers: airflow.ServerConfigurations{
{
URL: "/api/v1",
Description: "Apache Airflow Stable API.",
},
clientConf := &airflow.Configuration{
Scheme: u.Scheme,
Host: u.Host,
Debug: true,
Servers: airflow.ServerConfigurations{
{
URL: "/api/v1",
Description: "Apache Airflow Stable API.",
},
}),
},
}

return ProviderConfig{
ApiClient: airflow.NewAPIClient(clientConf),
AuthContext: authCtx,
}, nil
}
9 changes: 7 additions & 2 deletions provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,14 @@ func TestProvider_impl(t *testing.T) {
}

func testAccPreCheck(t *testing.T) {
if v := os.Getenv("AIRFLOW_OAUTH2_TOKEN"); v == "" {
t.Fatal("AIRFLOW_OAUTH2_TOKEN must be set for acceptance tests")
_, tokenOk := os.LookupEnv("AIRFLOW_OAUTH2_TOKEN")
_, userOk := os.LookupEnv("AIRFLOW_API_USERNAME")
_, passOk := os.LookupEnv("AIRFLOW_API_PASSWORD")

if tokenOk && !(userOk || passOk) {
t.Fatal("AIRFLOW_OAUTH2_TOKEN OR AIRFLOW_API_USERNAME/AIRFLOW_API_PASSWORD must be set for acceptance tests")
}

if v := os.Getenv("AIRFLOW_BASE_ENDPOINT"); v == "" {
t.Fatal("AIRFLOW_BASE_ENDPOINT must be set for acceptance tests")
}
Expand Down
24 changes: 12 additions & 12 deletions resource_role.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,18 @@ func resourceRoleCreate(d *schema.ResourceData, m interface{}) error {

name := d.Get("name").(string)
varApi := client.RoleApi
pool := airflow.Role{
role := airflow.Role{
Name: &name,
}

if v, ok := d.GetOk("action"); ok && v.(*schema.Set).Len() > 0 {
actions := expandAirflowRoleActions(d.Get("action").(*schema.Set).List())
pool.Actions = &actions
role.Actions = &actions
}

_, _, err := varApi.PostRole(pcfg.AuthContext).Role(pool).Execute()
_, _, err := varApi.PostRole(pcfg.AuthContext).Role(role).Execute()
if err != nil {
return fmt.Errorf("failed to create pool `%s` from Airflow: %w", name, err)
return fmt.Errorf("failed to create role `%s` from Airflow: %w", name, err)
}
d.SetId(name)

Expand All @@ -70,17 +70,17 @@ func resourceRoleRead(d *schema.ResourceData, m interface{}) error {
pcfg := m.(ProviderConfig)
client := pcfg.ApiClient

pool, resp, err := client.RoleApi.GetRole(pcfg.AuthContext, d.Id()).Execute()
role, resp, err := client.RoleApi.GetRole(pcfg.AuthContext, d.Id()).Execute()
if resp != nil && resp.StatusCode == 404 {
d.SetId("")
return nil
}
if err != nil {
return fmt.Errorf("failed to get pool `%s` from Airflow: %w", d.Id(), err)
return fmt.Errorf("failed to get role `%s` from Airflow: %w", d.Id(), err)
}

d.Set("name", pool.Name)
if err := d.Set("action", flattenAirflowRoleActions(*pool.Actions)); err != nil {
d.Set("name", role.Name)
if err := d.Set("action", flattenAirflowRoleActions(*role.Actions)); err != nil {
return fmt.Errorf("error setting action: %w", err)
}

Expand All @@ -93,14 +93,14 @@ func resourceRoleUpdate(d *schema.ResourceData, m interface{}) error {

name := d.Id()
actions := expandAirflowRoleActions(d.Get("action").(*schema.Set).List())
pool := airflow.Role{
role := airflow.Role{
Name: &name,
Actions: &actions,
}

_, _, err := client.RoleApi.PatchRole(pcfg.AuthContext, name).Role(pool).Execute()
_, _, err := client.RoleApi.PatchRole(pcfg.AuthContext, name).Role(role).Execute()
if err != nil {
return fmt.Errorf("failed to update pool `%s` from Airflow: %w", name, err)
return fmt.Errorf("failed to update role `%s` from Airflow: %w", name, err)
}

return resourceRoleRead(d, m)
Expand All @@ -112,7 +112,7 @@ func resourceRoleDelete(d *schema.ResourceData, m interface{}) error {

resp, err := client.RoleApi.DeleteRole(pcfg.AuthContext, d.Id()).Execute()
if err != nil {
return fmt.Errorf("failed to delete pool `%s` from Airflow: %w", d.Id(), err)
return fmt.Errorf("failed to delete role `%s` from Airflow: %w", d.Id(), err)
}

if resp != nil && resp.StatusCode == 404 {
Expand Down
20 changes: 8 additions & 12 deletions resource_role_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (

func TestAccAirflowRole_basic(t *testing.T) {
rName := acctest.RandomWithPrefix("tf-acc-test")
rNameUpdated := acctest.RandomWithPrefix("tf-acc-test")

resourceName := "airflow_role.test"
resource.Test(t, resource.TestCase{
Expand All @@ -20,24 +19,21 @@ func TestAccAirflowRole_basic(t *testing.T) {
CheckDestroy: testAccCheckAirflowRoleCheckDestroy,
Steps: []resource.TestStep{
{
Config: testAccAirflowRoleConfigBasic(rName, rName),
Config: testAccAirflowRoleConfigBasic(rName, "can_read", "Audit Logs"),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "name", rName),
resource.TestCheckResourceAttr(resourceName, "action.#", "1"),
resource.TestCheckTypeSetElemNestedAttrs(resourceName, "action.*", map[string]string{
"action": "can_read",
"resource": "Audit Logs",
}),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
{
Config: testAccAirflowRoleConfigBasic(rName, rNameUpdated),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "name", rName),
resource.TestCheckResourceAttr(resourceName, "action.#", "1"),
),
},
},
})
}
Expand Down Expand Up @@ -65,15 +61,15 @@ func testAccCheckAirflowRoleCheckDestroy(s *terraform.State) error {
return nil
}

func testAccAirflowRoleConfigBasic(rName, action string) string {
func testAccAirflowRoleConfigBasic(rName, action, resource string) string {
return fmt.Sprintf(`
resource "airflow_role" "test" {
name = %[1]q
action {
action = %[2]q
resource = %[2]q
resource = %[3]q
}
}
`, rName, action)
`, rName, action, resource)
}
Loading

0 comments on commit e50b0ce

Please sign in to comment.