Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provider based on teraform-framework #3

Closed
wants to merge 60 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
bd736c1
fix[tf]: use map for networks resource
Oct 2, 2023
27dc907
add mapped obj constructor in TF provider
EvgenyGri Oct 3, 2023
d343d8e
feat[tf]: plugin framework skeleton
Oct 3, 2023
b5e3d38
merge master
EvgenyGri Oct 3, 2023
7b8b114
feat[tf]: implementation for network resource
Oct 4, 2023
78fd3bf
feat[tf]: implementation for security group resource
Oct 5, 2023
d482246
feat[tf]: convert single network resource into collection
Oct 5, 2023
fd50b8b
feat[tf]: make collection resource from sgroups_group
Oct 6, 2023
e49cc97
feat[tf]: refactor collection's stuff to make it more abstract
Oct 8, 2023
1b97f90
+TODO(S)
EvgenyGri Oct 9, 2023
c6198db
fix todos
Oct 9, 2023
9ab0b8b
fix[tf]: use types.Set for networks attribute
Oct 10, 2023
04d2ee2
feat[tf]: ignore unchanged items on update
Oct 10, 2023
142f9e9
rename target folder
EvgenyGri Oct 12, 2023
fed0272
ditto
EvgenyGri Oct 12, 2023
ec85b5b
support build old tf provider
EvgenyGri Oct 12, 2023
413ab6d
use tflog
EvgenyGri Oct 12, 2023
6ec03f8
correct SGs resource
EvgenyGri Oct 12, 2023
0f1dbaf
correct descriptions
EvgenyGri Oct 12, 2023
8f7944b
simplify code
EvgenyGri Oct 12, 2023
a3e2bbc
feat[tf]: impl sg to fqdn rules resource
Oct 13, 2023
2abab27
fix[tf]: proper name for resource
Oct 13, 2023
0620971
feat[tf]: impl sg to sg rules resource
Oct 13, 2023
c051e56
fix[tf]: cleanup unused code
Oct 13, 2023
71621ea
fix[tf]: cleanup
Oct 13, 2023
f7bda4e
advance PR
EvgenyGri Oct 14, 2023
9327a2c
ditto
EvgenyGri Oct 14, 2023
31d5dfb
ditto
EvgenyGri Oct 14, 2023
0e11de9
fix typos
Oct 16, 2023
33dfe8e
fix[tf]: make nets/sgs one view with others
Oct 16, 2023
403cd92
feat[tf]: start acceptance testing
Oct 17, 2023
3d357cb
feat[tf]: tests for security groups resource
Oct 18, 2023
2eb9009
fix[tf]: code cleanup
Oct 19, 2023
a43309e
feat[tf]: tests for both rules
Oct 19, 2023
32cbaa8
fix[tf]: rename to be more clear
Oct 19, 2023
0439e3a
fix[tf]: run tf tests
Oct 19, 2023
d446354
advance PR
EvgenyGri Oct 25, 2023
557bf32
ditto
EvgenyGri Oct 25, 2023
deaca80
fix[tf]: first try suited tests
Oct 26, 2023
ea03f96
+ tf testing fixture tooling
EvgenyGri Oct 27, 2023
1a6a5eb
fix[tf]: move networks tests to table approach
Oct 27, 2023
716594b
fix[tf]: change map of cases to array
Oct 29, 2023
d3f6fd2
fix[tf]: use table approach for other resources
Oct 29, 2023
12bc6ed
fix[tf]: code cleanup
Oct 29, 2023
f1c7f56
+ advance PR
EvgenyGri Oct 29, 2023
5c7af9f
+ TODO(s)
EvgenyGri Oct 29, 2023
59ca763
fix typos in yaml files
EvgenyGri Oct 29, 2023
9461f9a
fix[tf]: use `ExpectationsChecker` in tests
Oct 30, 2023
a3b3e4b
fix[tf]: tests for some fixtures entities
Oct 30, 2023
b6a55ee
fix[tf]: remove unused attr
Oct 30, 2023
20d1248
fix[tf]: embed server
Oct 30, 2023
63c7a76
feat[tf]: move server stuf to its own struct
Oct 31, 2023
37dc040
fix[tf]: error handling
Oct 31, 2023
34432f4
+ TODO(S) and best wishes
EvgenyGri Oct 31, 2023
29d1c2c
fix[tf]: start embed server with corlib
Oct 31, 2023
4f25f20
+ TODO(S) and best wishes
EvgenyGri Nov 2, 2023
162785c
fix[tf]: resolve TODO(S) with hope
Nov 2, 2023
ea4f4cd
make code better
EvgenyGri Nov 2, 2023
c77780d
linter issue
EvgenyGri Nov 2, 2023
5fd3ce9
linter issues
EvgenyGri Nov 4, 2023
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
2 changes: 1 addition & 1 deletion .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ output:
run:
timeout: 5m
issues-exit-code: 1
tests: true
tests: false
skip-dirs:
- bin
- vendor
Expand Down
17 changes: 17 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ go-deps: ##install golang dependencies
$(GO) mod verify && \
echo -=OK=-

.PHONY: test-tf-provider
test-tf-provider: ##run tests for tf provider only
@echo running tf provider tests... && \
$(GO) clean -testcache && \
$(GO) test -v ./cmd/sgroups-tf-v2/internal/provider && \
echo -=OK=-

.PHONY: test
test: ##run tests
Expand Down Expand Up @@ -122,6 +128,17 @@ sgroups-tf:
$(GO) build -ldflags="$(LDFLAGS)" -o $(OUT) $(CURDIR)/cmd/$(APP) &&\
echo -=OK=-

.PHONY: sgroups-tf-v2
sgroups-tf-v2: | go-deps ##build SGroups Terraform provider V2
sgroups-tf-v2: APP=sgroups-tf-v2
sgroups-tf-v2: OUT=$(CURDIR)/bin/terraform-provider-sgroups
sgroups-tf-v2:
@echo build \"$(APP)\" for OS/ARCH=\"$(os)/$(arch)\" ... && \
echo into \"$(OUT)\" && \
env GOOS=$(os) GOARCH=$(arch) CGO_ENABLED=0 \
$(GO) build -ldflags="$(LDFLAGS)" -o $(OUT) $(CURDIR)/cmd/$(APP) &&\
echo -=OK=-


GOOSE_REPO:=https://github.com/pressly/goose
GOOSE_LATEST_VERSION:= $(shell git ls-remote --tags --refs --sort='v:refname' $(GOOSE_REPO)|tail -1|egrep -o "v[0-9]+.*")
Expand Down
226 changes: 226 additions & 0 deletions cmd/sgroups-tf-v2/internal/provider/abstract.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
package provider

import (
"context"
"fmt"

protos "github.com/H-BF/protos/pkg/api/sgroups"
sgAPI "github.com/H-BF/sgroups/internal/api/sgroups"

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

type (
/*//
// KeyOps -
KeyOps[T any] interface {
String() string
FromString(string) error
}
*/

SingleResource[T any] interface {
ResourceAttributes() map[string]schema.Attribute
IsDiffer(T) bool
}

subjectOfSync interface {
protos.SyncNetworks |
protos.SyncSecurityGroups |
protos.SyncSGRules |
protos.SyncFqdnRules
}

Description struct {
ResourceDescription string
ItemsDescription string
}

CollectionResource[T SingleResource[T], S subjectOfSync] struct {
suffix string
description Description
client *sgAPI.Client
toSubjOfSync func(context.Context, map[string]T) (*S, diag.Diagnostics)
read func(context.Context, CollectionResourceModel[T, S], *sgAPI.Client) (CollectionResourceModel[T, S], diag.Diagnostics)
}
)

func (c *CollectionResource[T, S]) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + c.suffix
}

func (c *CollectionResource[T, S]) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
var sr T
resp.Schema = schema.Schema{
Description: c.description.ResourceDescription,
Attributes: map[string]schema.Attribute{
"items": schema.MapNestedAttribute{
Description: c.description.ItemsDescription,
Required: true,
NestedObject: schema.NestedAttributeObject{
Attributes: sr.ResourceAttributes(),
},
},
},
}
}

func (c *CollectionResource[T, S]) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan CollectionResourceModel[T, S]

// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}

// Convert from Terraform data model into GRPC data model
syncReq, diags := plan.toSyncReq(ctx, protos.SyncReq_Upsert, c.toSubjOfSync)
if diags.HasError() {
resp.Diagnostics.Append(diags...)
return
}

// Send GRPC request
if _, err := c.client.Sync(ctx, syncReq); err != nil {
resp.Diagnostics.AddError(
"create "+c.description.ItemsDescription,
err.Error())
return
}

// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, plan)...)
}

func (c *CollectionResource[T, S]) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state CollectionResourceModel[T, S]

// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}

// Create GRPC request and convert response to Terraform data model
var diags diag.Diagnostics
if state, diags = c.read(ctx, state, c.client); diags.HasError() {
for _, diagError := range diags.Errors() {
resp.Diagnostics.AddError(
"read "+c.description.ItemsDescription,
diagError.Summary()+":"+diagError.Detail())
}
}

// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}

func (c *CollectionResource[T, S]) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan, state CollectionResourceModel[T, S]

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

itemsToDelete := map[string]T{}
for name, itemFeatures := range state.Items {
if _, ok := plan.Items[name]; !ok {
// if item is missing in plan state - delete it
itemsToDelete[name] = itemFeatures
}
}

if len(itemsToDelete) > 0 {
tempModel := CollectionResourceModel[T, S]{
Items: itemsToDelete,
}
delReq, diags := tempModel.toSyncReq(ctx, protos.SyncReq_Delete, c.toSubjOfSync)
if diags.HasError() {
resp.Diagnostics.Append(diags...)
return
}
if _, err := c.client.Sync(ctx, delReq); err != nil {
//c.description.ResourceDescription
resp.Diagnostics.AddError(
"delete "+c.description.ItemsDescription,
err.Error())
return
}
}

itemsToUpdate := map[string]T{}
for name, itemFeatures := range plan.Items {
// in plan state can have unchanged items which should be ignored
// missing items before and changed items should be updated
if oldItemFeatures, ok := state.Items[name]; !ok || itemFeatures.IsDiffer(oldItemFeatures) {
itemsToUpdate[name] = itemFeatures
}
}

if len(itemsToUpdate) > 0 {
tempModel := CollectionResourceModel[T, S]{
Items: itemsToUpdate,
}
updateReq, diags := tempModel.toSyncReq(ctx, protos.SyncReq_Upsert, c.toSubjOfSync)
if diags.HasError() {
resp.Diagnostics.Append(diags...)
return
}
if _, err := c.client.Sync(ctx, updateReq); err != nil {
resp.Diagnostics.AddError(
"update "+c.description.ItemsDescription,
err.Error())
return
}
}

resp.Diagnostics.Append(resp.State.Set(ctx, plan)...)
}

func (c *CollectionResource[T, S]) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var state CollectionResourceModel[T, S]

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

delReq, diags := state.toSyncReq(ctx, protos.SyncReq_Delete, c.toSubjOfSync)
if diags.HasError() {
resp.Diagnostics.Append(diags...)
return
}

if _, err := c.client.Sync(ctx, delReq); err != nil {
resp.Diagnostics.AddError(
"delete "+c.description.ItemsDescription,
err.Error())
return
}
}

func (c *CollectionResource[T, S]) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}

client, ok := req.ProviderData.(sgAPI.Client)
if !ok {
resp.Diagnostics.AddError(
"unexpected Data Source type",
fmt.Sprintf("Expected sgroups client but got: %T.", req.ProviderData),
)
return
}
c.client = &client
}

var (
_ resource.Resource = &CollectionResource[networkItem, protos.SyncNetworks]{}
_ resource.ResourceWithConfigure = &CollectionResource[networkItem, protos.SyncNetworks]{}
)
40 changes: 40 additions & 0 deletions cmd/sgroups-tf-v2/internal/provider/access-ports.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package provider

import (
protos "github.com/H-BF/protos/pkg/api/sgroups"

"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 (
AccessPorts struct {
Source types.String `tfsdk:"s"`
Destination types.String `tfsdk:"d"`
}
)

func (p AccessPorts) ResourceAttributes() map[string]schema.Attribute {
return map[string]schema.Attribute{
"s": schema.StringAttribute{
Description: "source port/ports range",
Optional: true,
Computed: true,
Default: stringdefault.StaticString(""),
},
"d": schema.StringAttribute{
Description: "destination port/ports range",
Optional: true,
Computed: true,
Default: stringdefault.StaticString(""),
},
}
}

func (p AccessPorts) toProto() *protos.AccPorts {
return &protos.AccPorts{
S: p.Source.ValueString(),
D: p.Destination.ValueString(),
}
}
40 changes: 40 additions & 0 deletions cmd/sgroups-tf-v2/internal/provider/collection-resource-model.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package provider

import (
"context"

protos "github.com/H-BF/protos/pkg/api/sgroups"
"github.com/hashicorp/terraform-plugin-framework/diag"
)

type (
CollectionResourceModel[T any, S subjectOfSync] struct {
Items map[string]T `tfsdk:"items"`
}
)

func (model *CollectionResourceModel[T, S]) toSyncReq(
ctx context.Context, operation protos.SyncReq_SyncOp,
toProto func(context.Context, map[string]T) (*S, diag.Diagnostics),
) (*protos.SyncReq, diag.Diagnostics) {
req := &protos.SyncReq{SyncOp: operation}

s, diags := toProto(ctx, model.Items)
if diags.HasError() {
return nil, diags
}
switch subject := any(s).(type) {
case *protos.SyncNetworks:
req.Subject = &protos.SyncReq_Networks{Networks: subject}
case *protos.SyncSecurityGroups:
req.Subject = &protos.SyncReq_Groups{Groups: subject}
case *protos.SyncSGRules:
req.Subject = &protos.SyncReq_SgRules{SgRules: subject}
case *protos.SyncFqdnRules:
req.Subject = &protos.SyncReq_FqdnRules{FqdnRules: subject}
default:
panic("unexpected subject")
}

return req, nil
}
Loading