ce7cb702218693d08ea6d83464d2457c9250027c
This commit is contained in:
commit
34c5f8963c
9 changed files with 145336 additions and 0 deletions
2
packages/versitygw-terraform-provider/.gitignore
vendored
Normal file
2
packages/versitygw-terraform-provider/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
bin/
|
||||
dist/
|
||||
34
packages/versitygw-terraform-provider/go.mod
Normal file
34
packages/versitygw-terraform-provider/go.mod
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
module github.com/quorate/terraform-provider-versitygw
|
||||
|
||||
go 1.26.4
|
||||
|
||||
require (
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.0
|
||||
github.com/hashicorp/terraform-plugin-framework v1.16.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/aws/smithy-go v1.24.0 // indirect
|
||||
github.com/fatih/color v1.15.0 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/hashicorp/go-hclog v1.6.3 // indirect
|
||||
github.com/hashicorp/go-plugin v1.7.0 // indirect
|
||||
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
||||
github.com/hashicorp/terraform-plugin-go v0.29.0 // indirect
|
||||
github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect
|
||||
github.com/hashicorp/terraform-registry-address v0.4.0 // indirect
|
||||
github.com/hashicorp/terraform-svchost v0.1.1 // indirect
|
||||
github.com/hashicorp/yamux v0.1.2 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
|
||||
github.com/oklog/run v1.1.0 // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
golang.org/x/net v0.43.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
|
||||
google.golang.org/grpc v1.75.1 // indirect
|
||||
google.golang.org/protobuf v1.36.9 // indirect
|
||||
)
|
||||
103
packages/versitygw-terraform-provider/go.sum
Normal file
103
packages/versitygw-terraform-provider/go.sum
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
|
||||
github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
|
||||
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
|
||||
github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
|
||||
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
||||
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA=
|
||||
github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8=
|
||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/terraform-plugin-framework v1.16.1 h1:1+zwFm3MEqd/0K3YBB2v9u9DtyYHyEuhVOfeIXbteWA=
|
||||
github.com/hashicorp/terraform-plugin-framework v1.16.1/go.mod h1:0xFOxLy5lRzDTayc4dzK/FakIgBhNf/lC4499R9cV4Y=
|
||||
github.com/hashicorp/terraform-plugin-go v0.29.0 h1:1nXKl/nSpaYIUBU1IG/EsDOX0vv+9JxAltQyDMpq5mU=
|
||||
github.com/hashicorp/terraform-plugin-go v0.29.0/go.mod h1:vYZbIyvxyy0FWSmDHChCqKvI40cFTDGSb3D8D70i9GM=
|
||||
github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0=
|
||||
github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow=
|
||||
github.com/hashicorp/terraform-registry-address v0.4.0 h1:S1yCGomj30Sao4l5BMPjTGZmCNzuv7/GDTDX99E9gTk=
|
||||
github.com/hashicorp/terraform-registry-address v0.4.0/go.mod h1:LRS1Ay0+mAiRkUyltGT+UHWkIqTFvigGn/LbMshfflE=
|
||||
github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ=
|
||||
github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc=
|
||||
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
|
||||
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
|
||||
github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94=
|
||||
github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU=
|
||||
github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8=
|
||||
github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
|
||||
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
|
||||
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
|
||||
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
246
packages/versitygw-terraform-provider/internal/client/client.go
Normal file
246
packages/versitygw-terraform-provider/internal/client/client.go
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"encoding/hex"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/aws/signer/v4"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Endpoint string
|
||||
Region string
|
||||
AdminAccessKey string
|
||||
AdminSecretKey string
|
||||
InsecureTLS bool
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
endpoint *url.URL
|
||||
region string
|
||||
creds aws.Credentials
|
||||
http *http.Client
|
||||
signer *v4.Signer
|
||||
}
|
||||
|
||||
type Account struct {
|
||||
Access string `xml:"Access"`
|
||||
Secret string `xml:"Secret,omitempty"`
|
||||
Role string `xml:"Role"`
|
||||
UserID int64 `xml:"UserID"`
|
||||
GroupID int64 `xml:"GroupID"`
|
||||
ProjectID int64 `xml:"ProjectID"`
|
||||
}
|
||||
|
||||
type MutableProps struct {
|
||||
XMLName xml.Name `xml:"MutableProps"`
|
||||
Secret *string `xml:"Secret,omitempty"`
|
||||
Role string `xml:"Role,omitempty"`
|
||||
UserID *int64 `xml:"UserID,omitempty"`
|
||||
GroupID *int64 `xml:"GroupID,omitempty"`
|
||||
ProjectID *int64 `xml:"ProjectID,omitempty"`
|
||||
}
|
||||
|
||||
type ListUserAccountsResult struct {
|
||||
XMLName xml.Name `xml:"ListUserAccountsResult"`
|
||||
Accounts []Account `xml:"Accounts"`
|
||||
}
|
||||
|
||||
type ErrorResponse struct {
|
||||
XMLName xml.Name `xml:"Error"`
|
||||
Code string `xml:"Code"`
|
||||
Message string `xml:"Message"`
|
||||
RequestID string `xml:"RequestId"`
|
||||
HostID string `xml:"HostId"`
|
||||
}
|
||||
|
||||
type APIError struct {
|
||||
Status int
|
||||
Code string
|
||||
Message string
|
||||
RequestID string
|
||||
HostID string
|
||||
RawBody string
|
||||
}
|
||||
|
||||
func (e *APIError) Error() string {
|
||||
if e.Message == "" {
|
||||
return fmt.Sprintf("admin API status %d: %s", e.Status, e.Code)
|
||||
}
|
||||
return fmt.Sprintf("admin API status %d: %s: %s", e.Status, e.Code, e.Message)
|
||||
}
|
||||
|
||||
func IsErrorCode(err error, code string) bool {
|
||||
var apiErr *APIError
|
||||
if !errors.As(err, &apiErr) {
|
||||
return false
|
||||
}
|
||||
return apiErr.Code == code
|
||||
}
|
||||
|
||||
func New(cfg Config) (*Client, error) {
|
||||
if cfg.Endpoint == "" {
|
||||
return nil, fmt.Errorf("endpoint is required")
|
||||
}
|
||||
if cfg.Region == "" {
|
||||
cfg.Region = "us-east-1"
|
||||
}
|
||||
if cfg.AdminAccessKey == "" {
|
||||
return nil, fmt.Errorf("admin access key is required")
|
||||
}
|
||||
if cfg.AdminSecretKey == "" {
|
||||
return nil, fmt.Errorf("admin secret key is required")
|
||||
}
|
||||
|
||||
endpoint, err := url.Parse(cfg.Endpoint)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse endpoint: %w", err)
|
||||
}
|
||||
if endpoint.Scheme == "" || endpoint.Host == "" {
|
||||
return nil, fmt.Errorf("endpoint must include scheme and host")
|
||||
}
|
||||
|
||||
return &Client{
|
||||
endpoint: endpoint,
|
||||
region: cfg.Region,
|
||||
creds: aws.Credentials{
|
||||
AccessKeyID: cfg.AdminAccessKey,
|
||||
SecretAccessKey: cfg.AdminSecretKey,
|
||||
},
|
||||
http: &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: cfg.InsecureTLS}, //nolint:gosec
|
||||
},
|
||||
},
|
||||
signer: v4.NewSigner(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) CreateUser(ctx context.Context, account Account) error {
|
||||
body, err := xml.Marshal(account)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal create-user request: %w", err)
|
||||
}
|
||||
|
||||
_, err = c.do(ctx, http.MethodPatch, "/create-user", nil, body)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) UpdateUser(ctx context.Context, access string, props MutableProps) error {
|
||||
body, err := xml.Marshal(props)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal update-user request: %w", err)
|
||||
}
|
||||
|
||||
query := url.Values{"access": []string{access}}
|
||||
_, err = c.do(ctx, http.MethodPatch, "/update-user", query, body)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) DeleteUser(ctx context.Context, access string) error {
|
||||
query := url.Values{"access": []string{access}}
|
||||
_, err := c.do(ctx, http.MethodPatch, "/delete-user", query, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) GetUser(ctx context.Context, access string) (Account, bool, error) {
|
||||
accounts, err := c.ListUsers(ctx)
|
||||
if err != nil {
|
||||
return Account{}, false, err
|
||||
}
|
||||
for _, account := range accounts {
|
||||
if account.Access == access {
|
||||
return account, true, nil
|
||||
}
|
||||
}
|
||||
return Account{}, false, nil
|
||||
}
|
||||
|
||||
func (c *Client) ListUsers(ctx context.Context) ([]Account, error) {
|
||||
body, err := c.do(ctx, http.MethodPatch, "/list-users", nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result ListUserAccountsResult
|
||||
if err := xml.Unmarshal(body, &result); err != nil {
|
||||
return nil, fmt.Errorf("parse list-users response: %w", err)
|
||||
}
|
||||
return result.Accounts, nil
|
||||
}
|
||||
|
||||
func (c *Client) do(ctx context.Context, method, path string, query url.Values, body []byte) ([]byte, error) {
|
||||
endpoint := *c.endpoint
|
||||
endpoint.Path = strings.TrimRight(c.endpoint.Path, "/") + path
|
||||
endpoint.RawQuery = query.Encode()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("build request: %w", err)
|
||||
}
|
||||
|
||||
hashedPayload := sha256.Sum256(body)
|
||||
hexPayload := hex.EncodeToString(hashedPayload[:])
|
||||
req.Header.Set("X-Amz-Content-Sha256", hexPayload)
|
||||
if len(body) > 0 {
|
||||
req.Header.Set("Content-Type", "application/xml")
|
||||
}
|
||||
|
||||
if err := c.signer.SignHTTP(ctx, c.creds, req, hexPayload, "s3", c.region, time.Now()); err != nil {
|
||||
return nil, fmt.Errorf("sign request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("send request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read response: %w", err)
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
return nil, parseError(resp.StatusCode, respBody)
|
||||
}
|
||||
|
||||
return respBody, nil
|
||||
}
|
||||
|
||||
func parseError(status int, body []byte) error {
|
||||
var apiErr ErrorResponse
|
||||
if err := xml.Unmarshal(body, &apiErr); err != nil {
|
||||
return &APIError{
|
||||
Status: status,
|
||||
RawBody: strings.TrimSpace(string(body)),
|
||||
Message: strings.TrimSpace(string(body)),
|
||||
}
|
||||
}
|
||||
if apiErr.Code == "" && apiErr.Message == "" {
|
||||
return &APIError{
|
||||
Status: status,
|
||||
RawBody: strings.TrimSpace(string(body)),
|
||||
Message: strings.TrimSpace(string(body)),
|
||||
}
|
||||
}
|
||||
return &APIError{
|
||||
Status: status,
|
||||
Code: apiErr.Code,
|
||||
Message: apiErr.Message,
|
||||
RequestID: apiErr.RequestID,
|
||||
HostID: apiErr.HostID,
|
||||
RawBody: strings.TrimSpace(string(body)),
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestListUserAccountsResultUnmarshal(t *testing.T) {
|
||||
body := []byte(`<ListUserAccountsResult>
|
||||
<Accounts>
|
||||
<Access>canary</Access>
|
||||
<Role>user</Role>
|
||||
<UserID>0</UserID>
|
||||
<GroupID>0</GroupID>
|
||||
<ProjectID>0</ProjectID>
|
||||
</Accounts>
|
||||
</ListUserAccountsResult>`)
|
||||
|
||||
var result ListUserAccountsResult
|
||||
if err := xml.Unmarshal(body, &result); err != nil {
|
||||
t.Fatalf("unmarshal list-users response: %v", err)
|
||||
}
|
||||
if len(result.Accounts) != 1 {
|
||||
t.Fatalf("expected 1 account, got %d", len(result.Accounts))
|
||||
}
|
||||
if result.Accounts[0].Access != "canary" {
|
||||
t.Fatalf("expected canary account, got %q", result.Accounts[0].Access)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLiveListUsers(t *testing.T) {
|
||||
endpoint := os.Getenv("VERSITYGW_ENDPOINT")
|
||||
accessKey := os.Getenv("VERSITYGW_ADMIN_ACCESS_KEY")
|
||||
secretKey := os.Getenv("VERSITYGW_ADMIN_SECRET_KEY")
|
||||
if endpoint == "" || accessKey == "" || secretKey == "" {
|
||||
t.Skip("VERSITYGW_ENDPOINT, VERSITYGW_ADMIN_ACCESS_KEY, and VERSITYGW_ADMIN_SECRET_KEY are required")
|
||||
}
|
||||
|
||||
api, err := New(Config{
|
||||
Endpoint: endpoint,
|
||||
Region: envOrDefault("VERSITYGW_REGION", "us-east-1"),
|
||||
AdminAccessKey: accessKey,
|
||||
AdminSecretKey: secretKey,
|
||||
InsecureTLS: os.Getenv("VERSITYGW_INSECURE_TLS") == "true",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("new client: %v", err)
|
||||
}
|
||||
|
||||
accounts, err := api.ListUsers(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("list users: %v", err)
|
||||
}
|
||||
|
||||
for _, account := range accounts {
|
||||
t.Logf("access=%s role=%s user_id=%d group_id=%d project_id=%d", account.Access, account.Role, account.UserID, account.GroupID, account.ProjectID)
|
||||
}
|
||||
}
|
||||
|
||||
func envOrDefault(name, fallback string) string {
|
||||
value := os.Getenv(name)
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||
"github.com/hashicorp/terraform-plugin-framework/provider"
|
||||
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
"github.com/quorate/terraform-provider-versitygw/internal/client"
|
||||
)
|
||||
|
||||
var _ provider.Provider = (*Provider)(nil)
|
||||
|
||||
func New() provider.Provider {
|
||||
return &Provider{}
|
||||
}
|
||||
|
||||
type Provider struct{}
|
||||
|
||||
type providerModel struct {
|
||||
Endpoint types.String `tfsdk:"endpoint"`
|
||||
Region types.String `tfsdk:"region"`
|
||||
AdminAccessKey types.String `tfsdk:"admin_access_key"`
|
||||
AdminSecretKey types.String `tfsdk:"admin_secret_key"`
|
||||
InsecureTLS types.Bool `tfsdk:"insecure_tls"`
|
||||
}
|
||||
|
||||
func (p *Provider) Metadata(_ context.Context, _ provider.MetadataRequest, resp *provider.MetadataResponse) {
|
||||
resp.TypeName = "versitygw"
|
||||
}
|
||||
|
||||
func (p *Provider) Schema(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"endpoint": schema.StringAttribute{
|
||||
Optional: true,
|
||||
Description: "VersityGW admin API endpoint. May also be set with VERSITYGW_ENDPOINT.",
|
||||
},
|
||||
"region": schema.StringAttribute{
|
||||
Optional: true,
|
||||
Description: "SigV4 region. Defaults to us-east-1. May also be set with VERSITYGW_REGION.",
|
||||
},
|
||||
"admin_access_key": schema.StringAttribute{
|
||||
Optional: true,
|
||||
Sensitive: true,
|
||||
Description: "VersityGW admin/root access key. May also be set with VERSITYGW_ADMIN_ACCESS_KEY.",
|
||||
},
|
||||
"admin_secret_key": schema.StringAttribute{
|
||||
Optional: true,
|
||||
Sensitive: true,
|
||||
Description: "VersityGW admin/root secret key. May also be set with VERSITYGW_ADMIN_SECRET_KEY.",
|
||||
},
|
||||
"insecure_tls": schema.BoolAttribute{
|
||||
Optional: true,
|
||||
Description: "Disable TLS certificate verification for the admin endpoint. May also be set with VERSITYGW_INSECURE_TLS=true.",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
|
||||
var cfg providerModel
|
||||
resp.Diagnostics.Append(req.Config.Get(ctx, &cfg)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
endpoint := stringValue(cfg.Endpoint, os.Getenv("VERSITYGW_ENDPOINT"))
|
||||
region := stringValue(cfg.Region, os.Getenv("VERSITYGW_REGION"))
|
||||
if region == "" {
|
||||
region = "us-east-1"
|
||||
}
|
||||
adminAccessKey := stringValue(cfg.AdminAccessKey, os.Getenv("VERSITYGW_ADMIN_ACCESS_KEY"))
|
||||
adminSecretKey := stringValue(cfg.AdminSecretKey, os.Getenv("VERSITYGW_ADMIN_SECRET_KEY"))
|
||||
insecureTLS := boolValue(cfg.InsecureTLS, os.Getenv("VERSITYGW_INSECURE_TLS") == "true")
|
||||
|
||||
if endpoint == "" {
|
||||
resp.Diagnostics.AddAttributeError(path.Root("endpoint"), "Missing VersityGW endpoint", "Set endpoint in provider configuration or VERSITYGW_ENDPOINT.")
|
||||
}
|
||||
if adminAccessKey == "" {
|
||||
resp.Diagnostics.AddAttributeError(path.Root("admin_access_key"), "Missing admin access key", "Set admin_access_key in provider configuration or VERSITYGW_ADMIN_ACCESS_KEY.")
|
||||
}
|
||||
if adminSecretKey == "" {
|
||||
resp.Diagnostics.AddAttributeError(path.Root("admin_secret_key"), "Missing admin secret key", "Set admin_secret_key in provider configuration or VERSITYGW_ADMIN_SECRET_KEY.")
|
||||
}
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
api, err := client.New(client.Config{
|
||||
Endpoint: endpoint,
|
||||
Region: region,
|
||||
AdminAccessKey: adminAccessKey,
|
||||
AdminSecretKey: adminSecretKey,
|
||||
InsecureTLS: insecureTLS,
|
||||
})
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Invalid VersityGW provider configuration", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.ResourceData = api
|
||||
}
|
||||
|
||||
func (p *Provider) Resources(_ context.Context) []func() resource.Resource {
|
||||
return []func() resource.Resource{
|
||||
NewUserResource,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource {
|
||||
return nil
|
||||
}
|
||||
|
||||
func stringValue(value types.String, fallback string) string {
|
||||
if value.IsNull() || value.IsUnknown() {
|
||||
return fallback
|
||||
}
|
||||
return value.ValueString()
|
||||
}
|
||||
|
||||
func boolValue(value types.Bool, fallback bool) bool {
|
||||
if value.IsNull() || value.IsUnknown() {
|
||||
return fallback
|
||||
}
|
||||
return value.ValueBool()
|
||||
}
|
||||
|
|
@ -0,0 +1,290 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"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/int64planmodifier"
|
||||
"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"
|
||||
"github.com/quorate/terraform-provider-versitygw/internal/client"
|
||||
)
|
||||
|
||||
var (
|
||||
_ resource.Resource = (*UserResource)(nil)
|
||||
_ resource.ResourceWithConfigure = (*UserResource)(nil)
|
||||
)
|
||||
|
||||
func NewUserResource() resource.Resource {
|
||||
return &UserResource{}
|
||||
}
|
||||
|
||||
type UserResource struct {
|
||||
client *client.Client
|
||||
}
|
||||
|
||||
type userModel struct {
|
||||
ID types.String `tfsdk:"id"`
|
||||
AccessKeyID types.String `tfsdk:"access_key_id"`
|
||||
SecretAccessKey types.String `tfsdk:"secret_access_key"`
|
||||
Role types.String `tfsdk:"role"`
|
||||
UserID types.Int64 `tfsdk:"user_id"`
|
||||
GroupID types.Int64 `tfsdk:"group_id"`
|
||||
ProjectID types.Int64 `tfsdk:"project_id"`
|
||||
}
|
||||
|
||||
func (r *UserResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_user"
|
||||
}
|
||||
|
||||
func (r *UserResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "VersityGW IAM user managed through the VersityGW admin API.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"id": schema.StringAttribute{
|
||||
Computed: true,
|
||||
Description: "User access key ID.",
|
||||
},
|
||||
"access_key_id": schema.StringAttribute{
|
||||
Required: true,
|
||||
Description: "VersityGW username/access key ID.",
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
"secret_access_key": schema.StringAttribute{
|
||||
Required: true,
|
||||
Sensitive: true,
|
||||
WriteOnly: true,
|
||||
Description: "VersityGW user secret access key.",
|
||||
},
|
||||
"role": schema.StringAttribute{
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Description: "VersityGW user role: user, userplus, or admin. Defaults to user.",
|
||||
},
|
||||
"user_id": schema.Int64Attribute{
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Description: "Optional POSIX user ID stored on the VersityGW user.",
|
||||
PlanModifiers: []planmodifier.Int64{
|
||||
int64planmodifier.UseStateForUnknown(),
|
||||
},
|
||||
},
|
||||
"group_id": schema.Int64Attribute{
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Description: "Optional POSIX group ID stored on the VersityGW user.",
|
||||
PlanModifiers: []planmodifier.Int64{
|
||||
int64planmodifier.UseStateForUnknown(),
|
||||
},
|
||||
},
|
||||
"project_id": schema.Int64Attribute{
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Description: "Optional project ID stored on the VersityGW user.",
|
||||
PlanModifiers: []planmodifier.Int64{
|
||||
int64planmodifier.UseStateForUnknown(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *UserResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
api, ok := req.ProviderData.(*client.Client)
|
||||
if !ok {
|
||||
resp.Diagnostics.AddError("Unexpected provider data", fmt.Sprintf("Expected *client.Client, got %T", req.ProviderData))
|
||||
return
|
||||
}
|
||||
r.client = api
|
||||
}
|
||||
|
||||
func (r *UserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||
var plan userModel
|
||||
var config userModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
if !validateUser(plan, config.SecretAccessKey, &resp.Diagnostics) {
|
||||
return
|
||||
}
|
||||
|
||||
role := valueOrDefault(plan.Role, "user")
|
||||
account := client.Account{
|
||||
Access: plan.AccessKeyID.ValueString(),
|
||||
Secret: config.SecretAccessKey.ValueString(),
|
||||
Role: role,
|
||||
UserID: int64OrZero(plan.UserID),
|
||||
GroupID: int64OrZero(plan.GroupID),
|
||||
ProjectID: int64OrZero(plan.ProjectID),
|
||||
}
|
||||
|
||||
if err := r.client.CreateUser(ctx, account); err != nil {
|
||||
if !client.IsErrorCode(err, "XAdminUserExists") {
|
||||
resp.Diagnostics.AddError("Create VersityGW user failed", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
secret := account.Secret
|
||||
props := client.MutableProps{
|
||||
Secret: &secret,
|
||||
Role: account.Role,
|
||||
UserID: &account.UserID,
|
||||
GroupID: &account.GroupID,
|
||||
ProjectID: &account.ProjectID,
|
||||
}
|
||||
if err := r.client.UpdateUser(ctx, account.Access, props); err != nil {
|
||||
resp.Diagnostics.AddError("Adopt existing VersityGW user failed", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
plan.ID = plan.AccessKeyID
|
||||
plan.Role = types.StringValue(role)
|
||||
plan.UserID = types.Int64Value(account.UserID)
|
||||
plan.GroupID = types.Int64Value(account.GroupID)
|
||||
plan.ProjectID = types.Int64Value(account.ProjectID)
|
||||
plan.SecretAccessKey = types.StringNull()
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||
}
|
||||
|
||||
func (r *UserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||
var state userModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
account, found, err := r.client.GetUser(ctx, state.AccessKeyID.ValueString())
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Read VersityGW user failed", err.Error())
|
||||
return
|
||||
}
|
||||
if !found {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
state.ID = types.StringValue(account.Access)
|
||||
state.AccessKeyID = types.StringValue(account.Access)
|
||||
state.Role = types.StringValue(account.Role)
|
||||
state.UserID = types.Int64Value(account.UserID)
|
||||
state.GroupID = types.Int64Value(account.GroupID)
|
||||
state.ProjectID = types.Int64Value(account.ProjectID)
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
|
||||
}
|
||||
|
||||
func (r *UserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||
var plan userModel
|
||||
var state userModel
|
||||
var config userModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
if !validateUser(plan, config.SecretAccessKey, &resp.Diagnostics) {
|
||||
return
|
||||
}
|
||||
|
||||
role := valueOrDefault(plan.Role, "user")
|
||||
secret := config.SecretAccessKey.ValueString()
|
||||
userID := int64OrZero(plan.UserID)
|
||||
groupID := int64OrZero(plan.GroupID)
|
||||
projectID := int64OrZero(plan.ProjectID)
|
||||
|
||||
props := client.MutableProps{
|
||||
Secret: &secret,
|
||||
Role: role,
|
||||
UserID: &userID,
|
||||
GroupID: &groupID,
|
||||
ProjectID: &projectID,
|
||||
}
|
||||
|
||||
if err := r.client.UpdateUser(ctx, state.AccessKeyID.ValueString(), props); err != nil {
|
||||
resp.Diagnostics.AddError("Update VersityGW user failed", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
plan.ID = plan.AccessKeyID
|
||||
plan.Role = types.StringValue(role)
|
||||
plan.UserID = types.Int64Value(userID)
|
||||
plan.GroupID = types.Int64Value(groupID)
|
||||
plan.ProjectID = types.Int64Value(projectID)
|
||||
plan.SecretAccessKey = types.StringNull()
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||
}
|
||||
|
||||
func (r *UserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||
var state userModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.client.DeleteUser(ctx, state.AccessKeyID.ValueString()); err != nil {
|
||||
resp.Diagnostics.AddError("Delete VersityGW user failed", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func validateUser(model userModel, secretAccessKey types.String, diags interface {
|
||||
AddAttributeError(path.Path, string, string)
|
||||
}) bool {
|
||||
ok := true
|
||||
if model.AccessKeyID.IsNull() || model.AccessKeyID.IsUnknown() || model.AccessKeyID.ValueString() == "" {
|
||||
diags.AddAttributeError(path.Root("access_key_id"), "Invalid access key ID", "access_key_id must be non-empty.")
|
||||
ok = false
|
||||
}
|
||||
if secretAccessKey.IsNull() || secretAccessKey.IsUnknown() || secretAccessKey.ValueString() == "" {
|
||||
diags.AddAttributeError(path.Root("secret_access_key"), "Invalid secret access key", "secret_access_key must be non-empty.")
|
||||
ok = false
|
||||
}
|
||||
role := valueOrDefault(model.Role, "user")
|
||||
if role != "user" && role != "userplus" && role != "admin" {
|
||||
diags.AddAttributeError(path.Root("role"), "Invalid role", "role must be one of: user, userplus, admin.")
|
||||
ok = false
|
||||
}
|
||||
if isNegative(model.UserID) {
|
||||
diags.AddAttributeError(path.Root("user_id"), "Invalid user ID", "user_id must be non-negative.")
|
||||
ok = false
|
||||
}
|
||||
if isNegative(model.GroupID) {
|
||||
diags.AddAttributeError(path.Root("group_id"), "Invalid group ID", "group_id must be non-negative.")
|
||||
ok = false
|
||||
}
|
||||
if isNegative(model.ProjectID) {
|
||||
diags.AddAttributeError(path.Root("project_id"), "Invalid project ID", "project_id must be non-negative.")
|
||||
ok = false
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
func valueOrDefault(value types.String, fallback string) string {
|
||||
if value.IsNull() || value.IsUnknown() || value.ValueString() == "" {
|
||||
return fallback
|
||||
}
|
||||
return value.ValueString()
|
||||
}
|
||||
|
||||
func int64OrZero(value types.Int64) int64 {
|
||||
if value.IsNull() || value.IsUnknown() {
|
||||
return 0
|
||||
}
|
||||
return value.ValueInt64()
|
||||
}
|
||||
|
||||
func isNegative(value types.Int64) bool {
|
||||
return !value.IsNull() && !value.IsUnknown() && value.ValueInt64() < 0
|
||||
}
|
||||
18
packages/versitygw-terraform-provider/main.go
Normal file
18
packages/versitygw-terraform-provider/main.go
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/providerserver"
|
||||
"github.com/quorate/terraform-provider-versitygw/internal/provider"
|
||||
)
|
||||
|
||||
func main() {
|
||||
err := providerserver.Serve(context.Background(), provider.New, providerserver.ServeOpts{
|
||||
Address: "registry.opentofu.org/quorate/versitygw",
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue