Skip to content

Commit

Permalink
feat: keycloak
Browse files Browse the repository at this point in the history
Closes #86
  • Loading branch information
miton18 committed Nov 6, 2024
1 parent 0049392 commit e0813da
Show file tree
Hide file tree
Showing 8 changed files with 324 additions and 2 deletions.
8 changes: 6 additions & 2 deletions pkg/provider.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package pkg

import "go.clever-cloud.com/terraform-provider/pkg/tmp"
import (
"strings"

"go.clever-cloud.com/terraform-provider/pkg/tmp"
)

func AddonProvidersAsList(providers []tmp.AddonProvider) []string {
return Map(providers, func(provider tmp.AddonProvider) string {
Expand All @@ -20,7 +24,7 @@ func LookupProviderPlan(provider *tmp.AddonProvider, planId string) *tmp.AddonPl
}

return First(provider.Plans, func(plan tmp.AddonPlan) bool {
return plan.Slug == planId
return strings.EqualFold(plan.Slug, planId)
})
}

Expand Down
2 changes: 2 additions & 0 deletions pkg/registry/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"go.clever-cloud.com/terraform-provider/pkg/resources/cellar/bucket"
"go.clever-cloud.com/terraform-provider/pkg/resources/docker"
"go.clever-cloud.com/terraform-provider/pkg/resources/java"
"go.clever-cloud.com/terraform-provider/pkg/resources/keycloak"
"go.clever-cloud.com/terraform-provider/pkg/resources/materiakv"
"go.clever-cloud.com/terraform-provider/pkg/resources/mongodb"
"go.clever-cloud.com/terraform-provider/pkg/resources/nodejs"
Expand All @@ -34,4 +35,5 @@ var Resources = []func() resource.Resource{
scala.NewResourceScala(),
static.NewResourceStatic(),
docker.NewResourceDocker,
keycloak.NewResourceKeycloak,
}
162 changes: 162 additions & 0 deletions pkg/resources/keycloak/crud.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package keycloak

import (
"context"
"fmt"
"strings"

"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-log/tflog"
"go.clever-cloud.com/terraform-provider/pkg"
"go.clever-cloud.com/terraform-provider/pkg/provider"
"go.clever-cloud.com/terraform-provider/pkg/tmp"
)

// Weird behaviour, but TF can ask for a Resource without having configured a Provider (maybe for Meta and Schema)
// So we need to handle the case there is no ProviderData
func (r *ResourceKeycloak) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
tflog.Debug(ctx, "ResourceKeycloak.Configure()")

// Prevent panic if the provider has not been configured.
if req.ProviderData == nil {
return
}

provider, ok := req.ProviderData.(provider.Provider)
if ok {
r.cc = provider.Client()
r.org = provider.Organization()
}

tflog.Warn(ctx, "Keycloak product is still in beta, use it with care")
}

// Create a new resource
func (r *ResourceKeycloak) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
kc := Keycloak{}

resp.Diagnostics.Append(req.Plan.Get(ctx, &kc)...)
if resp.Diagnostics.HasError() {
return
}

addonsProvidersRes := tmp.GetAddonsProviders(ctx, r.cc)
if addonsProvidersRes.HasError() {
resp.Diagnostics.AddError("failed to get addon providers", addonsProvidersRes.Error().Error())
return
}

addonsProviders := addonsProvidersRes.Payload()
provider := pkg.LookupAddonProvider(*addonsProviders, "keycloak")

plan := pkg.LookupProviderPlan(provider, "beta")
if plan == nil {
resp.Diagnostics.AddError("This plan does not exists", "available plans are: "+strings.Join(pkg.ProviderPlansAsList(provider), ", "))
return
}

addonReq := tmp.AddonRequest{
Name: kc.Name.ValueString(),
Plan: plan.ID,
ProviderID: "keycloak",
Region: kc.Region.ValueString(),
}

res := tmp.CreateAddon(ctx, r.cc, r.org, addonReq)
if res.HasError() {
resp.Diagnostics.AddError("failed to create addon", res.Error().Error())
return
}

kc.ID = pkg.FromStr(res.Payload().RealID)
kc.CreationDate = pkg.FromI(res.Payload().CreationDate)

resp.Diagnostics.Append(resp.State.Set(ctx, kc)...)
if resp.Diagnostics.HasError() {
return
}

kcEnvRes := tmp.GetAddonEnv(ctx, r.cc, r.org, kc.ID.ValueString())
if kcEnvRes.HasError() {
resp.Diagnostics.AddError("failed to get Keycloak connection infos", kcEnvRes.Error().Error())
return
}

kcEnv := *kcEnvRes.Payload()
tflog.Debug(ctx, "API response", map[string]interface{}{
"payload": fmt.Sprintf("%+v", kcEnv),
})

hostEnvVar := pkg.First(kcEnv, func(v tmp.EnvVar) bool {
return v.Name == "CC_KEYCLOAK_URL"
})
if hostEnvVar == nil {
resp.Diagnostics.AddError("cannot get Keycloak infos", "missing CC_KEYCLOAK_URL env var on created addon")
return
}

kc.Host = pkg.FromStr(hostEnvVar.Value)

resp.Diagnostics.Append(resp.State.Set(ctx, kc)...)
if resp.Diagnostics.HasError() {
return
}
}

// Read resource information
func (r *ResourceKeycloak) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
tflog.Debug(ctx, "Keycloak READ", map[string]interface{}{"request": req})

var kc Keycloak
diags := req.State.Get(ctx, &kc)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}

// TODO

diags = resp.State.Set(ctx, kc)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}

// Update resource
func (r *ResourceKeycloak) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
// TODO
}

// Delete resource
func (r *ResourceKeycloak) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
kc := Keycloak{}

diags := req.State.Get(ctx, &kc)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Debug(ctx, "Keycloak DELETE", map[string]interface{}{"keycloak": kc})

res := tmp.DeleteAddon(ctx, r.cc, r.org, kc.ID.ValueString())
if res.IsNotFoundError() {
resp.State.RemoveResource(ctx)
return
}
if res.HasError() {
resp.Diagnostics.AddError("failed to delete addon", res.Error().Error())
return
}

resp.State.RemoveResource(ctx)
}

// Import resource
func (r *ResourceKeycloak) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
// Save the import identifier in the id attribute
// and call Read() to fill fields
attr := path.Root("id")
resource.ImportStatePassthroughID(ctx, attr, req, resp)
}
1 change: 1 addition & 0 deletions pkg/resources/keycloak/doc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Manage Keycloak
21 changes: 21 additions & 0 deletions pkg/resources/keycloak/keycloak.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package keycloak

import (
"context"

"github.com/hashicorp/terraform-plugin-framework/resource"
"go.clever-cloud.dev/client"
)

type ResourceKeycloak struct {
cc *client.Client
org string
}

func NewResourceKeycloak() resource.Resource {
return &ResourceKeycloak{}
}

func (r *ResourceKeycloak) Metadata(ctx context.Context, req resource.MetadataRequest, res *resource.MetadataResponse) {
res.TypeName = req.ProviderTypeName + "_keycloak"
}
65 changes: 65 additions & 0 deletions pkg/resources/keycloak/keycloak_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package keycloak_test

import (
"context"
_ "embed"
"fmt"
"os"
"regexp"
"testing"
"time"

"github.com/hashicorp/terraform-plugin-framework/providerserver"
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/terraform"
"go.clever-cloud.com/terraform-provider/pkg/helper"
"go.clever-cloud.com/terraform-provider/pkg/provider/impl"
"go.clever-cloud.com/terraform-provider/pkg/tmp"
"go.clever-cloud.dev/client"
)

var protoV6Provider = map[string]func() (tfprotov6.ProviderServer, error){
"clevercloud": providerserver.NewProtocol6WithError(impl.New("test")()),
}

func TestAccKeycloak_basic(t *testing.T) {
ctx := context.Background()
rName := fmt.Sprintf("tf-test-kc-%d", time.Now().UnixMilli())
fullName := fmt.Sprintf("clevercloud_keycloak.%s", rName)
cc := client.New(client.WithAutoOauthConfig())
org := os.Getenv("ORGANISATION")
providerBlock := helper.NewProvider("clevercloud").SetOrganisation(org).String()
materiakvBlock := helper.NewRessource("clevercloud_keycloak", rName, helper.SetKeyValues(map[string]any{"name": rName, "region": "par"})).String()

resource.Test(t, resource.TestCase{
PreCheck: func() {
if org == "" {
t.Fatalf("missing ORGANISATION env var")
}
},
ProtoV6ProviderFactories: protoV6Provider,
CheckDestroy: func(state *terraform.State) error {
for _, resource := range state.RootModule().Resources {
res := tmp.GetAddon(ctx, cc, org, resource.Primary.ID)
if res.IsNotFoundError() {
continue
}
if res.HasError() {
return fmt.Errorf("unexpectd error: %s", res.Error().Error())
}

return fmt.Errorf("expect resource '%s' to be deleted: %+v", resource.Primary.ID, res.Payload())
}
return nil
},
Steps: []resource.TestStep{{
ResourceName: rName,
Config: providerBlock + materiakvBlock,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestMatchResourceAttr(fullName, "id", regexp.MustCompile(`^keycloak_.*`)),
resource.TestMatchResourceAttr(fullName, "host", regexp.MustCompile(`^.*clever-cloud.com$`)),
),
}},
})
}
46 changes: 46 additions & 0 deletions pkg/resources/keycloak/schema.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package keycloak

import (
"context"
_ "embed"

"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
"github.com/hashicorp/terraform-plugin-framework/types"
)

type Keycloak struct {
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
CreationDate types.Int64 `tfsdk:"creation_date"`
Region types.String `tfsdk:"region"`
Host types.String `tfsdk:"host"`
}

//go:embed doc.md
var resourceKeycloakDoc string

func (r ResourceKeycloak) Schema(_ context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Version: 0,
MarkdownDescription: resourceKeycloakDoc,
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{Required: true, MarkdownDescription: "Name of the service"},
"region": schema.StringAttribute{
Optional: true,
Computed: true,
Default: stringdefault.StaticString("par"),
MarkdownDescription: "Geographical region where the data will be stored",
},
"id": schema.StringAttribute{Computed: true, MarkdownDescription: "Generated unique identifier"},
"creation_date": schema.Int64Attribute{Computed: true, MarkdownDescription: "Date of database creation"},
"host": schema.StringAttribute{Computed: true, MarkdownDescription: "URL to access Keycloak"},
},
}
}

// https://developer.hashicorp.com/terraform/plugin/framework/resources/state-upgrade#implementing-state-upgrade-support
func (r ResourceKeycloak) UpgradeState(ctx context.Context) map[int64]resource.StateUpgrader {
return map[int64]resource.StateUpgrader{}
}
21 changes: 21 additions & 0 deletions pkg/tmp/addon.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,27 @@ func GetMongoDB(ctx context.Context, cc *client.Client, mongodbID string) client
return client.Get[MongoDB](ctx, cc, path)
}

type Keycloak struct {
OwnerID string `json:"ownerId"`
ID string `json:"addonId"`
NetworkgroupID *string `json:"networkgroupId"`
PostgresID string `json:"postgresId"`
FSBucketID string `json:"fsBucketId"`
Applications []KeycloakApplication `json:"applications"`
}
type KeycloakApplication struct {
KeycloakID string `json:"appId"`
KeycloakPlan string `json:"planIdentifier"`
Host string `json:"host"`
JavaApplicationID string `json:"javaId"`
}

// Not working ?
func GetKeycloak(ctx context.Context, cc *client.Client, organisationID, keycloakID string) client.Response[Keycloak] {
path := fmt.Sprintf("v4/keycloaks/organisations/%s/keycloaks/%s", organisationID, keycloakID)
return client.Get[Keycloak](ctx, cc, path)
}

type DeleteAddonResponse struct {
ID int64 `json:"id"`
Message string `json:"message"`
Expand Down

0 comments on commit e0813da

Please sign in to comment.