diff options
-rw-r--r-- | CHANGELOG.md | 7 | ||||
-rw-r--r-- | docs/resources/identity.md | 52 | ||||
-rw-r--r-- | examples/resources/eventline_identity/import.sh | 1 | ||||
-rw-r--r-- | examples/resources/eventline_identity/resource.tf | 12 | ||||
-rw-r--r-- | external/evcli/client.go | 31 | ||||
-rw-r--r-- | external/evcli/identities.go | 28 | ||||
-rw-r--r-- | internal/provider/identity_resource.go | 223 | ||||
-rw-r--r-- | internal/provider/provider.go | 1 |
8 files changed, 327 insertions, 28 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 4534eb6..08bac26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 0.1.0 - 2023-09-24 +### Features +* Add identity resource + +### Notes +* Upgraded dependencies + ## 0.0.7 - 2023-09-19 ### Notes diff --git a/docs/resources/identity.md b/docs/resources/identity.md new file mode 100644 index 0000000..9cfb965 --- /dev/null +++ b/docs/resources/identity.md @@ -0,0 +1,52 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "eventline_identity Resource - terraform-provider-eventline" +subcategory: "" +description: |- + Eventline identity resource +--- + +# eventline_identity (Resource) + +Eventline identity resource + +## Example Usage + +```terraform +data "eventline_project" "main" { + name = "main" +} + +resource "eventline_identity" "example" { + name = "example" + project_id = data.project.main.id + + connector = "eventline" + data = "{\n \"key\": \"test\"\n }" + type = "api_key" +} +``` + +<!-- schema generated by tfplugindocs --> +## Schema + +### Required + +- `connector` (String) The connector used for the identity. +- `data` (String, Sensitive) The json raw data of the identity. +- `name` (String) The name of the identity. +- `project_id` (String) Project id +- `type` (String) The type of the identity. + +### Read-Only + +- `id` (String) The identifier of the identity. +- `status` (String) The status of the identity. + +## Import + +Import is supported using the following syntax: + +```shell +terraform import eventline_identity.test <project_id>/<identity_id> +``` diff --git a/examples/resources/eventline_identity/import.sh b/examples/resources/eventline_identity/import.sh new file mode 100644 index 0000000..e0efa61 --- /dev/null +++ b/examples/resources/eventline_identity/import.sh @@ -0,0 +1 @@ +terraform import eventline_identity.test <project_id>/<identity_id> diff --git a/examples/resources/eventline_identity/resource.tf b/examples/resources/eventline_identity/resource.tf new file mode 100644 index 0000000..3ed68d0 --- /dev/null +++ b/examples/resources/eventline_identity/resource.tf @@ -0,0 +1,12 @@ +data "eventline_project" "main" { + name = "main" +} + +resource "eventline_identity" "example" { + name = "example" + project_id = data.project.main.id + + connector = "eventline" + data = "{\n \"key\": \"test\"\n }" + type = "api_key" +} diff --git a/external/evcli/client.go b/external/evcli/client.go index 769c19c..3720115 100644 --- a/external/evcli/client.go +++ b/external/evcli/client.go @@ -181,6 +181,12 @@ func (c *Client) UpdateProject(project *eventline.Project) error { return c.SendRequest("PUT", uri, project, nil) } +func (c *Client) CreateIdentity(identity *Identity) error { + uri := NewURL("identities") + + return c.SendRequest("POST", uri, identity, identity) +} + func (c *Client) FetchIdentities() (Identities, error) { var identities Identities @@ -209,6 +215,31 @@ func (c *Client) FetchIdentities() (Identities, error) { return identities, nil } +func (c *Client) FetchIdentityById(id eventline.Id) (*Identity, error) { + uri := NewURL("identities", "id", id.String()) + + var identity Identity + + err := c.SendRequest("GET", uri, nil, &identity) + if err != nil { + return nil, err + } + + return &identity, nil +} + +func (c *Client) UpdateIdentity(identity *Identity) error { + uri := NewURL("identities", "id", identity.Id.String()) + + return c.SendRequest("PUT", uri, identity, identity) +} + +func (c *Client) DeleteIdentity(id eventline.Id) error { + uri := NewURL("identities", "id", id.String()) + + return c.SendRequest("DELETE", uri, nil, nil) +} + func (c *Client) ReplayEvent(id string) (*eventline.Event, error) { var event eventline.Event diff --git a/external/evcli/identities.go b/external/evcli/identities.go index f89ae1b..a2c2549 100644 --- a/external/evcli/identities.go +++ b/external/evcli/identities.go @@ -2,7 +2,6 @@ package evcli import ( "encoding/json" - "fmt" "time" "github.com/exograd/eventline/pkg/eventline" @@ -27,7 +26,6 @@ type Identity struct { RefreshTime *time.Time `json:"refresh_time,omitempty"` Connector string `json:"connector"` Type string `json:"type"` - Data eventline.IdentityData `json:"-"` RawData json.RawMessage `json:"data"` } @@ -45,29 +43,3 @@ func (i *Identity) SortKey(sort string) (key string) { return } - -func (pi *Identity) MarshalJSON() ([]byte, error) { - type Identity2 Identity - - i := Identity2(*pi) - data, err := json.Marshal(i.Data) - if err != nil { - return nil, fmt.Errorf("cannot encode data: %w", err) - } - - i.RawData = data - - return json.Marshal(i) -} - -func (pi *Identity) UnmarshalJSON(data []byte) error { - type Identity2 Identity - - i := Identity2(*pi) - if err := json.Unmarshal(data, &i); err != nil { - return err - } - - *pi = Identity(i) - return nil -} diff --git a/internal/provider/identity_resource.go b/internal/provider/identity_resource.go new file mode 100644 index 0000000..8804f54 --- /dev/null +++ b/internal/provider/identity_resource.go @@ -0,0 +1,223 @@ +package provider + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strings" + + "git.adyxax.org/adyxax/terraform-provider-eventline/external/evcli" + "github.com/exograd/go-daemon/ksuid" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type IdentityResource struct { + client *evcli.Client +} + +var _ resource.Resource = &IdentityResource{} // Ensure provider defined types fully satisfy framework interfaces +var _ resource.ResourceWithImportState = &IdentityResource{} // Ensure provider defined types fully satisfy framework interfaces +func NewIdentityResource() resource.Resource { + return &IdentityResource{} +} + +type IdentityResourceModel struct { + Connector types.String `tfsdk:"connector"` + Id types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + ProjectId types.String `tfsdk:"project_id"` + RawData types.String `tfsdk:"data"` + Status types.String `tfsdk:"status"` + Type types.String `tfsdk:"type"` +} + +func (r *IdentityResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_identity" +} + +func (r *IdentityResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "connector": schema.StringAttribute{ + MarkdownDescription: "The connector used for the identity.", + Required: true, + }, + "data": schema.StringAttribute{ + MarkdownDescription: "The json raw data of the identity.", + Required: true, + Sensitive: true, + }, + "id": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "The identifier of the identity.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "The name of the identity.", + Required: true, + }, + "project_id": schema.StringAttribute{ + MarkdownDescription: "Project id", + Required: true, + }, + "status": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "The status of the identity.", + }, + "type": schema.StringAttribute{ + MarkdownDescription: "The type of the identity.", + Required: true, + }, + }, + MarkdownDescription: "Eventline identity resource", + } +} + +func (r *IdentityResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.client, _ = req.ProviderData.(*evcli.Client) +} + +func (r *IdentityResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data *IdentityResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + var id ksuid.KSUID + if err := id.Parse(data.ProjectId.ValueString()); err != nil { + resp.Diagnostics.AddError("KsuidParse", fmt.Sprintf("Unable to parse project id, got error: %s", err)) + return + } + r.client.ProjectId = &id + identity := evcli.Identity{ + Connector: data.Connector.ValueString(), + Name: data.Name.ValueString(), + ProjectId: &id, + RawData: json.RawMessage(data.RawData.ValueString()), + Type: data.Type.ValueString(), + } + if err := r.client.CreateIdentity(&identity); err != nil { + resp.Diagnostics.AddError("CreateIdentity", fmt.Sprintf("Unable to create identity, got error: %s\nTry importing the resource instead?", err)) + return + } + data.Id = types.StringValue(identity.Id.String()) + data.Status = types.StringValue(string(identity.Status)) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *IdentityResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data *IdentityResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + var pid ksuid.KSUID + if err := pid.Parse(data.ProjectId.ValueString()); err != nil { + resp.Diagnostics.AddError("KsuidParse", fmt.Sprintf("Unable to parse project id, got error: %s %s", err, data.ProjectId.ValueString())) + return + } + r.client.ProjectId = &pid + var id ksuid.KSUID + if err := id.Parse(data.Id.ValueString()); err != nil { + resp.Diagnostics.AddError("KsuidParse", fmt.Sprintf("Unable to parse identity id, got error: %s", err)) + return + } + identity, err := r.client.FetchIdentityById(id) + if err != nil { + var e *evcli.APIError + if errors.As(err, &e) && e.Code == "unknown_identity" { + resp.State.RemoveResource(ctx) // The identity does not exist + return + } + resp.Diagnostics.AddError("FetchIdentityById", fmt.Sprintf("Unable to fetch identity by id, got error: %s", err)) + return + } + data.Connector = types.StringValue(identity.Connector) + data.Id = types.StringValue(identity.Id.String()) + data.Name = types.StringValue(identity.Name) + data.RawData = types.StringValue(string(identity.RawData)) + data.Status = types.StringValue(string(identity.Status)) + data.Type = types.StringValue(identity.Type) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *IdentityResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data *IdentityResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + var pid ksuid.KSUID + if err := pid.Parse(data.ProjectId.ValueString()); err != nil { + resp.Diagnostics.AddError("KsuidParse", fmt.Sprintf("Unable to parse project id, got error: %s %s", err, data.ProjectId.ValueString())) + return + } + r.client.ProjectId = &pid + var id ksuid.KSUID + if err := id.Parse(data.Id.ValueString()); err != nil { + resp.Diagnostics.AddError("KsuidParse", fmt.Sprintf("Unable to parse identity id, got error: %s %s", err, data.Id.ValueString())) + return + } + identity := evcli.Identity{ + Id: id, + Name: data.Name.ValueString(), + Connector: data.Connector.ValueString(), + ProjectId: &pid, + RawData: json.RawMessage(data.RawData.ValueString()), + Type: data.Type.ValueString(), + } + if err := r.client.UpdateIdentity(&identity); err != nil { + resp.Diagnostics.AddError("UpdateIdentity", fmt.Sprintf("Unable to update identity, got error: %s", err)) + return + } + data.Status = types.StringValue(string(identity.Status)) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *IdentityResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data *IdentityResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + var pid ksuid.KSUID + if err := pid.Parse(data.ProjectId.ValueString()); err != nil { + resp.Diagnostics.AddError("KsuidParse", fmt.Sprintf("Unable to parse project id, got error: %s %s", err, data.ProjectId.ValueString())) + return + } + r.client.ProjectId = &pid + var id ksuid.KSUID + if err := id.Parse(data.Id.ValueString()); err != nil { + resp.Diagnostics.AddError("KsuidParse", fmt.Sprintf("Unable to parse identity id, got error: %s", err)) + return + } + if err := r.client.DeleteIdentity(id); err != nil { + var e *evcli.APIError + if errors.As(err, &e) && e.Code == "unknown_identity" { + return // the identity does not exist, that is what we want + } + resp.Diagnostics.AddError("DeleteIdentity", fmt.Sprintf("Unable to delete identity by id, got error: %s", err)) + return + } +} + +func (r *IdentityResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + idParts := strings.Split(req.ID, "/") + if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { + resp.Diagnostics.AddError( + "Unexpected Import Identifier", + fmt.Sprintf("Expected import identifier with format: projectID/identityID. Got: %q", req.ID), + ) + return + } + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), idParts[1])...) +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 5bc5d73..2b19f7a 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -70,6 +70,7 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest, func (p *Provider) Resources(ctx context.Context) []func() resource.Resource { return []func() resource.Resource{ + NewIdentityResource, NewProjectResource, } } |