mirror of
https://github.com/azukaar/Cosmos-Server.git
synced 2026-10-11 21:49:51 +00:00
[release] v0.22.0-unstable05
This commit is contained in:
@@ -209,6 +209,20 @@ jobs:
|
||||
git tag "go-sdk/v${VERSION}" 2>/dev/null || true
|
||||
git push origin "go-sdk/v${VERSION}" 2>/dev/null || true
|
||||
|
||||
- run:
|
||||
name: Build and attach Terraform provider to release
|
||||
command: |
|
||||
VERSION=$(jq -r .version package.json)
|
||||
cd terraform-provider-cosmos
|
||||
for PAIR in linux/amd64 linux/arm64 darwin/amd64 darwin/arm64; do
|
||||
OS="${PAIR%/*}"
|
||||
ARCH="${PAIR#*/}"
|
||||
GOOS=$OS GOARCH=$ARCH go build -ldflags "-s -w -X main.version=${VERSION}" -o "terraform-provider-cosmos_${OS}_${ARCH}"
|
||||
zip "terraform-provider-cosmos_${VERSION}_${OS}_${ARCH}.zip" "terraform-provider-cosmos_${OS}_${ARCH}"
|
||||
rm "terraform-provider-cosmos_${OS}_${ARCH}"
|
||||
done
|
||||
gh release upload "v${VERSION}" terraform-provider-cosmos_${VERSION}_*.zip --clobber
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build-all:
|
||||
|
||||
+8
-1
@@ -30,4 +30,11 @@ test-backup
|
||||
/backups
|
||||
ca.pem
|
||||
sdk/dist
|
||||
sdk/node_modules
|
||||
sdk/node_modules
|
||||
|
||||
.terraform
|
||||
terraform.tfstate
|
||||
terraform.tfstate.*
|
||||
.terraform.lock.hcl
|
||||
*.log
|
||||
main.tf
|
||||
|
||||
+101
-1
@@ -804,6 +804,99 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/backups-config": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Returns a map of all backup configurations from the server config, keyed by backup name.",
|
||||
"tags": [
|
||||
"Backups"
|
||||
],
|
||||
"summary": "List all backup configurations",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/utils.APIResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/utils.HTTPErrorResult"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/backups-config/{name}": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Returns a single backup configuration from the server config.",
|
||||
"tags": [
|
||||
"Backups"
|
||||
],
|
||||
"summary": "Get a single backup configuration by name",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Backup name",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/utils.APIResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/utils.HTTPErrorResult"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/utils.HTTPErrorResult"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/backups-repository": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -9582,6 +9675,12 @@
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"permissions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/utils.Permission"
|
||||
}
|
||||
},
|
||||
"readOnly": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -9735,7 +9834,8 @@
|
||||
"type": "object",
|
||||
"required": [
|
||||
"deviceName",
|
||||
"ip"
|
||||
"ip",
|
||||
"nickname"
|
||||
],
|
||||
"properties": {
|
||||
"PublicHostname": {
|
||||
|
||||
+85
-1
@@ -682,6 +682,83 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/backups-config": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Returns a map of all backup configurations from the server config, keyed by backup name.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Backups"
|
||||
],
|
||||
"summary": "List all backup configurations",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/utils.APIResponse"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/utils.HTTPErrorResult"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/backups-config/{name}": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Returns a single backup configuration from the server config.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Backups"
|
||||
],
|
||||
"summary": "Get a single backup configuration by name",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Backup name",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/utils.APIResponse"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/utils.HTTPErrorResult"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/utils.HTTPErrorResult"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/backups-repository": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -7839,6 +7916,12 @@
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"permissions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/utils.Permission"
|
||||
}
|
||||
},
|
||||
"readOnly": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -7992,7 +8075,8 @@
|
||||
"type": "object",
|
||||
"required": [
|
||||
"deviceName",
|
||||
"ip"
|
||||
"ip",
|
||||
"nickname"
|
||||
],
|
||||
"properties": {
|
||||
"PublicHostname": {
|
||||
|
||||
@@ -10,6 +10,10 @@ definitions:
|
||||
type: array
|
||||
name:
|
||||
type: string
|
||||
permissions:
|
||||
items:
|
||||
$ref: '#/definitions/utils.Permission'
|
||||
type: array
|
||||
readOnly:
|
||||
type: boolean
|
||||
restrictToConstellation:
|
||||
@@ -145,6 +149,7 @@ definitions:
|
||||
required:
|
||||
- deviceName
|
||||
- ip
|
||||
- nickname
|
||||
type: object
|
||||
constellation.DeviceEditRequestJSON:
|
||||
properties:
|
||||
@@ -2349,6 +2354,55 @@ paths:
|
||||
summary: Create a new backup configuration
|
||||
tags:
|
||||
- Backups
|
||||
/api/backups-config:
|
||||
get:
|
||||
description: Returns a map of all backup configurations from the server config,
|
||||
keyed by backup name.
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/utils.APIResponse'
|
||||
"403":
|
||||
description: Forbidden
|
||||
schema:
|
||||
$ref: '#/definitions/utils.HTTPErrorResult'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: List all backup configurations
|
||||
tags:
|
||||
- Backups
|
||||
/api/backups-config/{name}:
|
||||
get:
|
||||
description: Returns a single backup configuration from the server config.
|
||||
parameters:
|
||||
- description: Backup name
|
||||
in: path
|
||||
name: name
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/utils.APIResponse'
|
||||
"403":
|
||||
description: Forbidden
|
||||
schema:
|
||||
$ref: '#/definitions/utils.HTTPErrorResult'
|
||||
"404":
|
||||
description: Not Found
|
||||
schema:
|
||||
$ref: '#/definitions/utils.HTTPErrorResult'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: Get a single backup configuration by name
|
||||
tags:
|
||||
- Backups
|
||||
/api/backups-repository:
|
||||
get:
|
||||
produces:
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
## Version 0.22
|
||||
- Adding feature to create API key to directly query the Cosmos API
|
||||
- Added GO/TS SDK to call the API from any project (AI generated)
|
||||
- Added GO/TS/Terraform SDK to call the API from any project (AI generated)
|
||||
- Refactor the entire permission system for future work on roles and fine-grained permissions (partially AI assisted)
|
||||
- You can now create multiple admin users
|
||||
- Fixed issue with post-install on compose import
|
||||
@@ -10,6 +10,7 @@
|
||||
- Fix issue with Constellation sometime not connecting right away on new network
|
||||
- Sync more settings accross servers in Constellations (Rclone and api token/permissions configs)
|
||||
- Fix issue with /logout not clearing cookies normally
|
||||
- Fix issue with deleting backups
|
||||
|
||||
## Version 0.21.7
|
||||
- Fix issue with Constellation routing HTTP tunnels to the wrong protocol
|
||||
|
||||
+524
-287
@@ -219,11 +219,12 @@ func (e GetApiSnapraidNameActionParamsAction) Valid() bool {
|
||||
|
||||
// ConfigapiCreateAPITokenRequest defines model for configapi.CreateAPITokenRequest.
|
||||
type ConfigapiCreateAPITokenRequest struct {
|
||||
Description *string `json:"description,omitempty"`
|
||||
IpWhitelist *[]string `json:"ipWhitelist,omitempty"`
|
||||
Name string `json:"name"`
|
||||
ReadOnly *bool `json:"readOnly,omitempty"`
|
||||
RestrictToConstellation *bool `json:"restrictToConstellation,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
IpWhitelist *[]string `json:"ipWhitelist,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Permissions *[]UtilsPermission `json:"permissions,omitempty"`
|
||||
ReadOnly *bool `json:"readOnly,omitempty"`
|
||||
RestrictToConstellation *bool `json:"restrictToConstellation,omitempty"`
|
||||
}
|
||||
|
||||
// ConfigapiDNSConfigRequest defines model for configapi.DNSConfigRequest.
|
||||
@@ -295,7 +296,7 @@ type ConstellationDeviceCreateRequestJSON struct {
|
||||
IsRelay *bool `json:"isRelay,omitempty"`
|
||||
|
||||
// Nickname for devices only
|
||||
Nickname *string `json:"nickname,omitempty"`
|
||||
Nickname string `json:"nickname"`
|
||||
Port *string `json:"port,omitempty"`
|
||||
PublicKey *string `json:"publicKey,omitempty"`
|
||||
}
|
||||
@@ -1768,6 +1769,12 @@ type ClientInterface interface {
|
||||
|
||||
PostApiBackups(ctx context.Context, body PostApiBackupsJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error)
|
||||
|
||||
// GetApiBackupsConfig request
|
||||
GetApiBackupsConfig(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error)
|
||||
|
||||
// GetApiBackupsConfigName request
|
||||
GetApiBackupsConfigName(ctx context.Context, name string, reqEditors ...RequestEditorFn) (*http.Response, error)
|
||||
|
||||
// GetApiBackupsRepository request
|
||||
GetApiBackupsRepository(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error)
|
||||
|
||||
@@ -2545,6 +2552,30 @@ func (c *Client) PostApiBackups(ctx context.Context, body PostApiBackupsJSONRequ
|
||||
return c.Client.Do(req)
|
||||
}
|
||||
|
||||
func (c *Client) GetApiBackupsConfig(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) {
|
||||
req, err := NewGetApiBackupsConfigRequest(c.Server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
if err := c.applyEditors(ctx, req, reqEditors); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.Client.Do(req)
|
||||
}
|
||||
|
||||
func (c *Client) GetApiBackupsConfigName(ctx context.Context, name string, reqEditors ...RequestEditorFn) (*http.Response, error) {
|
||||
req, err := NewGetApiBackupsConfigNameRequest(c.Server, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
if err := c.applyEditors(ctx, req, reqEditors); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.Client.Do(req)
|
||||
}
|
||||
|
||||
func (c *Client) GetApiBackupsRepository(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) {
|
||||
req, err := NewGetApiBackupsRepositoryRequest(c.Server)
|
||||
if err != nil {
|
||||
@@ -5318,6 +5349,67 @@ func NewPostApiBackupsRequestWithBody(server string, contentType string, body io
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// NewGetApiBackupsConfigRequest generates requests for GetApiBackupsConfig
|
||||
func NewGetApiBackupsConfigRequest(server string) (*http.Request, error) {
|
||||
var err error
|
||||
|
||||
serverURL, err := url.Parse(server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
operationPath := fmt.Sprintf("/api/backups-config")
|
||||
if operationPath[0] == '/' {
|
||||
operationPath = "." + operationPath
|
||||
}
|
||||
|
||||
queryURL, err := serverURL.Parse(operationPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", queryURL.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// NewGetApiBackupsConfigNameRequest generates requests for GetApiBackupsConfigName
|
||||
func NewGetApiBackupsConfigNameRequest(server string, name string) (*http.Request, error) {
|
||||
var err error
|
||||
|
||||
var pathParam0 string
|
||||
|
||||
pathParam0, err = runtime.StyleParamWithOptions("simple", false, "name", name, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
serverURL, err := url.Parse(server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
operationPath := fmt.Sprintf("/api/backups-config/%s", pathParam0)
|
||||
if operationPath[0] == '/' {
|
||||
operationPath = "." + operationPath
|
||||
}
|
||||
|
||||
queryURL, err := serverURL.Parse(operationPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", queryURL.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// NewGetApiBackupsRepositoryRequest generates requests for GetApiBackupsRepository
|
||||
func NewGetApiBackupsRepositoryRequest(server string) (*http.Request, error) {
|
||||
var err error
|
||||
@@ -10807,6 +10899,12 @@ type ClientWithResponsesInterface interface {
|
||||
|
||||
PostApiBackupsWithResponse(ctx context.Context, body PostApiBackupsJSONRequestBody, reqEditors ...RequestEditorFn) (*PostApiBackupsResponse, error)
|
||||
|
||||
// GetApiBackupsConfigWithResponse request
|
||||
GetApiBackupsConfigWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetApiBackupsConfigResponse, error)
|
||||
|
||||
// GetApiBackupsConfigNameWithResponse request
|
||||
GetApiBackupsConfigNameWithResponse(ctx context.Context, name string, reqEditors ...RequestEditorFn) (*GetApiBackupsConfigNameResponse, error)
|
||||
|
||||
// GetApiBackupsRepositoryWithResponse request
|
||||
GetApiBackupsRepositoryWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetApiBackupsRepositoryResponse, error)
|
||||
|
||||
@@ -11631,6 +11729,53 @@ func (r PostApiBackupsResponse) StatusCode() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
type GetApiBackupsConfigResponse struct {
|
||||
Body []byte
|
||||
HTTPResponse *http.Response
|
||||
JSON200 *UtilsAPIResponse
|
||||
JSON403 *UtilsHTTPErrorResult
|
||||
}
|
||||
|
||||
// Status returns HTTPResponse.Status
|
||||
func (r GetApiBackupsConfigResponse) Status() string {
|
||||
if r.HTTPResponse != nil {
|
||||
return r.HTTPResponse.Status
|
||||
}
|
||||
return http.StatusText(0)
|
||||
}
|
||||
|
||||
// StatusCode returns HTTPResponse.StatusCode
|
||||
func (r GetApiBackupsConfigResponse) StatusCode() int {
|
||||
if r.HTTPResponse != nil {
|
||||
return r.HTTPResponse.StatusCode
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type GetApiBackupsConfigNameResponse struct {
|
||||
Body []byte
|
||||
HTTPResponse *http.Response
|
||||
JSON200 *UtilsAPIResponse
|
||||
JSON403 *UtilsHTTPErrorResult
|
||||
JSON404 *UtilsHTTPErrorResult
|
||||
}
|
||||
|
||||
// Status returns HTTPResponse.Status
|
||||
func (r GetApiBackupsConfigNameResponse) Status() string {
|
||||
if r.HTTPResponse != nil {
|
||||
return r.HTTPResponse.Status
|
||||
}
|
||||
return http.StatusText(0)
|
||||
}
|
||||
|
||||
// StatusCode returns HTTPResponse.StatusCode
|
||||
func (r GetApiBackupsConfigNameResponse) StatusCode() int {
|
||||
if r.HTTPResponse != nil {
|
||||
return r.HTTPResponse.StatusCode
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type GetApiBackupsRepositoryResponse struct {
|
||||
Body []byte
|
||||
HTTPResponse *http.Response
|
||||
@@ -15270,6 +15415,24 @@ func (c *ClientWithResponses) PostApiBackupsWithResponse(ctx context.Context, bo
|
||||
return ParsePostApiBackupsResponse(rsp)
|
||||
}
|
||||
|
||||
// GetApiBackupsConfigWithResponse request returning *GetApiBackupsConfigResponse
|
||||
func (c *ClientWithResponses) GetApiBackupsConfigWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetApiBackupsConfigResponse, error) {
|
||||
rsp, err := c.GetApiBackupsConfig(ctx, reqEditors...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ParseGetApiBackupsConfigResponse(rsp)
|
||||
}
|
||||
|
||||
// GetApiBackupsConfigNameWithResponse request returning *GetApiBackupsConfigNameResponse
|
||||
func (c *ClientWithResponses) GetApiBackupsConfigNameWithResponse(ctx context.Context, name string, reqEditors ...RequestEditorFn) (*GetApiBackupsConfigNameResponse, error) {
|
||||
rsp, err := c.GetApiBackupsConfigName(ctx, name, reqEditors...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ParseGetApiBackupsConfigNameResponse(rsp)
|
||||
}
|
||||
|
||||
// GetApiBackupsRepositoryWithResponse request returning *GetApiBackupsRepositoryResponse
|
||||
func (c *ClientWithResponses) GetApiBackupsRepositoryWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetApiBackupsRepositoryResponse, error) {
|
||||
rsp, err := c.GetApiBackupsRepository(ctx, reqEditors...)
|
||||
@@ -17500,6 +17663,79 @@ func ParsePostApiBackupsResponse(rsp *http.Response) (*PostApiBackupsResponse, e
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// ParseGetApiBackupsConfigResponse parses an HTTP response from a GetApiBackupsConfigWithResponse call
|
||||
func ParseGetApiBackupsConfigResponse(rsp *http.Response) (*GetApiBackupsConfigResponse, error) {
|
||||
bodyBytes, err := io.ReadAll(rsp.Body)
|
||||
defer func() { _ = rsp.Body.Close() }()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := &GetApiBackupsConfigResponse{
|
||||
Body: bodyBytes,
|
||||
HTTPResponse: rsp,
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
|
||||
var dest UtilsAPIResponse
|
||||
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.JSON200 = &dest
|
||||
|
||||
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403:
|
||||
var dest UtilsHTTPErrorResult
|
||||
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.JSON403 = &dest
|
||||
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// ParseGetApiBackupsConfigNameResponse parses an HTTP response from a GetApiBackupsConfigNameWithResponse call
|
||||
func ParseGetApiBackupsConfigNameResponse(rsp *http.Response) (*GetApiBackupsConfigNameResponse, error) {
|
||||
bodyBytes, err := io.ReadAll(rsp.Body)
|
||||
defer func() { _ = rsp.Body.Close() }()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := &GetApiBackupsConfigNameResponse{
|
||||
Body: bodyBytes,
|
||||
HTTPResponse: rsp,
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
|
||||
var dest UtilsAPIResponse
|
||||
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.JSON200 = &dest
|
||||
|
||||
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403:
|
||||
var dest UtilsHTTPErrorResult
|
||||
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.JSON403 = &dest
|
||||
|
||||
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404:
|
||||
var dest UtilsHTTPErrorResult
|
||||
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.JSON404 = &dest
|
||||
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// ParseGetApiBackupsRepositoryResponse parses an HTTP response from a GetApiBackupsRepositoryWithResponse call
|
||||
func ParseGetApiBackupsRepositoryResponse(rsp *http.Response) (*GetApiBackupsRepositoryResponse, error) {
|
||||
bodyBytes, err := io.ReadAll(rsp.Body)
|
||||
@@ -23523,287 +23759,288 @@ func ParsePostRcloneVfsStatsResponse(rsp *http.Response) (*PostRcloneVfsStatsRes
|
||||
// Base64 encoded, gzipped, json marshaled Swagger object
|
||||
var swaggerSpec = []string{
|
||||
|
||||
"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",
|
||||
"AP//FbMekod3AgA=",
|
||||
"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",
|
||||
"Y9MefP309f8FAAD///hIZQT7fAIA",
|
||||
}
|
||||
|
||||
// GetSwagger returns the content of the embedded swagger specification file
|
||||
|
||||
+77
-1
@@ -10,6 +10,82 @@ import (
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
)
|
||||
|
||||
// ListBackupConfigsRoute godoc
|
||||
// @Summary List all backup configurations
|
||||
// @Description Returns a map of all backup configurations from the server config, keyed by backup name.
|
||||
// @Tags Backups
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Router /api/backups-config [get]
|
||||
func ListBackupConfigsRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if req.Method == "GET" {
|
||||
config := utils.GetMainConfig()
|
||||
backups := config.Backup.Backups
|
||||
if backups == nil {
|
||||
backups = make(map[string]utils.SingleBackupConfig)
|
||||
}
|
||||
|
||||
// Strip passwords from the response
|
||||
sanitized := make(map[string]utils.SingleBackupConfig)
|
||||
for k, v := range backups {
|
||||
v.Password = ""
|
||||
sanitized[k] = v
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"data": sanitized,
|
||||
})
|
||||
} else {
|
||||
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||
}
|
||||
}
|
||||
|
||||
// GetBackupConfigRoute godoc
|
||||
// @Summary Get a single backup configuration by name
|
||||
// @Description Returns a single backup configuration from the server config.
|
||||
// @Tags Backups
|
||||
// @Produce json
|
||||
// @Param name path string true "Backup name"
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} utils.APIResponse
|
||||
// @Failure 403 {object} utils.HTTPErrorResult
|
||||
// @Failure 404 {object} utils.HTTPErrorResult
|
||||
// @Router /api/backups-config/{name} [get]
|
||||
func GetBackupConfigRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.CheckPermissions(w, req, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if req.Method == "GET" {
|
||||
vars := mux.Vars(req)
|
||||
name := vars["name"]
|
||||
|
||||
config := utils.GetMainConfig()
|
||||
backup, exists := config.Backup.Backups[name]
|
||||
if !exists {
|
||||
utils.HTTPError(w, "Backup not found", http.StatusNotFound, "BCK004")
|
||||
return
|
||||
}
|
||||
|
||||
// Strip password from the response
|
||||
backup.Password = ""
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"data": backup,
|
||||
})
|
||||
} else {
|
||||
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||
}
|
||||
}
|
||||
|
||||
// AddBackupRoute godoc
|
||||
// @Summary Create a new backup configuration
|
||||
// @Description Creates a backup config and initializes the repository if needed. Requires premium licence and non-container mode.
|
||||
@@ -245,7 +321,7 @@ func RemoveBackupRoute(w http.ResponseWriter, req *http.Request) {
|
||||
output, err := ListSnapshots(backup.Repository, backup.Password)
|
||||
if err == nil {
|
||||
var outputJSON []map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(output), &outputJSON); err == nil {
|
||||
if err := json.Unmarshal([]byte(SplitJSONObjects(output)), &outputJSON); err == nil {
|
||||
if len(outputJSON) == 0 {
|
||||
err = DeleteRepository(backup.Repository)
|
||||
if err != nil {
|
||||
|
||||
@@ -640,8 +640,9 @@ func DeleteByTag(repository string, password string, tag string) error {
|
||||
}
|
||||
|
||||
// Parse the JSON output to get snapshot IDs
|
||||
// Use SplitJSONObjects to extract JSON from potentially noisy PTY output
|
||||
var snapshots []map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(output), &snapshots); err != nil {
|
||||
if err := json.Unmarshal([]byte(SplitJSONObjects(output)), &snapshots); err != nil {
|
||||
return fmt.Errorf("[Restic] failed to parse snapshots: %w", err)
|
||||
}
|
||||
|
||||
|
||||
+31
-1
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
"github.com/gorilla/mux"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func OpenIDRoute(w http.ResponseWriter, req *http.Request) {
|
||||
@@ -49,6 +50,11 @@ func listOpenIDClients(w http.ResponseWriter, req *http.Request) {
|
||||
|
||||
config := utils.ReadConfigFromFile()
|
||||
|
||||
// delete secrets before returning
|
||||
for i := range config.OpenIDClients {
|
||||
config.OpenIDClients[i].Secret = ""
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"data": config.OpenIDClients,
|
||||
@@ -77,6 +83,9 @@ func getOpenIDClient(w http.ResponseWriter, req *http.Request) {
|
||||
|
||||
for _, client := range config.OpenIDClients {
|
||||
if client.ID == id {
|
||||
// delete secret
|
||||
client.Secret = ""
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"data": client,
|
||||
@@ -133,9 +142,21 @@ func createOpenIDClient(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a secret server-side, store the bcrypt hash, return the plain secret once
|
||||
plainSecret := utils.GenerateRandomString(32)
|
||||
hashedSecret, err := bcrypt.GenerateFromPassword([]byte(plainSecret), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
utils.Error("CreateOpenIDClient: Failed to hash secret", err)
|
||||
utils.HTTPError(w, "Failed to generate secret", http.StatusInternalServerError, "OID008")
|
||||
return
|
||||
}
|
||||
newClient.Secret = string(hashedSecret)
|
||||
|
||||
config.OpenIDClients = append(config.OpenIDClients, newClient)
|
||||
utils.SetBaseMainConfig(config)
|
||||
|
||||
utils.Log("OpenID client created: " + newClient.ID)
|
||||
|
||||
utils.TriggerEvent(
|
||||
"cosmos.settings",
|
||||
"OpenID client created: "+newClient.ID,
|
||||
@@ -144,9 +165,14 @@ func createOpenIDClient(w http.ResponseWriter, req *http.Request) {
|
||||
map[string]interface{}{
|
||||
})
|
||||
|
||||
// Return the plain secret once — it won't be retrievable after this
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"data": newClient,
|
||||
"data": map[string]interface{}{
|
||||
"id": newClient.ID,
|
||||
"redirect": newClient.Redirect,
|
||||
"secret": plainSecret,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -203,6 +229,8 @@ func updateOpenIDClient(w http.ResponseWriter, req *http.Request) {
|
||||
config.OpenIDClients = clients
|
||||
utils.SetBaseMainConfig(config)
|
||||
|
||||
utils.Log("OpenID client updated: " + id)
|
||||
|
||||
utils.TriggerEvent(
|
||||
"cosmos.settings",
|
||||
"OpenID client updated: "+id,
|
||||
@@ -258,6 +286,8 @@ func deleteOpenIDClient(w http.ResponseWriter, req *http.Request) {
|
||||
clients = append(clients[:clientIndex], clients[clientIndex+1:]...)
|
||||
config.OpenIDClients = clients
|
||||
utils.SetBaseMainConfig(config)
|
||||
|
||||
utils.Log("OpenID client deleted: " + id)
|
||||
|
||||
utils.TriggerEvent(
|
||||
"cosmos.settings",
|
||||
|
||||
@@ -120,6 +120,11 @@ func createRoute(w http.ResponseWriter, req *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if !newRoute.UseHost && !newRoute.UsePathPrefix {
|
||||
utils.HTTPError(w, "Route must have at least one of UseHost or UsePathPrefix enabled, otherwise it will catch all requests", http.StatusBadRequest, "RT008")
|
||||
return
|
||||
}
|
||||
|
||||
utils.ConfigLock.Lock()
|
||||
defer utils.ConfigLock.Unlock()
|
||||
|
||||
@@ -136,6 +141,8 @@ func createRoute(w http.ResponseWriter, req *http.Request) {
|
||||
config.HTTPConfig.ProxyConfig.Routes = append([]utils.ProxyRouteConfig{newRoute}, routes...)
|
||||
utils.SetBaseMainConfig(config)
|
||||
|
||||
utils.Log("Route created: " + newRoute.Name)
|
||||
|
||||
utils.TriggerEvent(
|
||||
"cosmos.routes",
|
||||
"Route created: "+newRoute.Name,
|
||||
@@ -203,10 +210,17 @@ func updateRoute(w http.ResponseWriter, req *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if !updatedRoute.UseHost && !updatedRoute.UsePathPrefix {
|
||||
utils.HTTPError(w, "Route must have at least one of UseHost or UsePathPrefix enabled, otherwise it will catch all requests", http.StatusBadRequest, "RT008")
|
||||
return
|
||||
}
|
||||
|
||||
routes[routeIndex] = updatedRoute
|
||||
config.HTTPConfig.ProxyConfig.Routes = routes
|
||||
utils.SetBaseMainConfig(config)
|
||||
|
||||
utils.Log("Route updated: " + name)
|
||||
|
||||
utils.TriggerEvent(
|
||||
"cosmos.routes",
|
||||
"Route updated: "+name,
|
||||
@@ -267,6 +281,9 @@ func deleteRoute(w http.ResponseWriter, req *http.Request) {
|
||||
config.HTTPConfig.ProxyConfig.Routes = routes
|
||||
utils.SetBaseMainConfig(config)
|
||||
|
||||
|
||||
utils.Log("Route deleted: " + name)
|
||||
|
||||
utils.TriggerEvent(
|
||||
"cosmos.routes",
|
||||
"Route deleted: "+name,
|
||||
|
||||
@@ -16,7 +16,7 @@ type DeviceCreateRequestJSON struct {
|
||||
PublicKey string `json:"publicKey,omitempty"`
|
||||
|
||||
// for devices only
|
||||
Nickname string `json:"nickname,omitempty" validate:"max=32,alphanum"`
|
||||
Nickname string `json:"nickname" validate:"required,max=32,alphanum"`
|
||||
Invisible bool `json:"invisible,omitempty"`
|
||||
|
||||
// for lighthouse only
|
||||
|
||||
@@ -729,6 +729,8 @@ func InitServer() *mux.Router {
|
||||
srapiAdmin.HandleFunc("/api/list-dir", storage.ListDirectoryRoute)
|
||||
srapiAdmin.HandleFunc("/api/new-dir", storage.CreateFolderRoute)
|
||||
|
||||
srapiAdmin.HandleFunc("/api/backups-config", backups.ListBackupConfigsRoute)
|
||||
srapiAdmin.HandleFunc("/api/backups-config/{name}", backups.GetBackupConfigRoute)
|
||||
srapiAdmin.HandleFunc("/api/backups-repository", backups.ListRepos)
|
||||
srapiAdmin.HandleFunc("/api/backups-repository/{name}/snapshots", backups.ListSnapshotsRouteFromRepo)
|
||||
srapiAdmin.HandleFunc("/api/backups/{name}/snapshots", backups.ListSnapshotsRoute)
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
terraform-provider-cosmos
|
||||
*.exe
|
||||
dist/
|
||||
.terraform/
|
||||
*.tfstate
|
||||
*.tfstate.backup
|
||||
*.tfplan
|
||||
crash.log
|
||||
@@ -0,0 +1,40 @@
|
||||
version: 2
|
||||
|
||||
builds:
|
||||
- env:
|
||||
- CGO_ENABLED=0
|
||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- "-s -w -X main.version={{ .Version }}"
|
||||
goos:
|
||||
- linux
|
||||
- darwin
|
||||
- windows
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
binary: "{{ .ProjectName }}_v{{ .Version }}"
|
||||
|
||||
archives:
|
||||
- format: zip
|
||||
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
|
||||
|
||||
checksum:
|
||||
name_template: "{{ .ProjectName }}_{{ .Version }}_SHA256SUMS"
|
||||
algorithm: sha256
|
||||
|
||||
signs:
|
||||
- artifacts: checksum
|
||||
args:
|
||||
- "--batch"
|
||||
- "--local-user"
|
||||
- "{{ .Env.GPG_FINGERPRINT }}"
|
||||
- "--output"
|
||||
- "${signature}"
|
||||
- "--detach-sign"
|
||||
- "${artifact}"
|
||||
|
||||
release:
|
||||
draft: true
|
||||
@@ -0,0 +1,19 @@
|
||||
default: build
|
||||
|
||||
build:
|
||||
go build -o terraform-provider-cosmos
|
||||
|
||||
install: build
|
||||
mkdir -p ~/.terraform.d/plugins/cosmos-cloud.io/azukaar/cosmos/0.1.0/linux_amd64
|
||||
cp terraform-provider-cosmos ~/.terraform.d/plugins/cosmos-cloud.io/azukaar/cosmos/0.1.0/linux_amd64/
|
||||
|
||||
test:
|
||||
go test ./... -v
|
||||
|
||||
testacc:
|
||||
TF_ACC=1 go test ./... -v -timeout 120m
|
||||
|
||||
fmt:
|
||||
go fmt ./...
|
||||
|
||||
.PHONY: build install test testacc fmt
|
||||
@@ -0,0 +1,139 @@
|
||||
# Terraform Provider for Cosmos Server
|
||||
|
||||
Manage [Cosmos Server](https://github.com/azukaar/cosmos-server) resources with Terraform.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Terraform >= 1.0
|
||||
- Go >= 1.22 (for development)
|
||||
- A running Cosmos Server instance with an API token
|
||||
|
||||
## Installation
|
||||
|
||||
```hcl
|
||||
terraform {
|
||||
required_providers {
|
||||
cosmos = {
|
||||
source = "cosmos-cloud.io/azukaar/cosmos"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "cosmos" {
|
||||
base_url = "https://cosmos.example.com" # or COSMOS_BASE_URL env
|
||||
token = var.cosmos_token # or COSMOS_TOKEN env
|
||||
insecure = true # or COSMOS_INSECURE env
|
||||
}
|
||||
```
|
||||
|
||||
`terraform init` will automatically download the provider for your platform.
|
||||
|
||||
### Manual installation
|
||||
|
||||
Alternatively, download the binary from [GitHub Releases](https://github.com/azukaar/cosmos-server/releases) and place it in:
|
||||
|
||||
```
|
||||
~/.terraform.d/plugins/cosmos-cloud.io/azukaar/cosmos/<VERSION>/<OS>_<ARCH>/
|
||||
```
|
||||
|
||||
## Provider Configuration
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|-----------|--------|----------|-------------|
|
||||
| `base_url` | string | yes | Cosmos Server URL (e.g. `https://cosmos.example.com`) |
|
||||
| `token` | string | yes | API token (sensitive) |
|
||||
| `insecure` | bool | no | Skip TLS verification. Default: `false` |
|
||||
|
||||
All attributes can be set via environment variables: `COSMOS_BASE_URL`, `COSMOS_TOKEN`, `COSMOS_INSECURE`.
|
||||
|
||||
## Resources
|
||||
|
||||
| Resource | Description |
|
||||
|----------|-------------|
|
||||
| `cosmos_route` | Proxy route with smart shield, auth, and filtering |
|
||||
| `cosmos_docker_service` | Docker container (JSON blob config, recreate on change) |
|
||||
| `cosmos_docker_volume` | Docker volume |
|
||||
| `cosmos_docker_network` | Docker network |
|
||||
| `cosmos_api_token` | API token (token value returned once on create) |
|
||||
| `cosmos_user` | User account |
|
||||
| `cosmos_backup` | Backup configuration (password managed server-side) |
|
||||
| `cosmos_alert` | Alert with conditions and actions |
|
||||
| `cosmos_cron_job` | Scheduled cron job |
|
||||
| `cosmos_openid_client` | OpenID Connect client (secret generated server-side, returned once) |
|
||||
| `cosmos_constellation` | Initialize Constellation VPN (destroy resets the VPN) |
|
||||
| `cosmos_constellation_device` | VPN device (config YAML returned once on create) |
|
||||
| `cosmos_constellation_dns` | Constellation DNS entry |
|
||||
| `cosmos_snapraid` | SnapRAID array configuration |
|
||||
| `cosmos_storage_mount` | Storage mount (all fields require replacement) |
|
||||
|
||||
## Data Sources
|
||||
|
||||
| Data Source | Description |
|
||||
|-------------|-------------|
|
||||
| `cosmos_status` | Server status (hostname, version, docker/constellation status) |
|
||||
| `cosmos_containers` | List of running Docker containers |
|
||||
|
||||
## Examples
|
||||
|
||||
See the [examples/](examples/) directory for complete, working configurations:
|
||||
|
||||
- **[web-app](examples/web-app/)** — Token, volume, container, route with smart shield, backup, and alert
|
||||
- **[vpn](examples/vpn/)** — Constellation VPN init, device, and DNS
|
||||
|
||||
## Development
|
||||
|
||||
### Build and install locally
|
||||
|
||||
```bash
|
||||
cd terraform-provider-cosmos
|
||||
make build # compile
|
||||
make install # copy to ~/.terraform.d/plugins/
|
||||
```
|
||||
|
||||
After installing, in your Terraform project:
|
||||
|
||||
```bash
|
||||
rm .terraform.lock.hcl && terraform init
|
||||
```
|
||||
|
||||
### Dev overrides (faster iteration)
|
||||
|
||||
Add to `~/.terraformrc`:
|
||||
|
||||
```hcl
|
||||
provider_installation {
|
||||
dev_overrides {
|
||||
"cosmos-cloud.io/azukaar/cosmos" = "/path/to/terraform-provider-cosmos"
|
||||
}
|
||||
direct {}
|
||||
}
|
||||
```
|
||||
|
||||
Then just `make build` and run `terraform plan` directly — no `terraform init` needed.
|
||||
|
||||
### Project structure
|
||||
|
||||
```
|
||||
main.go # Entry point
|
||||
internal/
|
||||
provider/provider.go # Provider config
|
||||
client/client.go # SDK wrapper, response parsing, retry logic
|
||||
resources/ # All resource implementations
|
||||
datasources/ # Data source implementations
|
||||
examples/ # Usage examples
|
||||
```
|
||||
|
||||
### Adding a new resource
|
||||
|
||||
1. Create `internal/resources/my_resource.go`
|
||||
2. Implement `resource.Resource` and `resource.ResourceWithImportState`
|
||||
3. Register it in `internal/provider/provider.go` -> `Resources()`
|
||||
4. Run `make build` to verify
|
||||
|
||||
### Common patterns
|
||||
|
||||
- **Response parsing**: `client.ParseResponse[T]()` for typed, `client.CheckResponse()` for status-only, `client.CheckStreamingResponse()` for streaming endpoints
|
||||
- **404 handling**: `client.IsNotFound(err)` — Read methods should call `resp.State.RemoveResource(ctx)` on 404
|
||||
- **Create-time secrets**: `Computed + Sensitive` with `UseStateForUnknown` — capture on create, preserve on read
|
||||
- **Optional with server defaults**: `Optional + Computed + Default` to avoid phantom diffs
|
||||
- **Nested objects**: Use `types.Object` (not Go struct pointers) for `SingleNestedAttribute`
|
||||
@@ -0,0 +1,49 @@
|
||||
module github.com/azukaar/terraform-provider-cosmos
|
||||
|
||||
go 1.22.5
|
||||
|
||||
require (
|
||||
github.com/azukaar/cosmos-server/go-sdk v0.22.0-unstable04
|
||||
github.com/hashicorp/terraform-plugin-framework v1.13.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
|
||||
github.com/fatih/color v1.13.0 // indirect
|
||||
github.com/getkin/kin-openapi v0.134.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/go-hclog v1.5.0 // indirect
|
||||
github.com/hashicorp/go-plugin v1.6.2 // indirect
|
||||
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
||||
github.com/hashicorp/terraform-plugin-go v0.25.0 // indirect
|
||||
github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect
|
||||
github.com/hashicorp/terraform-registry-address v0.2.3 // indirect
|
||||
github.com/hashicorp/terraform-svchost v0.1.1 // indirect
|
||||
github.com/hashicorp/yamux v0.1.1 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||
github.com/oapi-codegen/runtime v1.2.0 // indirect
|
||||
github.com/oasdiff/yaml v0.0.0-20260313112342-a3ea61cb4d4c // indirect
|
||||
github.com/oasdiff/yaml3 v0.0.0-20260224194419-61cd415a242b // indirect
|
||||
github.com/oklog/run v1.0.0 // indirect
|
||||
github.com/perimeterx/marshmallow v1.1.5 // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/woodsbury/decimal128 v1.3.0 // indirect
|
||||
golang.org/x/net v0.28.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect
|
||||
google.golang.org/grpc v1.67.1 // indirect
|
||||
google.golang.org/protobuf v1.35.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
replace github.com/azukaar/cosmos-server/go-sdk => ../go-sdk
|
||||
@@ -0,0 +1,119 @@
|
||||
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
|
||||
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
|
||||
github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA=
|
||||
github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/getkin/kin-openapi v0.134.0 h1:/L5+1+kfe6dXh8Ot/wqiTgUkjOIEJiC0bbYVziHB8rU=
|
||||
github.com/getkin/kin-openapi v0.134.0/go.mod h1:wK6ZLG/VgoETO9pcLJ/VmAtIcl/DNlMayNTb716EUxE=
|
||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
|
||||
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
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.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
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.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c=
|
||||
github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-plugin v1.6.2 h1:zdGAEd0V1lCaU0u+MxWQhtSDQmahpkwOun8U8EiRVog=
|
||||
github.com/hashicorp/go-plugin v1.6.2/go.mod h1:CkgLQ5CZqNmdL9U9JzM532t8ZiYQ35+pj3b1FD37R0Q=
|
||||
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.13.0 h1:8OTG4+oZUfKgnfTdPTJwZ532Bh2BobF4H+yBiYJ/scw=
|
||||
github.com/hashicorp/terraform-plugin-framework v1.13.0/go.mod h1:j64rwMGpgM3NYXTKuxrCnyubQb/4VKldEKlcG8cvmjU=
|
||||
github.com/hashicorp/terraform-plugin-go v0.25.0 h1:oi13cx7xXA6QciMcpcFi/rwA974rdTxjqEhXJjbAyks=
|
||||
github.com/hashicorp/terraform-plugin-go v0.25.0/go.mod h1:+SYagMYadJP86Kvn+TGeV+ofr/R3g4/If0O5sO96MVw=
|
||||
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.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI=
|
||||
github.com/hashicorp/terraform-registry-address v0.2.3/go.mod h1:lFHA76T8jfQteVfT7caREqguFrW3c4MFSPhZB7HHgUM=
|
||||
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.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
|
||||
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
|
||||
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
|
||||
github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/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/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
||||
github.com/oapi-codegen/runtime v1.2.0 h1:RvKc1CVS1QeKSNzO97FBQbSMZyQ8s6rZd+LpmzwHMP4=
|
||||
github.com/oapi-codegen/runtime v1.2.0/go.mod h1:Y7ZhmmlE8ikZOmuHRRndiIm7nf3xcVv+YMweKgG1DT0=
|
||||
github.com/oasdiff/yaml v0.0.0-20260313112342-a3ea61cb4d4c h1:7ACFcSaQsrWtrH4WHHfUqE1C+f8r2uv8KGaW0jTNjus=
|
||||
github.com/oasdiff/yaml v0.0.0-20260313112342-a3ea61cb4d4c/go.mod h1:JKox4Gszkxt57kj27u7rvi7IFoIULvCZHUsBTUmQM/s=
|
||||
github.com/oasdiff/yaml3 v0.0.0-20260224194419-61cd415a242b h1:vivRhVUAa9t1q0Db4ZmezBP8pWQWnXHFokZj0AOea2g=
|
||||
github.com/oasdiff/yaml3 v0.0.0-20260224194419-61cd415a242b/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
|
||||
github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
|
||||
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
|
||||
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
|
||||
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
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=
|
||||
github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0=
|
||||
github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds=
|
||||
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
|
||||
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
|
||||
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.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
|
||||
google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
|
||||
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
|
||||
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
|
||||
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -0,0 +1,325 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
cosmossdk "github.com/azukaar/cosmos-server/go-sdk"
|
||||
)
|
||||
|
||||
// CosmosClient wraps the generated SDK client with convenience helpers.
|
||||
type CosmosClient struct {
|
||||
Raw *cosmossdk.Client
|
||||
BaseURL string
|
||||
}
|
||||
|
||||
// apiResponse is the standard Cosmos API envelope.
|
||||
type apiResponse struct {
|
||||
Status string `json:"status"`
|
||||
Data json.RawMessage `json:"data,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Code string `json:"code,omitempty"`
|
||||
}
|
||||
|
||||
// retryTransport wraps an http.RoundTripper and retries on connection errors.
|
||||
type retryTransport struct {
|
||||
base http.RoundTripper
|
||||
maxRetries int
|
||||
delay time.Duration
|
||||
}
|
||||
|
||||
func (t *retryTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
var resp *http.Response
|
||||
var err error
|
||||
for i := 0; i <= t.maxRetries; i++ {
|
||||
resp, err = t.base.RoundTrip(req)
|
||||
if err == nil {
|
||||
return resp, nil
|
||||
}
|
||||
// Only retry on connection errors (server restarting, etc.)
|
||||
if !isConnectionError(err) {
|
||||
return resp, err
|
||||
}
|
||||
if i < t.maxRetries {
|
||||
time.Sleep(t.delay)
|
||||
}
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func isConnectionError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
s := err.Error()
|
||||
return strings.Contains(s, "connection refused") ||
|
||||
strings.Contains(s, "connection reset") ||
|
||||
strings.Contains(s, "EOF") ||
|
||||
strings.Contains(s, "no such host")
|
||||
}
|
||||
|
||||
// NewCosmosClient creates a new SDK client configured with auth and TLS settings.
|
||||
func NewCosmosClient(baseURL, token string, insecure bool) (*CosmosClient, error) {
|
||||
var base http.RoundTripper = http.DefaultTransport
|
||||
if insecure {
|
||||
base = &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
}
|
||||
|
||||
httpClient := &http.Client{
|
||||
Transport: &retryTransport{
|
||||
base: base,
|
||||
maxRetries: 3,
|
||||
delay: 1 * time.Second,
|
||||
},
|
||||
}
|
||||
|
||||
addToken := func(ctx context.Context, req *http.Request) error {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
return nil
|
||||
}
|
||||
|
||||
sdkClient, err := cosmossdk.NewClient(
|
||||
baseURL+"/cosmos",
|
||||
cosmossdk.WithHTTPClient(httpClient),
|
||||
cosmossdk.WithRequestEditorFn(addToken),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating cosmos SDK client: %w", err)
|
||||
}
|
||||
|
||||
return &CosmosClient{
|
||||
Raw: sdkClient,
|
||||
BaseURL: baseURL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NotFoundError is returned when the API returns HTTP 404.
|
||||
type NotFoundError struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *NotFoundError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
// IsNotFound returns true if the error is a 404 response.
|
||||
func IsNotFound(err error) bool {
|
||||
_, ok := err.(*NotFoundError)
|
||||
return ok
|
||||
}
|
||||
|
||||
// ParseResponse reads an http.Response body, checks the status envelope, and
|
||||
// unmarshals the data field into T.
|
||||
func ParseResponse[T any](resp *http.Response) (*T, error) {
|
||||
if resp == nil {
|
||||
return nil, fmt.Errorf("nil response")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading response body: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode == 404 {
|
||||
return nil, &NotFoundError{Message: string(body)}
|
||||
}
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return nil, fmt.Errorf("API returned HTTP %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var envelope apiResponse
|
||||
if err := json.Unmarshal(body, &envelope); err != nil {
|
||||
return nil, fmt.Errorf("parsing response envelope: %w (body: %s)", err, string(body))
|
||||
}
|
||||
|
||||
if envelope.Status != "OK" && envelope.Status != "ok" {
|
||||
msg := envelope.Message
|
||||
if msg == "" {
|
||||
msg = string(body)
|
||||
}
|
||||
return nil, fmt.Errorf("API error (status=%s): %s", envelope.Status, msg)
|
||||
}
|
||||
|
||||
if len(envelope.Data) == 0 || string(envelope.Data) == "null" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var result T
|
||||
if err := json.Unmarshal(envelope.Data, &result); err != nil {
|
||||
return nil, fmt.Errorf("parsing response data: %w (raw: %s)", err, string(envelope.Data))
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// CheckResponse reads an http.Response and verifies the status envelope without
|
||||
// trying to parse data.
|
||||
func CheckResponse(resp *http.Response) error {
|
||||
if resp == nil {
|
||||
return fmt.Errorf("nil response")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading response body: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode == 404 {
|
||||
return &NotFoundError{Message: string(body)}
|
||||
}
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return fmt.Errorf("API returned HTTP %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var envelope apiResponse
|
||||
if err := json.Unmarshal(body, &envelope); err != nil {
|
||||
return fmt.Errorf("parsing response envelope: %w (body: %s)", err, string(body))
|
||||
}
|
||||
|
||||
if envelope.Status != "OK" && envelope.Status != "ok" {
|
||||
msg := envelope.Message
|
||||
if msg == "" {
|
||||
msg = string(body)
|
||||
}
|
||||
return fmt.Errorf("API error (status=%s): %s", envelope.Status, msg)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseRawResponse reads an http.Response and returns the raw data field from
|
||||
// the response envelope.
|
||||
func ParseRawResponse(resp *http.Response) (json.RawMessage, error) {
|
||||
if resp == nil {
|
||||
return nil, fmt.Errorf("nil response")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading response body: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode == 404 {
|
||||
return nil, &NotFoundError{Message: string(body)}
|
||||
}
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return nil, fmt.Errorf("API returned HTTP %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var envelope apiResponse
|
||||
if err := json.Unmarshal(body, &envelope); err != nil {
|
||||
return nil, fmt.Errorf("parsing response envelope: %w (body: %s)", err, string(body))
|
||||
}
|
||||
|
||||
if envelope.Status != "OK" && envelope.Status != "ok" {
|
||||
msg := envelope.Message
|
||||
if msg == "" {
|
||||
msg = string(body)
|
||||
}
|
||||
return nil, fmt.Errorf("API error (status=%s): %s", envelope.Status, msg)
|
||||
}
|
||||
|
||||
return envelope.Data, nil
|
||||
}
|
||||
|
||||
// CheckStreamingResponse handles API endpoints that stream progress text
|
||||
// (e.g. docker pull logs, service creation) instead of returning a standard
|
||||
// JSON envelope. It reads the full body and checks for success/failure markers.
|
||||
func CheckStreamingResponse(resp *http.Response) error {
|
||||
if resp == nil {
|
||||
return fmt.Errorf("nil response")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading response body: %w", err)
|
||||
}
|
||||
|
||||
bodyStr := string(body)
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return fmt.Errorf("API returned HTTP %d: %s", resp.StatusCode, bodyStr)
|
||||
}
|
||||
|
||||
// Check for failure indicators in the streamed output.
|
||||
if strings.Contains(bodyStr, "[OPERATION FAILED]") || strings.Contains(bodyStr, "[FAIL]") {
|
||||
return fmt.Errorf("operation failed: %s", bodyStr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helper functions for converting between Terraform and SDK types.
|
||||
|
||||
func StringPtr(s string) *string {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
return &s
|
||||
}
|
||||
|
||||
func StringPtrVal(s *string) string {
|
||||
if s == nil {
|
||||
return ""
|
||||
}
|
||||
return *s
|
||||
}
|
||||
|
||||
func BoolPtr(b bool) *bool {
|
||||
return &b
|
||||
}
|
||||
|
||||
func BoolPtrVal(b *bool) bool {
|
||||
if b == nil {
|
||||
return false
|
||||
}
|
||||
return *b
|
||||
}
|
||||
|
||||
func IntPtr(i int) *int {
|
||||
return &i
|
||||
}
|
||||
|
||||
func IntPtrVal(i *int) int {
|
||||
if i == nil {
|
||||
return 0
|
||||
}
|
||||
return *i
|
||||
}
|
||||
|
||||
func Int64Ptr(i int64) *int64 {
|
||||
return &i
|
||||
}
|
||||
|
||||
func Int64PtrVal(i *int64) int64 {
|
||||
if i == nil {
|
||||
return 0
|
||||
}
|
||||
return *i
|
||||
}
|
||||
|
||||
func Float32Ptr(f float32) *float32 {
|
||||
return &f
|
||||
}
|
||||
|
||||
func Float32PtrVal(f *float32) float32 {
|
||||
if f == nil {
|
||||
return 0
|
||||
}
|
||||
return *f
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package datasources
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/azukaar/terraform-provider-cosmos/internal/client"
|
||||
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
)
|
||||
|
||||
var _ datasource.DataSource = &containersDataSource{}
|
||||
|
||||
func NewContainersDataSource() datasource.DataSource {
|
||||
return &containersDataSource{}
|
||||
}
|
||||
|
||||
type containersDataSource struct {
|
||||
client *client.CosmosClient
|
||||
}
|
||||
|
||||
// ---------- Terraform model ----------
|
||||
|
||||
type containersDataSourceModel struct {
|
||||
Containers []containerModel `tfsdk:"containers"`
|
||||
}
|
||||
|
||||
type containerModel struct {
|
||||
ID types.String `tfsdk:"id"`
|
||||
Name types.String `tfsdk:"name"`
|
||||
Image types.String `tfsdk:"image"`
|
||||
State types.String `tfsdk:"state"`
|
||||
}
|
||||
|
||||
// ---------- datasource.DataSource ----------
|
||||
|
||||
func (d *containersDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_containers"
|
||||
}
|
||||
|
||||
func (d *containersDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "Lists all Docker containers managed by Cosmos Server.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"containers": schema.ListNestedAttribute{
|
||||
Description: "List of containers.",
|
||||
Computed: true,
|
||||
NestedObject: schema.NestedAttributeObject{
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"id": schema.StringAttribute{
|
||||
Description: "The container ID.",
|
||||
Computed: true,
|
||||
},
|
||||
"name": schema.StringAttribute{
|
||||
Description: "The container name.",
|
||||
Computed: true,
|
||||
},
|
||||
"image": schema.StringAttribute{
|
||||
Description: "The container image.",
|
||||
Computed: true,
|
||||
},
|
||||
"state": schema.StringAttribute{
|
||||
Description: "The current state of the container (e.g. running, stopped).",
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *containersDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
c, ok := req.ProviderData.(*client.CosmosClient)
|
||||
if !ok {
|
||||
resp.Diagnostics.AddError(
|
||||
"Unexpected DataSource Configure Type",
|
||||
fmt.Sprintf("Expected *client.CosmosClient, got: %T", req.ProviderData),
|
||||
)
|
||||
return
|
||||
}
|
||||
d.client = c
|
||||
}
|
||||
|
||||
func (d *containersDataSource) Read(ctx context.Context, _ datasource.ReadRequest, resp *datasource.ReadResponse) {
|
||||
httpResp, err := d.client.Raw.GetApiServapps(ctx, nil)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error reading containers", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
rawData, err := client.ParseRawResponse(httpResp)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error reading containers", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var model containersDataSourceModel
|
||||
|
||||
if rawData == nil || string(rawData) == "null" {
|
||||
model.Containers = []containerModel{}
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &model)...)
|
||||
return
|
||||
}
|
||||
|
||||
var containers []map[string]interface{}
|
||||
if err := json.Unmarshal(rawData, &containers); err != nil {
|
||||
resp.Diagnostics.AddError("Error parsing containers response", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
model.Containers = make([]containerModel, 0, len(containers))
|
||||
for _, c := range containers {
|
||||
cm := containerModel{
|
||||
ID: types.StringValue(containerFieldString(c, "Id", "id", "ID")),
|
||||
Name: types.StringValue(containerFieldString(c, "Name", "name", "Names")),
|
||||
Image: types.StringValue(containerFieldString(c, "Image", "image")),
|
||||
State: types.StringValue(containerFieldString(c, "State", "state", "Status")),
|
||||
}
|
||||
model.Containers = append(model.Containers, cm)
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &model)...)
|
||||
}
|
||||
|
||||
// ---------- helpers ----------
|
||||
|
||||
// containerFieldString extracts a string field from a container map, trying multiple key variants.
|
||||
func containerFieldString(m map[string]interface{}, keys ...string) string {
|
||||
for _, key := range keys {
|
||||
if v, ok := m[key]; ok {
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
return val
|
||||
case []interface{}:
|
||||
// Docker sometimes returns Names as an array like ["/container_name"].
|
||||
if len(val) > 0 {
|
||||
if s, ok := val[0].(string); ok {
|
||||
// Strip leading slash if present.
|
||||
if len(s) > 0 && s[0] == '/' {
|
||||
return s[1:]
|
||||
}
|
||||
return s
|
||||
}
|
||||
}
|
||||
default:
|
||||
return fmt.Sprintf("%v", val)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package datasources
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/azukaar/terraform-provider-cosmos/internal/client"
|
||||
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
)
|
||||
|
||||
var _ datasource.DataSource = &statusDataSource{}
|
||||
|
||||
func NewStatusDataSource() datasource.DataSource {
|
||||
return &statusDataSource{}
|
||||
}
|
||||
|
||||
type statusDataSource struct {
|
||||
client *client.CosmosClient
|
||||
}
|
||||
|
||||
// ---------- Terraform model ----------
|
||||
|
||||
type statusDataSourceModel struct {
|
||||
Hostname types.String `tfsdk:"hostname"`
|
||||
Version types.String `tfsdk:"version"`
|
||||
NeedsUpdate types.Bool `tfsdk:"needs_update"`
|
||||
DockerStatus types.String `tfsdk:"docker_status"`
|
||||
ConstellationStatus types.String `tfsdk:"constellation_status"`
|
||||
}
|
||||
|
||||
// ---------- datasource.DataSource ----------
|
||||
|
||||
func (d *statusDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_status"
|
||||
}
|
||||
|
||||
func (d *statusDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "Reads the current status of the Cosmos Server instance.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"hostname": schema.StringAttribute{
|
||||
Description: "The hostname of the Cosmos Server.",
|
||||
Computed: true,
|
||||
},
|
||||
"version": schema.StringAttribute{
|
||||
Description: "The current version of Cosmos Server.",
|
||||
Computed: true,
|
||||
},
|
||||
"needs_update": schema.BoolAttribute{
|
||||
Description: "Whether an update is available for Cosmos Server.",
|
||||
Computed: true,
|
||||
},
|
||||
"docker_status": schema.StringAttribute{
|
||||
Description: "The status of the Docker daemon.",
|
||||
Computed: true,
|
||||
},
|
||||
"constellation_status": schema.StringAttribute{
|
||||
Description: "The status of the Constellation VPN.",
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *statusDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
c, ok := req.ProviderData.(*client.CosmosClient)
|
||||
if !ok {
|
||||
resp.Diagnostics.AddError(
|
||||
"Unexpected DataSource Configure Type",
|
||||
fmt.Sprintf("Expected *client.CosmosClient, got: %T", req.ProviderData),
|
||||
)
|
||||
return
|
||||
}
|
||||
d.client = c
|
||||
}
|
||||
|
||||
func (d *statusDataSource) Read(ctx context.Context, _ datasource.ReadRequest, resp *datasource.ReadResponse) {
|
||||
httpResp, err := d.client.Raw.GetApiStatus(ctx)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error reading Cosmos status", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
rawData, err := client.ParseRawResponse(httpResp)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error reading Cosmos status", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the status response as a generic map since the structure may vary.
|
||||
var status map[string]interface{}
|
||||
if rawData != nil {
|
||||
if err := json.Unmarshal(rawData, &status); err != nil {
|
||||
resp.Diagnostics.AddError("Error parsing Cosmos status response", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var model statusDataSourceModel
|
||||
|
||||
model.Hostname = types.StringValue(extractString(status, "hostname"))
|
||||
model.Version = types.StringValue(extractString(status, "version"))
|
||||
model.NeedsUpdate = types.BoolValue(extractBool(status, "needsUpdate"))
|
||||
model.DockerStatus = types.StringValue(extractString(status, "dockerStatus"))
|
||||
model.ConstellationStatus = types.StringValue(extractString(status, "constellationStatus"))
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &model)...)
|
||||
}
|
||||
|
||||
// ---------- helpers ----------
|
||||
|
||||
// extractString safely extracts a string value from a map, returning "" if not found.
|
||||
func extractString(m map[string]interface{}, key string) string {
|
||||
if m == nil {
|
||||
return ""
|
||||
}
|
||||
if v, ok := m[key]; ok {
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
return val
|
||||
case float64:
|
||||
return fmt.Sprintf("%v", val)
|
||||
default:
|
||||
// Try JSON marshalling for complex types.
|
||||
b, err := json.Marshal(val)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("%v", val)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// extractBool safely extracts a boolean value from a map, returning false if not found.
|
||||
func extractBool(m map[string]interface{}, key string) bool {
|
||||
if m == nil {
|
||||
return false
|
||||
}
|
||||
if v, ok := m[key]; ok {
|
||||
if b, ok := v.(bool); ok {
|
||||
return b
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/azukaar/terraform-provider-cosmos/internal/client"
|
||||
"github.com/azukaar/terraform-provider-cosmos/internal/datasources"
|
||||
"github.com/azukaar/terraform-provider-cosmos/internal/resources"
|
||||
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||
"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"
|
||||
)
|
||||
|
||||
var _ provider.Provider = &CosmosProvider{}
|
||||
|
||||
type CosmosProvider struct {
|
||||
version string
|
||||
}
|
||||
|
||||
type CosmosProviderModel struct {
|
||||
BaseURL types.String `tfsdk:"base_url"`
|
||||
Token types.String `tfsdk:"token"`
|
||||
Insecure types.Bool `tfsdk:"insecure"`
|
||||
}
|
||||
|
||||
func New(version string) func() provider.Provider {
|
||||
return func() provider.Provider {
|
||||
return &CosmosProvider{version: version}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *CosmosProvider) Metadata(_ context.Context, _ provider.MetadataRequest, resp *provider.MetadataResponse) {
|
||||
resp.TypeName = "cosmos"
|
||||
resp.Version = p.version
|
||||
}
|
||||
|
||||
func (p *CosmosProvider) Schema(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "Terraform provider for managing Cosmos Server resources.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"base_url": schema.StringAttribute{
|
||||
Description: "The base URL of the Cosmos Server (e.g. https://cosmos.example.com). Can also be set with COSMOS_BASE_URL env var.",
|
||||
Optional: true,
|
||||
},
|
||||
"token": schema.StringAttribute{
|
||||
Description: "API token for authenticating with Cosmos Server. Can also be set with COSMOS_TOKEN env var.",
|
||||
Optional: true,
|
||||
Sensitive: true,
|
||||
},
|
||||
"insecure": schema.BoolAttribute{
|
||||
Description: "Skip TLS certificate verification. Can also be set with COSMOS_INSECURE env var. Default: false.",
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *CosmosProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
|
||||
var config CosmosProviderModel
|
||||
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
baseURL := os.Getenv("COSMOS_BASE_URL")
|
||||
if !config.BaseURL.IsNull() {
|
||||
baseURL = config.BaseURL.ValueString()
|
||||
}
|
||||
if baseURL == "" {
|
||||
resp.Diagnostics.AddError("Missing base_url", "base_url must be set in the provider configuration or via COSMOS_BASE_URL environment variable.")
|
||||
return
|
||||
}
|
||||
|
||||
token := os.Getenv("COSMOS_TOKEN")
|
||||
if !config.Token.IsNull() {
|
||||
token = config.Token.ValueString()
|
||||
}
|
||||
if token == "" {
|
||||
resp.Diagnostics.AddError("Missing token", "token must be set in the provider configuration or via COSMOS_TOKEN environment variable.")
|
||||
return
|
||||
}
|
||||
|
||||
insecure := os.Getenv("COSMOS_INSECURE") == "true"
|
||||
if !config.Insecure.IsNull() {
|
||||
insecure = config.Insecure.ValueBool()
|
||||
}
|
||||
|
||||
cosmosClient, err := client.NewCosmosClient(baseURL, token, insecure)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Failed to create Cosmos client", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.DataSourceData = cosmosClient
|
||||
resp.ResourceData = cosmosClient
|
||||
}
|
||||
|
||||
func (p *CosmosProvider) Resources(_ context.Context) []func() resource.Resource {
|
||||
return []func() resource.Resource{
|
||||
resources.NewRouteResource,
|
||||
resources.NewUserResource,
|
||||
resources.NewAPITokenResource,
|
||||
resources.NewOpenIDClientResource,
|
||||
resources.NewConstellationResource,
|
||||
resources.NewConstellationDNSResource,
|
||||
resources.NewCronJobResource,
|
||||
resources.NewAlertResource,
|
||||
resources.NewConstellationDeviceResource,
|
||||
resources.NewBackupResource,
|
||||
resources.NewDockerNetworkResource,
|
||||
resources.NewDockerVolumeResource,
|
||||
resources.NewDockerServiceResource,
|
||||
resources.NewSnapRAIDResource,
|
||||
resources.NewStorageMountResource,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *CosmosProvider) DataSources(_ context.Context) []func() datasource.DataSource {
|
||||
return []func() datasource.DataSource{
|
||||
datasources.NewStatusDataSource,
|
||||
datasources.NewContainersDataSource,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,465 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
cosmossdk "github.com/azukaar/cosmos-server/go-sdk"
|
||||
"github.com/azukaar/terraform-provider-cosmos/internal/client"
|
||||
"github.com/hashicorp/terraform-plugin-framework/attr"
|
||||
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||
"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"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
|
||||
)
|
||||
|
||||
var (
|
||||
_ resource.Resource = &alertResource{}
|
||||
_ resource.ResourceWithImportState = &alertResource{}
|
||||
)
|
||||
|
||||
// NewAlertResource returns a new alert resource instance.
|
||||
func NewAlertResource() resource.Resource {
|
||||
return &alertResource{}
|
||||
}
|
||||
|
||||
type alertResource struct {
|
||||
client *client.CosmosClient
|
||||
}
|
||||
|
||||
// ---------- Terraform model types ----------
|
||||
|
||||
type alertResourceModel struct {
|
||||
Name types.String `tfsdk:"name"`
|
||||
Enabled types.Bool `tfsdk:"enabled"`
|
||||
TrackingMetric types.String `tfsdk:"tracking_metric"`
|
||||
Period types.String `tfsdk:"period"`
|
||||
Severity types.String `tfsdk:"severity"`
|
||||
Condition types.Object `tfsdk:"condition"`
|
||||
Actions types.List `tfsdk:"actions"`
|
||||
LastTriggered types.String `tfsdk:"last_triggered"`
|
||||
Throttled types.Bool `tfsdk:"throttled"`
|
||||
}
|
||||
|
||||
type alertConditionModel struct {
|
||||
Operator types.String `tfsdk:"operator"`
|
||||
Value types.Int64 `tfsdk:"value"`
|
||||
Percent types.Bool `tfsdk:"percent"`
|
||||
}
|
||||
|
||||
type alertActionModel struct {
|
||||
Type types.String `tfsdk:"type"`
|
||||
Target types.String `tfsdk:"target"`
|
||||
}
|
||||
|
||||
// Attribute type maps used when constructing framework object/list values.
|
||||
var alertConditionAttrTypes = map[string]attr.Type{
|
||||
"operator": types.StringType,
|
||||
"value": types.Int64Type,
|
||||
"percent": types.BoolType,
|
||||
}
|
||||
|
||||
var alertActionAttrTypes = map[string]attr.Type{
|
||||
"type": types.StringType,
|
||||
"target": types.StringType,
|
||||
}
|
||||
|
||||
// ---------- Resource interface ----------
|
||||
|
||||
func (r *alertResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_alert"
|
||||
}
|
||||
|
||||
func (r *alertResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "Manages a Cosmos Server monitoring alert.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"name": schema.StringAttribute{
|
||||
Description: "The unique name of the alert.",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
"enabled": schema.BoolAttribute{
|
||||
Description: "Whether the alert is enabled.",
|
||||
Optional: true,
|
||||
},
|
||||
"tracking_metric": schema.StringAttribute{
|
||||
Description: "The metric to track for this alert.",
|
||||
Optional: true,
|
||||
},
|
||||
"period": schema.StringAttribute{
|
||||
Description: "The time period for alert evaluation.",
|
||||
Optional: true,
|
||||
},
|
||||
"severity": schema.StringAttribute{
|
||||
Description: "The severity level of the alert.",
|
||||
Optional: true,
|
||||
},
|
||||
"condition": schema.SingleNestedAttribute{
|
||||
Description: "The condition that triggers the alert.",
|
||||
Optional: true,
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"operator": schema.StringAttribute{
|
||||
Description: "The comparison operator (e.g. >, <, ==).",
|
||||
Optional: true,
|
||||
},
|
||||
"value": schema.Int64Attribute{
|
||||
Description: "The threshold value.",
|
||||
Optional: true,
|
||||
},
|
||||
"percent": schema.BoolAttribute{
|
||||
Description: "Whether the value is a percentage.",
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
"actions": schema.ListNestedAttribute{
|
||||
Description: "Actions to perform when the alert triggers.",
|
||||
Optional: true,
|
||||
NestedObject: schema.NestedAttributeObject{
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"type": schema.StringAttribute{
|
||||
Description: "The action type.",
|
||||
Optional: true,
|
||||
},
|
||||
"target": schema.StringAttribute{
|
||||
Description: "The action target.",
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"last_triggered": schema.StringAttribute{
|
||||
Description: "The last time the alert was triggered.",
|
||||
Computed: true,
|
||||
},
|
||||
"throttled": schema.BoolAttribute{
|
||||
Description: "Whether the alert is currently throttled.",
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *alertResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
c, ok := req.ProviderData.(*client.CosmosClient)
|
||||
if !ok {
|
||||
resp.Diagnostics.AddError(
|
||||
"Unexpected Resource Configure Type",
|
||||
fmt.Sprintf("Expected *client.CosmosClient, got: %T", req.ProviderData),
|
||||
)
|
||||
return
|
||||
}
|
||||
r.client = c
|
||||
}
|
||||
|
||||
// ---------- CRUD ----------
|
||||
|
||||
func (r *alertResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||
var plan alertResourceModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
alert := r.modelToSDK(ctx, &plan, &resp.Diagnostics)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
httpResp, err := r.client.Raw.PostApiAlerts(ctx, alert)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error creating alert", err.Error())
|
||||
return
|
||||
}
|
||||
if err := client.CheckResponse(httpResp); err != nil {
|
||||
resp.Diagnostics.AddError("Error creating alert", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Read back to populate computed fields.
|
||||
if !r.readAlert(ctx, plan.Name.ValueString(), &plan, &resp.Diagnostics) {
|
||||
resp.Diagnostics.AddError("Error reading alert", "Alert not found after creation")
|
||||
return
|
||||
}
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||
}
|
||||
|
||||
func (r *alertResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||
var state alertResourceModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
name := state.Name.ValueString()
|
||||
httpResp, err := r.client.Raw.GetApiAlertsName(ctx, name, cosmossdk.UtilsAlert{Name: name})
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error reading alert", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
alert, err := client.ParseResponse[cosmossdk.UtilsAlert](httpResp)
|
||||
if err != nil {
|
||||
if client.IsNotFound(err) {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
resp.Diagnostics.AddError("Error parsing alert response", err.Error())
|
||||
return
|
||||
}
|
||||
if alert == nil {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
r.sdkToModel(alert, &state)
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
|
||||
}
|
||||
|
||||
func (r *alertResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||
var plan alertResourceModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
alert := r.modelToSDK(ctx, &plan, &resp.Diagnostics)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
name := plan.Name.ValueString()
|
||||
httpResp, err := r.client.Raw.PutApiAlertsName(ctx, name, alert)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error updating alert", err.Error())
|
||||
return
|
||||
}
|
||||
if err := client.CheckResponse(httpResp); err != nil {
|
||||
resp.Diagnostics.AddError("Error updating alert", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if !r.readAlert(ctx, name, &plan, &resp.Diagnostics) {
|
||||
resp.Diagnostics.AddError("Error reading alert", "Alert not found after update")
|
||||
return
|
||||
}
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||
}
|
||||
|
||||
func (r *alertResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||
var state alertResourceModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
name := state.Name.ValueString()
|
||||
httpResp, err := r.client.Raw.DeleteApiAlertsName(ctx, name, cosmossdk.UtilsAlert{Name: name})
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error deleting alert", err.Error())
|
||||
return
|
||||
}
|
||||
if err := client.CheckResponse(httpResp); err != nil {
|
||||
resp.Diagnostics.AddError("Error deleting alert", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (r *alertResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||
resource.ImportStatePassthroughID(ctx, path.Root("name"), req, resp)
|
||||
}
|
||||
|
||||
// ---------- helpers ----------
|
||||
|
||||
// readAlert fetches the alert from the API and updates the Terraform model.
|
||||
// It returns false if the alert was not found (404), in which case no diagnostic
|
||||
// error is added — the caller should remove the resource from state.
|
||||
func (r *alertResource) readAlert(ctx context.Context, name string, model *alertResourceModel, diags *diag.Diagnostics) bool {
|
||||
httpResp, err := r.client.Raw.GetApiAlertsName(ctx, name, cosmossdk.UtilsAlert{Name: name})
|
||||
if err != nil {
|
||||
diags.AddError("Error reading alert", err.Error())
|
||||
return true
|
||||
}
|
||||
|
||||
alert, err := client.ParseResponse[cosmossdk.UtilsAlert](httpResp)
|
||||
if err != nil {
|
||||
if client.IsNotFound(err) {
|
||||
return false
|
||||
}
|
||||
diags.AddError("Error parsing alert response", err.Error())
|
||||
return true
|
||||
}
|
||||
if alert == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
r.sdkToModel(alert, model)
|
||||
return true
|
||||
}
|
||||
|
||||
// modelToSDK converts the Terraform resource model into an SDK UtilsAlert.
|
||||
func (r *alertResource) modelToSDK(ctx context.Context, model *alertResourceModel, diags *diag.Diagnostics) cosmossdk.UtilsAlert {
|
||||
alert := cosmossdk.UtilsAlert{
|
||||
Name: model.Name.ValueString(),
|
||||
}
|
||||
|
||||
if !model.Enabled.IsNull() && !model.Enabled.IsUnknown() {
|
||||
alert.Enabled = client.BoolPtr(model.Enabled.ValueBool())
|
||||
}
|
||||
if !model.TrackingMetric.IsNull() && !model.TrackingMetric.IsUnknown() {
|
||||
alert.TrackingMetric = client.StringPtr(model.TrackingMetric.ValueString())
|
||||
}
|
||||
if !model.Period.IsNull() && !model.Period.IsUnknown() {
|
||||
alert.Period = client.StringPtr(model.Period.ValueString())
|
||||
}
|
||||
if !model.Severity.IsNull() && !model.Severity.IsUnknown() {
|
||||
alert.Severity = client.StringPtr(model.Severity.ValueString())
|
||||
}
|
||||
|
||||
// Condition
|
||||
if !model.Condition.IsNull() && !model.Condition.IsUnknown() {
|
||||
var condModel alertConditionModel
|
||||
d := model.Condition.As(ctx, &condModel, basetypes.ObjectAsOptions{})
|
||||
diags.Append(d...)
|
||||
if d.HasError() {
|
||||
return alert
|
||||
}
|
||||
cond := cosmossdk.UtilsAlertCondition{}
|
||||
if !condModel.Operator.IsNull() && !condModel.Operator.IsUnknown() {
|
||||
cond.Operator = client.StringPtr(condModel.Operator.ValueString())
|
||||
}
|
||||
if !condModel.Value.IsNull() && !condModel.Value.IsUnknown() {
|
||||
v := int(condModel.Value.ValueInt64())
|
||||
cond.Value = client.IntPtr(v)
|
||||
}
|
||||
if !condModel.Percent.IsNull() && !condModel.Percent.IsUnknown() {
|
||||
cond.Percent = client.BoolPtr(condModel.Percent.ValueBool())
|
||||
}
|
||||
alert.Condition = &cond
|
||||
}
|
||||
|
||||
// Actions
|
||||
if !model.Actions.IsNull() && !model.Actions.IsUnknown() {
|
||||
var actionModels []alertActionModel
|
||||
d := model.Actions.ElementsAs(ctx, &actionModels, false)
|
||||
diags.Append(d...)
|
||||
if d.HasError() {
|
||||
return alert
|
||||
}
|
||||
actions := make([]cosmossdk.UtilsAlertAction, len(actionModels))
|
||||
for i, am := range actionModels {
|
||||
a := cosmossdk.UtilsAlertAction{}
|
||||
if !am.Type.IsNull() && !am.Type.IsUnknown() {
|
||||
a.Type = client.StringPtr(am.Type.ValueString())
|
||||
}
|
||||
if !am.Target.IsNull() && !am.Target.IsUnknown() {
|
||||
a.Target = client.StringPtr(am.Target.ValueString())
|
||||
}
|
||||
actions[i] = a
|
||||
}
|
||||
alert.Actions = &actions
|
||||
}
|
||||
|
||||
return alert
|
||||
}
|
||||
|
||||
// sdkToModel populates the Terraform resource model from an SDK UtilsAlert.
|
||||
func (r *alertResource) sdkToModel(alert *cosmossdk.UtilsAlert, model *alertResourceModel) {
|
||||
model.Name = types.StringValue(alert.Name)
|
||||
|
||||
if alert.Enabled != nil {
|
||||
model.Enabled = types.BoolValue(*alert.Enabled)
|
||||
}
|
||||
if alert.TrackingMetric != nil {
|
||||
model.TrackingMetric = types.StringValue(*alert.TrackingMetric)
|
||||
}
|
||||
if alert.Period != nil {
|
||||
model.Period = types.StringValue(*alert.Period)
|
||||
}
|
||||
if alert.Severity != nil {
|
||||
model.Severity = types.StringValue(*alert.Severity)
|
||||
}
|
||||
|
||||
// Computed fields always get a value.
|
||||
if alert.LastTriggered != nil {
|
||||
model.LastTriggered = types.StringValue(*alert.LastTriggered)
|
||||
} else {
|
||||
model.LastTriggered = types.StringValue("")
|
||||
}
|
||||
if alert.Throttled != nil {
|
||||
model.Throttled = types.BoolValue(*alert.Throttled)
|
||||
} else {
|
||||
model.Throttled = types.BoolValue(false)
|
||||
}
|
||||
|
||||
// Condition
|
||||
if alert.Condition != nil {
|
||||
c := alert.Condition
|
||||
condObj, _ := types.ObjectValue(alertConditionAttrTypes, map[string]attr.Value{
|
||||
"operator": alertStringOrNull(c.Operator),
|
||||
"value": alertIntOrNull(c.Value),
|
||||
"percent": alertBoolOrNull(c.Percent),
|
||||
})
|
||||
model.Condition = condObj
|
||||
} else {
|
||||
model.Condition = types.ObjectNull(alertConditionAttrTypes)
|
||||
}
|
||||
|
||||
// Actions
|
||||
if alert.Actions != nil && len(*alert.Actions) > 0 {
|
||||
actionValues := make([]attr.Value, len(*alert.Actions))
|
||||
for i, a := range *alert.Actions {
|
||||
obj, _ := types.ObjectValue(alertActionAttrTypes, map[string]attr.Value{
|
||||
"type": alertStringOrNull(a.Type),
|
||||
"target": alertStringOrNull(a.Target),
|
||||
})
|
||||
actionValues[i] = obj
|
||||
}
|
||||
actionList, _ := types.ListValue(types.ObjectType{AttrTypes: alertActionAttrTypes}, actionValues)
|
||||
model.Actions = actionList
|
||||
} else {
|
||||
model.Actions = types.ListNull(types.ObjectType{AttrTypes: alertActionAttrTypes})
|
||||
}
|
||||
}
|
||||
|
||||
// Small helpers to convert SDK pointers into framework attr.Value.
|
||||
|
||||
func alertStringOrNull(s *string) attr.Value {
|
||||
if s == nil || *s == "" {
|
||||
return types.StringNull()
|
||||
}
|
||||
return types.StringValue(*s)
|
||||
}
|
||||
|
||||
func alertIntOrNull(i *int) attr.Value {
|
||||
if i == nil {
|
||||
return types.Int64Null()
|
||||
}
|
||||
return types.Int64Value(int64(*i))
|
||||
}
|
||||
|
||||
func alertBoolOrNull(b *bool) attr.Value {
|
||||
if b == nil || !*b {
|
||||
return types.BoolNull()
|
||||
}
|
||||
return types.BoolValue(*b)
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
cosmossdk "github.com/azukaar/cosmos-server/go-sdk"
|
||||
"github.com/azukaar/terraform-provider-cosmos/internal/client"
|
||||
"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/booldefault"
|
||||
"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"
|
||||
)
|
||||
|
||||
// Ensure interface compliance.
|
||||
var (
|
||||
_ resource.Resource = &apiTokenResource{}
|
||||
_ resource.ResourceWithImportState = &apiTokenResource{}
|
||||
)
|
||||
|
||||
// NewAPITokenResource returns a new resource.Resource for API tokens.
|
||||
func NewAPITokenResource() resource.Resource {
|
||||
return &apiTokenResource{}
|
||||
}
|
||||
|
||||
type apiTokenResource struct {
|
||||
client *client.CosmosClient
|
||||
}
|
||||
|
||||
// apiTokenModel maps the resource schema to a Go struct.
|
||||
type apiTokenModel struct {
|
||||
Name types.String `tfsdk:"name"`
|
||||
Description types.String `tfsdk:"description"`
|
||||
ReadOnly types.Bool `tfsdk:"read_only"`
|
||||
IpWhitelist types.List `tfsdk:"ip_whitelist"`
|
||||
RestrictToConstellation types.Bool `tfsdk:"restrict_to_constellation"`
|
||||
Token types.String `tfsdk:"token"`
|
||||
}
|
||||
|
||||
func (r *apiTokenResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_api_token"
|
||||
}
|
||||
|
||||
func (r *apiTokenResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "Manages a Cosmos Server API token.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"name": schema.StringAttribute{
|
||||
Description: "The unique name of the API token. Changing this forces re-creation.",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
"description": schema.StringAttribute{
|
||||
Description: "A human-readable description of the API token.",
|
||||
Optional: true,
|
||||
},
|
||||
"read_only": schema.BoolAttribute{
|
||||
Description: "Whether the token is read-only. Can only be set at creation time.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Default: booldefault.StaticBool(false),
|
||||
PlanModifiers: []planmodifier.Bool{
|
||||
// read_only is only sent on create; after that it is immutable.
|
||||
},
|
||||
},
|
||||
"ip_whitelist": schema.ListAttribute{
|
||||
Description: "List of IP addresses or CIDRs allowed to use this token.",
|
||||
Optional: true,
|
||||
ElementType: types.StringType,
|
||||
},
|
||||
"restrict_to_constellation": schema.BoolAttribute{
|
||||
Description: "Whether this token is restricted to the Constellation network.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Default: booldefault.StaticBool(false),
|
||||
},
|
||||
"token": schema.StringAttribute{
|
||||
Description: "The raw API token string. Only available after creation; it is not returned by subsequent reads. On import this value will be empty.",
|
||||
Computed: true,
|
||||
Sensitive: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *apiTokenResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
c, ok := req.ProviderData.(*client.CosmosClient)
|
||||
if !ok {
|
||||
resp.Diagnostics.AddError(
|
||||
"Unexpected Resource Configure Type",
|
||||
fmt.Sprintf("Expected *client.CosmosClient, got: %T", req.ProviderData),
|
||||
)
|
||||
return
|
||||
}
|
||||
r.client = c
|
||||
}
|
||||
|
||||
func (r *apiTokenResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||
var plan apiTokenModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
// Build the SDK request.
|
||||
createReq := cosmossdk.ConfigapiCreateAPITokenRequest{
|
||||
Name: plan.Name.ValueString(),
|
||||
}
|
||||
|
||||
if !plan.Description.IsNull() && !plan.Description.IsUnknown() {
|
||||
createReq.Description = client.StringPtr(plan.Description.ValueString())
|
||||
}
|
||||
if !plan.ReadOnly.IsNull() && !plan.ReadOnly.IsUnknown() {
|
||||
createReq.ReadOnly = client.BoolPtr(plan.ReadOnly.ValueBool())
|
||||
}
|
||||
if !plan.RestrictToConstellation.IsNull() && !plan.RestrictToConstellation.IsUnknown() {
|
||||
createReq.RestrictToConstellation = client.BoolPtr(plan.RestrictToConstellation.ValueBool())
|
||||
}
|
||||
if !plan.IpWhitelist.IsNull() && !plan.IpWhitelist.IsUnknown() {
|
||||
var ips []string
|
||||
resp.Diagnostics.Append(plan.IpWhitelist.ElementsAs(ctx, &ips, false)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
createReq.IpWhitelist = &ips
|
||||
}
|
||||
|
||||
// Call the API.
|
||||
httpResp, err := r.client.Raw.PostApiApiTokens(ctx, createReq)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error creating API token", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// The create response data is {"token":"..."} — use ParseRawResponse so we
|
||||
// can extract the token value which is only returned on create.
|
||||
rawData, err := client.ParseRawResponse(httpResp)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error parsing create API token response", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var createResult struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
if err := json.Unmarshal(rawData, &createResult); err != nil {
|
||||
resp.Diagnostics.AddError("Error unmarshalling create API token response", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
plan.Token = types.StringValue(createResult.Token)
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||
}
|
||||
|
||||
func (r *apiTokenResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||
var state apiTokenModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
name := state.Name.ValueString()
|
||||
|
||||
httpResp, err := r.client.Raw.GetApiApiTokens(ctx)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error reading API tokens", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
rawData, err := client.ParseRawResponse(httpResp)
|
||||
if err != nil {
|
||||
if client.IsNotFound(err) {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
resp.Diagnostics.AddError("Error parsing API tokens response", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// The API returns a map keyed by token name.
|
||||
var tokensMap map[string]cosmossdk.UtilsAPITokenConfig
|
||||
if err := json.Unmarshal(rawData, &tokensMap); err != nil {
|
||||
resp.Diagnostics.AddError("Error parsing API tokens response", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
tokenData, ok := tokensMap[name]
|
||||
if !ok {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
found := &tokenData
|
||||
|
||||
// Map API response back to state.
|
||||
state.Name = types.StringValue(client.StringPtrVal(found.Name))
|
||||
state.Description = types.StringValue(client.StringPtrVal(found.Description))
|
||||
|
||||
if found.RestrictToConstellation != nil {
|
||||
state.RestrictToConstellation = types.BoolValue(*found.RestrictToConstellation)
|
||||
} else {
|
||||
state.RestrictToConstellation = types.BoolValue(false)
|
||||
}
|
||||
|
||||
if found.IpWhitelist != nil {
|
||||
listVal, diags := types.ListValueFrom(ctx, types.StringType, *found.IpWhitelist)
|
||||
resp.Diagnostics.Append(diags...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
state.IpWhitelist = listVal
|
||||
} else {
|
||||
state.IpWhitelist = types.ListNull(types.StringType)
|
||||
}
|
||||
|
||||
// The token field is never returned by Read. Preserve whatever is already
|
||||
// in state (set during Create). On import it will be empty/unknown.
|
||||
// state.Token is left unchanged from what was in req.State.
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
|
||||
}
|
||||
|
||||
func (r *apiTokenResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||
var plan apiTokenModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
// Preserve the token from current state since it is not part of the update.
|
||||
var state apiTokenModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
name := plan.Name.ValueString()
|
||||
|
||||
updateReq := cosmossdk.ConfigapiUpdateAPITokenRequest{}
|
||||
|
||||
if !plan.Description.IsNull() && !plan.Description.IsUnknown() {
|
||||
updateReq.Description = client.StringPtr(plan.Description.ValueString())
|
||||
}
|
||||
if !plan.RestrictToConstellation.IsNull() && !plan.RestrictToConstellation.IsUnknown() {
|
||||
updateReq.RestrictToConstellation = client.BoolPtr(plan.RestrictToConstellation.ValueBool())
|
||||
}
|
||||
if !plan.IpWhitelist.IsNull() && !plan.IpWhitelist.IsUnknown() {
|
||||
var ips []string
|
||||
resp.Diagnostics.Append(plan.IpWhitelist.ElementsAs(ctx, &ips, false)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
updateReq.IpWhitelist = &ips
|
||||
}
|
||||
|
||||
httpResp, err := r.client.Raw.PutApiApiTokensName(ctx, name, updateReq)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error updating API token", err.Error())
|
||||
return
|
||||
}
|
||||
if err := client.CheckResponse(httpResp); err != nil {
|
||||
resp.Diagnostics.AddError("Error updating API token", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Preserve the token value from state — it is never returned after create.
|
||||
plan.Token = state.Token
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||
}
|
||||
|
||||
func (r *apiTokenResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||
var state apiTokenModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
name := state.Name.ValueString()
|
||||
|
||||
httpResp, err := r.client.Raw.DeleteApiApiTokens(ctx, cosmossdk.ConfigapiDeleteAPITokenRequest{
|
||||
Name: name,
|
||||
})
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error deleting API token", err.Error())
|
||||
return
|
||||
}
|
||||
if err := client.CheckResponse(httpResp); err != nil {
|
||||
resp.Diagnostics.AddError("Error deleting API token", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (r *apiTokenResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||
// Import by name. Note: the token value will be empty after import because
|
||||
// it is only returned during creation.
|
||||
resource.ImportStatePassthroughID(ctx, path.Root("name"), req, resp)
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
cosmossdk "github.com/azukaar/cosmos-server/go-sdk"
|
||||
"github.com/azukaar/terraform-provider-cosmos/internal/client"
|
||||
"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/booldefault"
|
||||
"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"
|
||||
)
|
||||
|
||||
// Ensure interface compliance.
|
||||
var (
|
||||
_ resource.Resource = &backupResource{}
|
||||
_ resource.ResourceWithImportState = &backupResource{}
|
||||
)
|
||||
|
||||
// NewBackupResource returns a new resource.Resource for backup configurations.
|
||||
func NewBackupResource() resource.Resource {
|
||||
return &backupResource{}
|
||||
}
|
||||
|
||||
type backupResource struct {
|
||||
client *client.CosmosClient
|
||||
}
|
||||
|
||||
type backupResourceModel struct {
|
||||
Name types.String `tfsdk:"name"`
|
||||
Repository types.String `tfsdk:"repository"`
|
||||
Source types.String `tfsdk:"source"`
|
||||
Crontab types.String `tfsdk:"crontab"`
|
||||
CrontabForget types.String `tfsdk:"crontab_forget"`
|
||||
RetentionPolicy types.String `tfsdk:"retention_policy"`
|
||||
AutoStopContainers types.Bool `tfsdk:"auto_stop_containers"`
|
||||
}
|
||||
|
||||
func (r *backupResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_backup"
|
||||
}
|
||||
|
||||
func (r *backupResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "Manages a Cosmos Server backup configuration.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"name": schema.StringAttribute{
|
||||
Description: "The unique name of the backup configuration (used as the resource ID). Changing this forces re-creation.",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
"repository": schema.StringAttribute{
|
||||
Description: "The repository path for storing backups.",
|
||||
Required: true,
|
||||
},
|
||||
"source": schema.StringAttribute{
|
||||
Description: "The source path to back up.",
|
||||
Optional: true,
|
||||
},
|
||||
"crontab": schema.StringAttribute{
|
||||
Description: "Cron schedule for running backups.",
|
||||
Optional: true,
|
||||
},
|
||||
"crontab_forget": schema.StringAttribute{
|
||||
Description: "Cron schedule for running backup forget/prune.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"retention_policy": schema.StringAttribute{
|
||||
Description: "Retention policy for backup snapshots.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"auto_stop_containers": schema.BoolAttribute{
|
||||
Description: "Whether to automatically stop containers before backing up.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Default: booldefault.StaticBool(false),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *backupResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
c, ok := req.ProviderData.(*client.CosmosClient)
|
||||
if !ok {
|
||||
resp.Diagnostics.AddError(
|
||||
"Unexpected Resource Configure Type",
|
||||
fmt.Sprintf("Expected *client.CosmosClient, got: %T", req.ProviderData),
|
||||
)
|
||||
return
|
||||
}
|
||||
r.client = c
|
||||
}
|
||||
|
||||
func (r *backupResource) backupConfigFromModel(model *backupResourceModel) cosmossdk.UtilsSingleBackupConfig {
|
||||
cfg := cosmossdk.UtilsSingleBackupConfig{
|
||||
Name: model.Name.ValueString(),
|
||||
Repository: model.Repository.ValueString(),
|
||||
}
|
||||
|
||||
if !model.Source.IsNull() && !model.Source.IsUnknown() {
|
||||
cfg.Source = client.StringPtr(model.Source.ValueString())
|
||||
}
|
||||
if !model.Crontab.IsNull() && !model.Crontab.IsUnknown() {
|
||||
cfg.Crontab = client.StringPtr(model.Crontab.ValueString())
|
||||
}
|
||||
if !model.CrontabForget.IsNull() && !model.CrontabForget.IsUnknown() {
|
||||
cfg.CrontabForget = client.StringPtr(model.CrontabForget.ValueString())
|
||||
}
|
||||
if !model.RetentionPolicy.IsNull() && !model.RetentionPolicy.IsUnknown() {
|
||||
cfg.RetentionPolicy = client.StringPtr(model.RetentionPolicy.ValueString())
|
||||
}
|
||||
if !model.AutoStopContainers.IsNull() && !model.AutoStopContainers.IsUnknown() {
|
||||
cfg.AutoStopContainers = client.BoolPtr(model.AutoStopContainers.ValueBool())
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func (r *backupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||
var plan backupResourceModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
cfg := r.backupConfigFromModel(&plan)
|
||||
|
||||
httpResp, err := r.client.Raw.PostApiBackups(ctx, cfg)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error creating backup", err.Error())
|
||||
return
|
||||
}
|
||||
if err := client.CheckResponse(httpResp); err != nil {
|
||||
resp.Diagnostics.AddError("Error creating backup", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Resolve any unknown Computed fields to null so Terraform doesn't error.
|
||||
// We don't read back server-set defaults to avoid phantom diffs on next plan.
|
||||
if plan.CrontabForget.IsUnknown() {
|
||||
plan.CrontabForget = types.StringNull()
|
||||
}
|
||||
if plan.RetentionPolicy.IsUnknown() {
|
||||
plan.RetentionPolicy = types.StringNull()
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||
}
|
||||
|
||||
func (r *backupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||
var state backupResourceModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
name := state.Name.ValueString()
|
||||
|
||||
httpResp, err := r.client.Raw.GetApiBackupsConfigName(ctx, name)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error reading backup", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
backupPtr, err := client.ParseResponse[cosmossdk.UtilsSingleBackupConfig](httpResp)
|
||||
if err != nil {
|
||||
if client.IsNotFound(err) {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
resp.Diagnostics.AddError("Error reading backup", err.Error())
|
||||
return
|
||||
}
|
||||
if backupPtr == nil {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
backup := *backupPtr
|
||||
|
||||
state.Name = types.StringValue(backup.Name)
|
||||
state.Repository = types.StringValue(backup.Repository)
|
||||
|
||||
if backup.Source != nil {
|
||||
state.Source = types.StringValue(*backup.Source)
|
||||
} else {
|
||||
state.Source = types.StringNull()
|
||||
}
|
||||
|
||||
if backup.Crontab != nil {
|
||||
state.Crontab = types.StringValue(*backup.Crontab)
|
||||
} else {
|
||||
state.Crontab = types.StringNull()
|
||||
}
|
||||
|
||||
// Only update crontab_forget and retention_policy from API if the user set them.
|
||||
// The server may populate defaults that would cause phantom diffs.
|
||||
if !state.CrontabForget.IsNull() && backup.CrontabForget != nil {
|
||||
state.CrontabForget = types.StringValue(*backup.CrontabForget)
|
||||
}
|
||||
|
||||
if !state.RetentionPolicy.IsNull() && backup.RetentionPolicy != nil {
|
||||
state.RetentionPolicy = types.StringValue(*backup.RetentionPolicy)
|
||||
}
|
||||
|
||||
if backup.AutoStopContainers != nil {
|
||||
state.AutoStopContainers = types.BoolValue(*backup.AutoStopContainers)
|
||||
} else {
|
||||
state.AutoStopContainers = types.BoolNull()
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
|
||||
}
|
||||
|
||||
func (r *backupResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||
var plan backupResourceModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
cfg := r.backupConfigFromModel(&plan)
|
||||
|
||||
httpResp, err := r.client.Raw.PostApiBackupsEdit(ctx, cfg)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error updating backup", err.Error())
|
||||
return
|
||||
}
|
||||
if err := client.CheckResponse(httpResp); err != nil {
|
||||
resp.Diagnostics.AddError("Error updating backup", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||
}
|
||||
|
||||
func (r *backupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||
var state backupResourceModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
httpResp, err := r.client.Raw.DeleteApiBackupsName(ctx, state.Name.ValueString())
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error deleting backup", err.Error())
|
||||
return
|
||||
}
|
||||
if err := client.CheckResponse(httpResp); err != nil {
|
||||
resp.Diagnostics.AddError("Error deleting backup", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (r *backupResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||
resource.ImportStatePassthroughID(ctx, path.Root("name"), req, resp)
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/azukaar/terraform-provider-cosmos/internal/client"
|
||||
"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/booldefault"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
)
|
||||
|
||||
var (
|
||||
_ resource.Resource = &constellationResource{}
|
||||
_ resource.ResourceWithImportState = &constellationResource{}
|
||||
)
|
||||
|
||||
func NewConstellationResource() resource.Resource {
|
||||
return &constellationResource{}
|
||||
}
|
||||
|
||||
type constellationResource struct {
|
||||
client *client.CosmosClient
|
||||
}
|
||||
|
||||
type constellationModel struct {
|
||||
DeviceName types.String `tfsdk:"device_name"`
|
||||
Hostname types.String `tfsdk:"hostname"`
|
||||
IPRange types.String `tfsdk:"ip_range"`
|
||||
IsLighthouse types.Bool `tfsdk:"is_lighthouse"`
|
||||
Enabled types.Bool `tfsdk:"enabled"`
|
||||
}
|
||||
|
||||
func (r *constellationResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_constellation"
|
||||
}
|
||||
|
||||
func (r *constellationResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "Initializes a Cosmos Constellation VPN network. This is a one-time setup that generates CA certificates and creates the lighthouse device. Changing any field triggers a full reset and re-initialization.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"device_name": schema.StringAttribute{
|
||||
Description: "Name for the lighthouse device on this server.",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
"hostname": schema.StringAttribute{
|
||||
Description: "Public hostname for the Constellation VPN (e.g. vpn.example.com).",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
"ip_range": schema.StringAttribute{
|
||||
Description: "IP range for the VPN network in CIDR notation.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Default: stringdefault.StaticString("192.168.201.0/24"),
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
"is_lighthouse": schema.BoolAttribute{
|
||||
Description: "Whether this device acts as a lighthouse (should be true for the initial server).",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Default: booldefault.StaticBool(true),
|
||||
},
|
||||
"enabled": schema.BoolAttribute{
|
||||
Description: "Whether the Constellation VPN is currently enabled.",
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *constellationResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
c, ok := req.ProviderData.(*client.CosmosClient)
|
||||
if !ok {
|
||||
resp.Diagnostics.AddError(
|
||||
"Unexpected Resource Configure Type",
|
||||
fmt.Sprintf("Expected *client.CosmosClient, got %T", req.ProviderData),
|
||||
)
|
||||
return
|
||||
}
|
||||
r.client = c
|
||||
}
|
||||
|
||||
func (r *constellationResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||
var plan constellationModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
body := map[string]interface{}{
|
||||
"deviceName": plan.DeviceName.ValueString(),
|
||||
"hostname": plan.Hostname.ValueString(),
|
||||
"ipRange": plan.IPRange.ValueString(),
|
||||
"isLighthouse": plan.IsLighthouse.ValueBool(),
|
||||
}
|
||||
|
||||
httpResp, err := r.client.Raw.PostApiConstellationCreate(ctx, body)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error creating constellation", err.Error())
|
||||
return
|
||||
}
|
||||
if err := client.CheckResponse(httpResp); err != nil {
|
||||
resp.Diagnostics.AddError("Error creating constellation", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
plan.Enabled = types.BoolValue(true)
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||
}
|
||||
|
||||
func (r *constellationResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||
var state constellationModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if constellation is still active by calling the config endpoint.
|
||||
httpResp, err := r.client.Raw.GetApiConstellationConfig(ctx)
|
||||
if err != nil {
|
||||
// If we can't reach the API, preserve state.
|
||||
return
|
||||
}
|
||||
|
||||
_, err = client.ParseRawResponse(httpResp)
|
||||
if err != nil {
|
||||
if client.IsNotFound(err) {
|
||||
// Constellation was reset/disabled externally — remove from state.
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
// Constellation was reset/disabled externally — remove from state.
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
// Constellation is active. User-set fields are preserved from state.
|
||||
state.Enabled = types.BoolValue(true)
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
|
||||
}
|
||||
|
||||
func (r *constellationResource) Update(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||
// All fields are RequiresReplace, so this should never be called.
|
||||
resp.Diagnostics.AddError("Update not supported", "All constellation fields require replacement. This is a provider bug.")
|
||||
}
|
||||
|
||||
func (r *constellationResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||
httpResp, err := r.client.Raw.GetApiConstellationReset(ctx)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error resetting constellation", err.Error())
|
||||
return
|
||||
}
|
||||
if err := client.CheckResponse(httpResp); err != nil {
|
||||
resp.Diagnostics.AddError("Error resetting constellation", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (r *constellationResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||
resource.ImportStatePassthroughID(ctx, path.Root("device_name"), req, resp)
|
||||
}
|
||||
@@ -0,0 +1,458 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
cosmossdk "github.com/azukaar/cosmos-server/go-sdk"
|
||||
"github.com/azukaar/terraform-provider-cosmos/internal/client"
|
||||
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||
"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/booldefault"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
|
||||
"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"
|
||||
)
|
||||
|
||||
var (
|
||||
_ resource.Resource = &constellationDeviceResource{}
|
||||
_ resource.ResourceWithImportState = &constellationDeviceResource{}
|
||||
)
|
||||
|
||||
// NewConstellationDeviceResource returns a new constellation device resource instance.
|
||||
func NewConstellationDeviceResource() resource.Resource {
|
||||
return &constellationDeviceResource{}
|
||||
}
|
||||
|
||||
type constellationDeviceResource struct {
|
||||
client *client.CosmosClient
|
||||
}
|
||||
|
||||
// ---------- Terraform model ----------
|
||||
|
||||
type constellationDeviceResourceModel struct {
|
||||
DeviceName types.String `tfsdk:"device_name"`
|
||||
IP types.String `tfsdk:"ip"`
|
||||
Nickname types.String `tfsdk:"nickname"`
|
||||
IsLighthouse types.Bool `tfsdk:"is_lighthouse"`
|
||||
IsRelay types.Bool `tfsdk:"is_relay"`
|
||||
IsExitNode types.Bool `tfsdk:"is_exit_node"`
|
||||
IsLoadBalancer types.Bool `tfsdk:"is_load_balancer"`
|
||||
PublicHostname types.String `tfsdk:"public_hostname"`
|
||||
Invisible types.Bool `tfsdk:"invisible"`
|
||||
CosmosNode types.Int64 `tfsdk:"cosmos_node"`
|
||||
Config types.String `tfsdk:"config"`
|
||||
}
|
||||
|
||||
type deviceJSON struct {
|
||||
DeviceName string `json:"deviceName"`
|
||||
IP string `json:"ip"`
|
||||
Nickname string `json:"nickname"`
|
||||
IsLighthouse *bool `json:"isLighthouse,omitempty"`
|
||||
IsRelay *bool `json:"isRelay,omitempty"`
|
||||
IsExitNode *bool `json:"isExitNode,omitempty"`
|
||||
IsLoadBalancer *bool `json:"isLoadBalancer,omitempty"`
|
||||
PublicHostname string `json:"PublicHostname,omitempty"`
|
||||
Invisible *bool `json:"invisible,omitempty"`
|
||||
CosmosNode *int `json:"cosmosNode,omitempty"`
|
||||
Port string `json:"port,omitempty"`
|
||||
}
|
||||
|
||||
// ---------- Resource interface ----------
|
||||
|
||||
func (r *constellationDeviceResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_constellation_device"
|
||||
}
|
||||
|
||||
func (r *constellationDeviceResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "Manages a Cosmos Server constellation VPN device.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"device_name": schema.StringAttribute{
|
||||
Description: "The unique name of the device.",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
"ip": schema.StringAttribute{
|
||||
Description: "The IP address assigned to the device in the constellation network.",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
"is_lighthouse": schema.BoolAttribute{
|
||||
Description: "Whether this device acts as a lighthouse.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Default: booldefault.StaticBool(false),
|
||||
},
|
||||
"is_relay": schema.BoolAttribute{
|
||||
Description: "Whether this device acts as a relay.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Default: booldefault.StaticBool(false),
|
||||
},
|
||||
"is_exit_node": schema.BoolAttribute{
|
||||
Description: "Whether this device acts as an exit node.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Default: booldefault.StaticBool(false),
|
||||
},
|
||||
"is_load_balancer": schema.BoolAttribute{
|
||||
Description: "Whether this device acts as a load balancer.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Default: booldefault.StaticBool(false),
|
||||
},
|
||||
"public_hostname": schema.StringAttribute{
|
||||
Description: "The public hostname of the device.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"nickname": schema.StringAttribute{
|
||||
Description: "The device nickname. Defaults to device_name if not set.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"invisible": schema.BoolAttribute{
|
||||
Description: "Whether the device is hidden from the device list.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Default: booldefault.StaticBool(false),
|
||||
},
|
||||
"cosmos_node": schema.Int64Attribute{
|
||||
Description: "Cosmos node type (0 = not a cosmos node, 1 = agent, 2 = full).",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Default: int64default.StaticInt64(0),
|
||||
},
|
||||
"config": schema.StringAttribute{
|
||||
Description: "The full Nebula YAML configuration for this device. Only available after creation — not returned by subsequent reads. On import this value will be empty.",
|
||||
Computed: true,
|
||||
Sensitive: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *constellationDeviceResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
c, ok := req.ProviderData.(*client.CosmosClient)
|
||||
if !ok {
|
||||
resp.Diagnostics.AddError(
|
||||
"Unexpected Resource Configure Type",
|
||||
fmt.Sprintf("Expected *client.CosmosClient, got: %T", req.ProviderData),
|
||||
)
|
||||
return
|
||||
}
|
||||
r.client = c
|
||||
}
|
||||
|
||||
// ---------- CRUD ----------
|
||||
|
||||
func (r *constellationDeviceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||
var plan constellationDeviceResourceModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
deviceName := plan.DeviceName.ValueString()
|
||||
nickname := deviceName
|
||||
if !plan.Nickname.IsNull() && !plan.Nickname.IsUnknown() {
|
||||
nickname = plan.Nickname.ValueString()
|
||||
}
|
||||
|
||||
body := cosmossdk.ConstellationDeviceCreateRequestJSON{
|
||||
DeviceName: deviceName,
|
||||
Ip: plan.IP.ValueString(),
|
||||
Nickname: nickname,
|
||||
}
|
||||
if !plan.IsLighthouse.IsNull() && !plan.IsLighthouse.IsUnknown() {
|
||||
body.IsLighthouse = client.BoolPtr(plan.IsLighthouse.ValueBool())
|
||||
}
|
||||
if !plan.IsRelay.IsNull() && !plan.IsRelay.IsUnknown() {
|
||||
body.IsRelay = client.BoolPtr(plan.IsRelay.ValueBool())
|
||||
}
|
||||
if !plan.IsExitNode.IsNull() && !plan.IsExitNode.IsUnknown() {
|
||||
body.IsExitNode = client.BoolPtr(plan.IsExitNode.ValueBool())
|
||||
}
|
||||
if !plan.IsLoadBalancer.IsNull() && !plan.IsLoadBalancer.IsUnknown() {
|
||||
body.IsLoadBalancer = client.BoolPtr(plan.IsLoadBalancer.ValueBool())
|
||||
}
|
||||
if !plan.PublicHostname.IsNull() && !plan.PublicHostname.IsUnknown() {
|
||||
body.PublicHostname = client.StringPtr(plan.PublicHostname.ValueString())
|
||||
}
|
||||
if !plan.Invisible.IsNull() && !plan.Invisible.IsUnknown() {
|
||||
body.Invisible = client.BoolPtr(plan.Invisible.ValueBool())
|
||||
}
|
||||
if !plan.CosmosNode.IsNull() && !plan.CosmosNode.IsUnknown() {
|
||||
v := int(plan.CosmosNode.ValueInt64())
|
||||
body.CosmosNode = &v
|
||||
}
|
||||
|
||||
httpResp, err := r.client.Raw.PostApiConstellationDevices(ctx, body)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error creating constellation device", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Capture the Config from the create response — it's only returned once.
|
||||
rawData, err := client.ParseRawResponse(httpResp)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error creating constellation device", err.Error())
|
||||
return
|
||||
}
|
||||
if rawData != nil {
|
||||
var createResult struct {
|
||||
Config string `json:"Config"`
|
||||
}
|
||||
if err := json.Unmarshal(rawData, &createResult); err == nil && createResult.Config != "" {
|
||||
plan.Config = types.StringValue(createResult.Config)
|
||||
}
|
||||
}
|
||||
|
||||
// Read back to populate computed fields.
|
||||
if !r.readDevice(ctx, plan.DeviceName.ValueString(), &plan, &resp.Diagnostics) {
|
||||
resp.Diagnostics.AddError("Error reading constellation device", "Device not found after creation")
|
||||
return
|
||||
}
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||
}
|
||||
|
||||
func (r *constellationDeviceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||
var state constellationDeviceResourceModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
deviceName := state.DeviceName.ValueString()
|
||||
|
||||
httpResp, err := r.client.Raw.GetApiConstellationDevices(ctx)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error reading constellation devices", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
rawData, err := client.ParseRawResponse(httpResp)
|
||||
if err != nil {
|
||||
if client.IsNotFound(err) {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
resp.Diagnostics.AddError("Error parsing constellation devices response", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if rawData == nil {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
device, err := findDevice(rawData, deviceName)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error locating constellation device", err.Error())
|
||||
return
|
||||
}
|
||||
if device == nil {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
populateDeviceModel(device, &state)
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
|
||||
}
|
||||
|
||||
func (r *constellationDeviceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||
var plan constellationDeviceResourceModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
body := cosmossdk.ConstellationDeviceEditRequestJSON{}
|
||||
if !plan.IsLighthouse.IsNull() && !plan.IsLighthouse.IsUnknown() {
|
||||
body.IsLighthouse = client.BoolPtr(plan.IsLighthouse.ValueBool())
|
||||
}
|
||||
if !plan.IsRelay.IsNull() && !plan.IsRelay.IsUnknown() {
|
||||
body.IsRelay = client.BoolPtr(plan.IsRelay.ValueBool())
|
||||
}
|
||||
if !plan.IsExitNode.IsNull() && !plan.IsExitNode.IsUnknown() {
|
||||
body.IsExitNode = client.BoolPtr(plan.IsExitNode.ValueBool())
|
||||
}
|
||||
if !plan.IsLoadBalancer.IsNull() && !plan.IsLoadBalancer.IsUnknown() {
|
||||
body.IsLoadBalancer = client.BoolPtr(plan.IsLoadBalancer.ValueBool())
|
||||
}
|
||||
|
||||
httpResp, err := r.client.Raw.PostApiConstellationEditDevice(ctx, body)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error updating constellation device", err.Error())
|
||||
return
|
||||
}
|
||||
if err := client.CheckResponse(httpResp); err != nil {
|
||||
resp.Diagnostics.AddError("Error updating constellation device", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Read back to refresh state.
|
||||
if !r.readDevice(ctx, plan.DeviceName.ValueString(), &plan, &resp.Diagnostics) {
|
||||
resp.Diagnostics.AddError("Error reading constellation device", "Device not found after update")
|
||||
return
|
||||
}
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||
}
|
||||
|
||||
func (r *constellationDeviceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||
var state constellationDeviceResourceModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
name := state.DeviceName.ValueString()
|
||||
httpResp, err := r.client.Raw.PostApiConstellationBlock(ctx, cosmossdk.ConstellationDeviceBlockRequestJSON{
|
||||
DeviceName: name,
|
||||
Nickname: name,
|
||||
Block: client.BoolPtr(true),
|
||||
})
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error deleting (blocking) constellation device", err.Error())
|
||||
return
|
||||
}
|
||||
if err := client.CheckResponse(httpResp); err != nil {
|
||||
resp.Diagnostics.AddError("Error deleting (blocking) constellation device", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (r *constellationDeviceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||
resource.ImportStatePassthroughID(ctx, path.Root("device_name"), req, resp)
|
||||
}
|
||||
|
||||
// ---------- helpers ----------
|
||||
|
||||
// readDevice fetches the device list from the API, finds the device by name,
|
||||
// and populates the Terraform model. The response data structure may be a map
|
||||
// keyed by device name or a list — we try both.
|
||||
// It returns false if the device was not found (404), in which case no diagnostic
|
||||
// error is added — the caller should remove the resource from state.
|
||||
func (r *constellationDeviceResource) readDevice(ctx context.Context, deviceName string, model *constellationDeviceResourceModel, diags *diag.Diagnostics) bool {
|
||||
httpResp, err := r.client.Raw.GetApiConstellationDevices(ctx)
|
||||
if err != nil {
|
||||
diags.AddError("Error reading constellation devices", err.Error())
|
||||
return true
|
||||
}
|
||||
|
||||
rawData, err := client.ParseRawResponse(httpResp)
|
||||
if err != nil {
|
||||
if client.IsNotFound(err) {
|
||||
return false
|
||||
}
|
||||
diags.AddError("Error parsing constellation devices response", err.Error())
|
||||
return true
|
||||
}
|
||||
if rawData == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
device, err := findDevice(rawData, deviceName)
|
||||
if err != nil {
|
||||
diags.AddError("Error locating constellation device", err.Error())
|
||||
return true
|
||||
}
|
||||
if device == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
populateDeviceModel(device, model)
|
||||
return true
|
||||
}
|
||||
|
||||
// populateDeviceModel maps the raw device JSON fields to the Terraform model.
|
||||
func populateDeviceModel(device *deviceJSON, model *constellationDeviceResourceModel) {
|
||||
model.DeviceName = types.StringValue(device.DeviceName)
|
||||
model.IP = types.StringValue(device.IP)
|
||||
|
||||
if device.Nickname != "" {
|
||||
model.Nickname = types.StringValue(device.Nickname)
|
||||
} else {
|
||||
model.Nickname = types.StringValue("")
|
||||
}
|
||||
|
||||
model.IsLighthouse = types.BoolValue(client.BoolPtrVal(device.IsLighthouse))
|
||||
model.IsRelay = types.BoolValue(client.BoolPtrVal(device.IsRelay))
|
||||
model.IsExitNode = types.BoolValue(client.BoolPtrVal(device.IsExitNode))
|
||||
model.IsLoadBalancer = types.BoolValue(client.BoolPtrVal(device.IsLoadBalancer))
|
||||
model.PublicHostname = types.StringValue(device.PublicHostname)
|
||||
model.Invisible = types.BoolValue(client.BoolPtrVal(device.Invisible))
|
||||
if device.CosmosNode != nil {
|
||||
model.CosmosNode = types.Int64Value(int64(*device.CosmosNode))
|
||||
} else {
|
||||
model.CosmosNode = types.Int64Value(0)
|
||||
}
|
||||
}
|
||||
|
||||
// findDevice tries to locate a device by name in the raw JSON data. The API
|
||||
// may return either a map[string]Device (keyed by device name) or a []Device.
|
||||
func findDevice(rawData json.RawMessage, deviceName string) (*deviceJSON, error) {
|
||||
// Try map keyed by device name first.
|
||||
var deviceMap map[string]json.RawMessage
|
||||
if err := json.Unmarshal(rawData, &deviceMap); err == nil {
|
||||
// Look for an exact key match.
|
||||
if raw, ok := deviceMap[deviceName]; ok {
|
||||
var d deviceJSON
|
||||
if err := json.Unmarshal(raw, &d); err != nil {
|
||||
return nil, fmt.Errorf("unmarshalling device from map: %w", err)
|
||||
}
|
||||
return &d, nil
|
||||
}
|
||||
// The map may contain devices where the key differs from the
|
||||
// deviceName field. Iterate values and check the field.
|
||||
for _, raw := range deviceMap {
|
||||
var d deviceJSON
|
||||
if err := json.Unmarshal(raw, &d); err != nil {
|
||||
continue
|
||||
}
|
||||
if d.DeviceName == deviceName {
|
||||
return &d, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try as a list.
|
||||
var deviceList []json.RawMessage
|
||||
if err := json.Unmarshal(rawData, &deviceList); err == nil {
|
||||
for _, raw := range deviceList {
|
||||
var d deviceJSON
|
||||
if err := json.Unmarshal(raw, &d); err != nil {
|
||||
continue
|
||||
}
|
||||
if d.DeviceName == deviceName {
|
||||
return &d, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
cosmossdk "github.com/azukaar/cosmos-server/go-sdk"
|
||||
"github.com/azukaar/terraform-provider-cosmos/internal/client"
|
||||
"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"
|
||||
)
|
||||
|
||||
var (
|
||||
_ resource.Resource = &constellationDNSResource{}
|
||||
_ resource.ResourceWithImportState = &constellationDNSResource{}
|
||||
)
|
||||
|
||||
type constellationDNSResource struct {
|
||||
client *client.CosmosClient
|
||||
}
|
||||
|
||||
type constellationDNSResourceModel struct {
|
||||
Key types.String `tfsdk:"key"`
|
||||
Type types.String `tfsdk:"type"`
|
||||
Value types.String `tfsdk:"value"`
|
||||
}
|
||||
|
||||
func NewConstellationDNSResource() resource.Resource {
|
||||
return &constellationDNSResource{}
|
||||
}
|
||||
|
||||
func (r *constellationDNSResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_constellation_dns"
|
||||
}
|
||||
|
||||
func (r *constellationDNSResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "Manages a Cosmos Constellation DNS entry.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"key": schema.StringAttribute{
|
||||
Description: "The DNS entry key (used as the resource ID).",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
"type": schema.StringAttribute{
|
||||
Description: "The DNS record type (e.g. \"A\", \"CNAME\").",
|
||||
Required: true,
|
||||
},
|
||||
"value": schema.StringAttribute{
|
||||
Description: "The DNS record value.",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *constellationDNSResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
|
||||
cosmosClient, ok := req.ProviderData.(*client.CosmosClient)
|
||||
if !ok {
|
||||
resp.Diagnostics.AddError(
|
||||
"Unexpected Resource Configure Type",
|
||||
fmt.Sprintf("Expected *client.CosmosClient, got: %T", req.ProviderData),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
r.client = cosmosClient
|
||||
}
|
||||
|
||||
func (r *constellationDNSResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||
var plan constellationDNSResourceModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
entry := cosmossdk.UtilsConstellationDNSEntry{
|
||||
Key: plan.Key.ValueString(),
|
||||
Type: client.StringPtr(plan.Type.ValueString()),
|
||||
Value: client.StringPtr(plan.Value.ValueString()),
|
||||
}
|
||||
|
||||
httpResp, err := r.client.Raw.PostApiConstellationDns(ctx, entry)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error creating constellation DNS entry", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := client.CheckResponse(httpResp); err != nil {
|
||||
resp.Diagnostics.AddError("Error creating constellation DNS entry", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||
}
|
||||
|
||||
func (r *constellationDNSResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||
var state constellationDNSResourceModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
key := state.Key.ValueString()
|
||||
|
||||
httpResp, err := r.client.Raw.GetApiConstellationDnsKey(ctx, key)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error reading constellation DNS entry", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
entry, err := client.ParseResponse[cosmossdk.UtilsConstellationDNSEntry](httpResp)
|
||||
if err != nil {
|
||||
if client.IsNotFound(err) {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
resp.Diagnostics.AddError("Error reading constellation DNS entry", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if entry == nil {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
state.Key = types.StringValue(entry.Key)
|
||||
state.Type = types.StringValue(client.StringPtrVal(entry.Type))
|
||||
state.Value = types.StringValue(client.StringPtrVal(entry.Value))
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
|
||||
}
|
||||
|
||||
func (r *constellationDNSResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||
var plan constellationDNSResourceModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
key := plan.Key.ValueString()
|
||||
|
||||
entry := cosmossdk.UtilsConstellationDNSEntry{
|
||||
Key: key,
|
||||
Type: client.StringPtr(plan.Type.ValueString()),
|
||||
Value: client.StringPtr(plan.Value.ValueString()),
|
||||
}
|
||||
|
||||
httpResp, err := r.client.Raw.PutApiConstellationDnsKey(ctx, key, entry)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error updating constellation DNS entry", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := client.CheckResponse(httpResp); err != nil {
|
||||
resp.Diagnostics.AddError("Error updating constellation DNS entry", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||
}
|
||||
|
||||
func (r *constellationDNSResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||
var state constellationDNSResourceModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
key := state.Key.ValueString()
|
||||
|
||||
httpResp, err := r.client.Raw.DeleteApiConstellationDnsKey(ctx, key)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error deleting constellation DNS entry", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := client.CheckResponse(httpResp); err != nil {
|
||||
resp.Diagnostics.AddError("Error deleting constellation DNS entry", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (r *constellationDNSResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||
resource.ImportStatePassthroughID(ctx, path.Root("key"), req, resp)
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
cosmossdk "github.com/azukaar/cosmos-server/go-sdk"
|
||||
"github.com/azukaar/terraform-provider-cosmos/internal/client"
|
||||
"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/booldefault"
|
||||
"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"
|
||||
)
|
||||
|
||||
var (
|
||||
_ resource.Resource = &cronJobResource{}
|
||||
_ resource.ResourceWithImportState = &cronJobResource{}
|
||||
)
|
||||
|
||||
type cronJobResource struct {
|
||||
client *client.CosmosClient
|
||||
}
|
||||
|
||||
type cronJobResourceModel struct {
|
||||
Name types.String `tfsdk:"name"`
|
||||
Crontab types.String `tfsdk:"crontab"`
|
||||
Command types.String `tfsdk:"command"`
|
||||
Container types.String `tfsdk:"container"`
|
||||
Enabled types.Bool `tfsdk:"enabled"`
|
||||
}
|
||||
|
||||
func NewCronJobResource() resource.Resource {
|
||||
return &cronJobResource{}
|
||||
}
|
||||
|
||||
func (r *cronJobResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_cron_job"
|
||||
}
|
||||
|
||||
func (r *cronJobResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "Manages a Cosmos cron job.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"name": schema.StringAttribute{
|
||||
Description: "The cron job name (used as the resource ID).",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
"crontab": schema.StringAttribute{
|
||||
Description: "The cron schedule expression (e.g. \"0 * * * *\").",
|
||||
Required: true,
|
||||
},
|
||||
"command": schema.StringAttribute{
|
||||
Description: "The command to execute.",
|
||||
Optional: true,
|
||||
},
|
||||
"container": schema.StringAttribute{
|
||||
Description: "The container to run the command in.",
|
||||
Optional: true,
|
||||
},
|
||||
"enabled": schema.BoolAttribute{
|
||||
Description: "Whether the cron job is enabled. Default: true.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Default: booldefault.StaticBool(true),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *cronJobResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
|
||||
cosmosClient, ok := req.ProviderData.(*client.CosmosClient)
|
||||
if !ok {
|
||||
resp.Diagnostics.AddError(
|
||||
"Unexpected Resource Configure Type",
|
||||
fmt.Sprintf("Expected *client.CosmosClient, got: %T", req.ProviderData),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
r.client = cosmosClient
|
||||
}
|
||||
|
||||
func (r *cronJobResource) cronConfigFromModel(model *cronJobResourceModel) cosmossdk.UtilsCRONConfig {
|
||||
cfg := cosmossdk.UtilsCRONConfig{
|
||||
Name: model.Name.ValueString(),
|
||||
Crontab: client.StringPtr(model.Crontab.ValueString()),
|
||||
Enabled: client.BoolPtr(model.Enabled.ValueBool()),
|
||||
}
|
||||
|
||||
if !model.Command.IsNull() && !model.Command.IsUnknown() {
|
||||
cfg.Command = client.StringPtr(model.Command.ValueString())
|
||||
}
|
||||
|
||||
if !model.Container.IsNull() && !model.Container.IsUnknown() {
|
||||
cfg.Container = client.StringPtr(model.Container.ValueString())
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func (r *cronJobResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||
var plan cronJobResourceModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
cfg := r.cronConfigFromModel(&plan)
|
||||
|
||||
httpResp, err := r.client.Raw.PostApiCron(ctx, cfg)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error creating cron job", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := client.CheckResponse(httpResp); err != nil {
|
||||
resp.Diagnostics.AddError("Error creating cron job", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||
}
|
||||
|
||||
func (r *cronJobResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||
var state cronJobResourceModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
name := state.Name.ValueString()
|
||||
|
||||
httpResp, err := r.client.Raw.GetApiCronName(ctx, name, cosmossdk.UtilsCRONConfig{Name: name})
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error reading cron job", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
cronCfg, err := client.ParseResponse[cosmossdk.UtilsCRONConfig](httpResp)
|
||||
if err != nil {
|
||||
if client.IsNotFound(err) {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
resp.Diagnostics.AddError("Error reading cron job", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if cronCfg == nil {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
state.Name = types.StringValue(cronCfg.Name)
|
||||
state.Crontab = types.StringValue(client.StringPtrVal(cronCfg.Crontab))
|
||||
state.Enabled = types.BoolValue(client.BoolPtrVal(cronCfg.Enabled))
|
||||
|
||||
if cronCfg.Command != nil {
|
||||
state.Command = types.StringValue(client.StringPtrVal(cronCfg.Command))
|
||||
} else {
|
||||
state.Command = types.StringNull()
|
||||
}
|
||||
|
||||
if cronCfg.Container != nil {
|
||||
state.Container = types.StringValue(client.StringPtrVal(cronCfg.Container))
|
||||
} else {
|
||||
state.Container = types.StringNull()
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
|
||||
}
|
||||
|
||||
func (r *cronJobResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||
var plan cronJobResourceModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
name := plan.Name.ValueString()
|
||||
cfg := r.cronConfigFromModel(&plan)
|
||||
|
||||
httpResp, err := r.client.Raw.PutApiCronName(ctx, name, cfg)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error updating cron job", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := client.CheckResponse(httpResp); err != nil {
|
||||
resp.Diagnostics.AddError("Error updating cron job", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||
}
|
||||
|
||||
func (r *cronJobResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||
var state cronJobResourceModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
name := state.Name.ValueString()
|
||||
|
||||
httpResp, err := r.client.Raw.DeleteApiCronName(ctx, name, cosmossdk.UtilsCRONConfig{Name: name})
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error deleting cron job", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := client.CheckResponse(httpResp); err != nil {
|
||||
resp.Diagnostics.AddError("Error deleting cron job", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (r *cronJobResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||
resource.ImportStatePassthroughID(ctx, path.Root("name"), req, resp)
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
cosmossdk "github.com/azukaar/cosmos-server/go-sdk"
|
||||
"github.com/azukaar/terraform-provider-cosmos/internal/client"
|
||||
"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/boolplanmodifier"
|
||||
"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"
|
||||
)
|
||||
|
||||
// Ensure interface compliance.
|
||||
var (
|
||||
_ resource.Resource = &dockerNetworkResource{}
|
||||
_ resource.ResourceWithImportState = &dockerNetworkResource{}
|
||||
)
|
||||
|
||||
// NewDockerNetworkResource returns a new resource.Resource for Docker networks.
|
||||
func NewDockerNetworkResource() resource.Resource {
|
||||
return &dockerNetworkResource{}
|
||||
}
|
||||
|
||||
type dockerNetworkResource struct {
|
||||
client *client.CosmosClient
|
||||
}
|
||||
|
||||
type dockerNetworkResourceModel struct {
|
||||
ID types.String `tfsdk:"id"`
|
||||
Name types.String `tfsdk:"name"`
|
||||
Driver types.String `tfsdk:"driver"`
|
||||
Subnet types.String `tfsdk:"subnet"`
|
||||
AttachCosmos types.Bool `tfsdk:"attach_cosmos"`
|
||||
}
|
||||
|
||||
func (r *dockerNetworkResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_docker_network"
|
||||
}
|
||||
|
||||
func (r *dockerNetworkResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "Manages a Docker network through Cosmos Server.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"id": schema.StringAttribute{
|
||||
Description: "The Docker network ID assigned on creation.",
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
},
|
||||
},
|
||||
"name": schema.StringAttribute{
|
||||
Description: "The name of the Docker network. Changing this forces re-creation.",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
"driver": schema.StringAttribute{
|
||||
Description: "The network driver to use (e.g. bridge, overlay). Changing this forces re-creation.",
|
||||
Optional: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
"subnet": schema.StringAttribute{
|
||||
Description: "The subnet in CIDR format for the network. Changing this forces re-creation.",
|
||||
Optional: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
"attach_cosmos": schema.BoolAttribute{
|
||||
Description: "Whether to attach the Cosmos container to this network. Changing this forces re-creation.",
|
||||
Optional: true,
|
||||
PlanModifiers: []planmodifier.Bool{
|
||||
boolplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *dockerNetworkResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
c, ok := req.ProviderData.(*client.CosmosClient)
|
||||
if !ok {
|
||||
resp.Diagnostics.AddError(
|
||||
"Unexpected Resource Configure Type",
|
||||
fmt.Sprintf("Expected *client.CosmosClient, got: %T", req.ProviderData),
|
||||
)
|
||||
return
|
||||
}
|
||||
r.client = c
|
||||
}
|
||||
|
||||
func (r *dockerNetworkResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||
var plan dockerNetworkResourceModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
payload := cosmossdk.DockerCreateNetworkPayload{
|
||||
Name: plan.Name.ValueString(),
|
||||
}
|
||||
if !plan.Driver.IsNull() && !plan.Driver.IsUnknown() {
|
||||
payload.Driver = client.StringPtr(plan.Driver.ValueString())
|
||||
}
|
||||
if !plan.Subnet.IsNull() && !plan.Subnet.IsUnknown() {
|
||||
payload.Subnet = client.StringPtr(plan.Subnet.ValueString())
|
||||
}
|
||||
if !plan.AttachCosmos.IsNull() && !plan.AttachCosmos.IsUnknown() {
|
||||
payload.AttachCosmos = client.BoolPtr(plan.AttachCosmos.ValueBool())
|
||||
}
|
||||
|
||||
httpResp, err := r.client.Raw.PostApiNetworks(ctx, payload)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error creating Docker network", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
rawData, err := client.ParseRawResponse(httpResp)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error creating Docker network", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// The response data may contain the network ID in various formats.
|
||||
// Try to extract it as a JSON object with an "Id" field first,
|
||||
// then fall back to a plain string.
|
||||
var networkID string
|
||||
var idObj struct {
|
||||
ID string `json:"Id"`
|
||||
}
|
||||
if err := json.Unmarshal(rawData, &idObj); err == nil && idObj.ID != "" {
|
||||
networkID = idObj.ID
|
||||
} else {
|
||||
// Try as a plain string.
|
||||
if err := json.Unmarshal(rawData, &networkID); err != nil || networkID == "" {
|
||||
// Fall back: read the network list to find by name.
|
||||
foundID, findErr := r.findNetworkIDByName(ctx, plan.Name.ValueString())
|
||||
if findErr != nil {
|
||||
resp.Diagnostics.AddError("Error determining Docker network ID after creation", findErr.Error())
|
||||
return
|
||||
}
|
||||
networkID = foundID
|
||||
}
|
||||
}
|
||||
|
||||
plan.ID = types.StringValue(networkID)
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||
}
|
||||
|
||||
func (r *dockerNetworkResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||
var state dockerNetworkResourceModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
networkID := state.ID.ValueString()
|
||||
networkName := state.Name.ValueString()
|
||||
|
||||
httpResp, err := r.client.Raw.GetApiNetworks(ctx)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error reading Docker networks", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
rawData, err := client.ParseRawResponse(httpResp)
|
||||
if err != nil {
|
||||
if client.IsNotFound(err) {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
resp.Diagnostics.AddError("Error reading Docker networks", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var networks []networkInfo
|
||||
if err := json.Unmarshal(rawData, &networks); err != nil {
|
||||
resp.Diagnostics.AddError("Error parsing Docker networks response", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var found *networkInfo
|
||||
for i := range networks {
|
||||
if networks[i].ID == networkID {
|
||||
found = &networks[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
// Fall back to matching by name if ID was not found.
|
||||
if found == nil {
|
||||
for i := range networks {
|
||||
if networks[i].Name == networkName {
|
||||
found = &networks[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if found == nil {
|
||||
// Network no longer exists; remove from state.
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
state.ID = types.StringValue(found.ID)
|
||||
state.Name = types.StringValue(found.Name)
|
||||
|
||||
if found.Driver != "" {
|
||||
state.Driver = types.StringValue(found.Driver)
|
||||
} else if !state.Driver.IsNull() {
|
||||
state.Driver = types.StringNull()
|
||||
}
|
||||
|
||||
// Subnet and AttachCosmos are creation-time only and are not returned
|
||||
// by the list endpoint, so we preserve the existing state values.
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
|
||||
}
|
||||
|
||||
func (r *dockerNetworkResource) Update(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||
// All attributes require replacement; no in-place update is supported.
|
||||
resp.Diagnostics.AddError(
|
||||
"Update not supported",
|
||||
"All attributes of cosmos_docker_network require replacement. This method should never be called.",
|
||||
)
|
||||
}
|
||||
|
||||
func (r *dockerNetworkResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||
var state dockerNetworkResourceModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
httpResp, err := r.client.Raw.DeleteApiNetworkNetworkID(ctx, state.ID.ValueString())
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error deleting Docker network", err.Error())
|
||||
return
|
||||
}
|
||||
if err := client.CheckResponse(httpResp); err != nil {
|
||||
resp.Diagnostics.AddError("Error deleting Docker network", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (r *dockerNetworkResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
|
||||
}
|
||||
|
||||
// ---------- helpers ----------
|
||||
|
||||
// networkInfo represents the minimal fields returned by the network list API.
|
||||
type networkInfo struct {
|
||||
ID string `json:"Id"`
|
||||
Name string `json:"Name"`
|
||||
Driver string `json:"Driver"`
|
||||
}
|
||||
|
||||
// findNetworkIDByName queries the network list and returns the ID for the
|
||||
// given network name.
|
||||
func (r *dockerNetworkResource) findNetworkIDByName(ctx context.Context, name string) (string, error) {
|
||||
httpResp, err := r.client.Raw.GetApiNetworks(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("listing networks: %w", err)
|
||||
}
|
||||
|
||||
rawData, err := client.ParseRawResponse(httpResp)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("parsing network list: %w", err)
|
||||
}
|
||||
|
||||
var networks []networkInfo
|
||||
if err := json.Unmarshal(rawData, &networks); err != nil {
|
||||
return "", fmt.Errorf("unmarshalling network list: %w", err)
|
||||
}
|
||||
|
||||
for _, n := range networks {
|
||||
if n.Name == name {
|
||||
return n.ID, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("network %q not found after creation", name)
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
cosmossdk "github.com/azukaar/cosmos-server/go-sdk"
|
||||
"github.com/azukaar/terraform-provider-cosmos/internal/client"
|
||||
"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"
|
||||
)
|
||||
|
||||
var (
|
||||
_ resource.Resource = &dockerServiceResource{}
|
||||
_ resource.ResourceWithImportState = &dockerServiceResource{}
|
||||
)
|
||||
|
||||
func NewDockerServiceResource() resource.Resource {
|
||||
return &dockerServiceResource{}
|
||||
}
|
||||
|
||||
type dockerServiceResource struct {
|
||||
client *client.CosmosClient
|
||||
}
|
||||
|
||||
// ---------- Terraform model ----------
|
||||
|
||||
type dockerServiceModel struct {
|
||||
Name types.String `tfsdk:"name"`
|
||||
Image types.String `tfsdk:"image"`
|
||||
ServiceJSON types.String `tfsdk:"service_json"`
|
||||
}
|
||||
|
||||
// ---------- resource.Resource ----------
|
||||
|
||||
func (r *dockerServiceResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_docker_service"
|
||||
}
|
||||
|
||||
func (r *dockerServiceResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "Manages a Docker service (container) in Cosmos Server.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"name": schema.StringAttribute{
|
||||
Description: "The container name. Used as the resource ID.",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
"image": schema.StringAttribute{
|
||||
Description: "The Docker image to use for the container.",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
"service_json": schema.StringAttribute{
|
||||
Description: "Optional JSON blob representing the full container configuration (ports, volumes, environment, etc.). Changes trigger container recreation.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *dockerServiceResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
c, ok := req.ProviderData.(*client.CosmosClient)
|
||||
if !ok {
|
||||
resp.Diagnostics.AddError(
|
||||
"Unexpected Resource Configure Type",
|
||||
fmt.Sprintf("Expected *client.CosmosClient, got: %T", req.ProviderData),
|
||||
)
|
||||
return
|
||||
}
|
||||
r.client = c
|
||||
}
|
||||
|
||||
// ---------- CRUD ----------
|
||||
|
||||
func (r *dockerServiceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||
var plan dockerServiceModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
containerName := plan.Name.ValueString()
|
||||
image := plan.Image.ValueString()
|
||||
|
||||
// Build the container definition. Start from service_json if provided,
|
||||
// otherwise construct a minimal container config.
|
||||
var container cosmossdk.DockerContainerCreateRequestContainer
|
||||
if !plan.ServiceJSON.IsNull() && !plan.ServiceJSON.IsUnknown() && plan.ServiceJSON.ValueString() != "" {
|
||||
if err := json.Unmarshal([]byte(plan.ServiceJSON.ValueString()), &container); err != nil {
|
||||
resp.Diagnostics.AddError("Error parsing service_json", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Always set image and container_name from the explicit attributes.
|
||||
container.Image = image
|
||||
container.ContainerName = client.StringPtr(containerName)
|
||||
|
||||
services := map[string]cosmossdk.DockerContainerCreateRequestContainer{
|
||||
containerName: container,
|
||||
}
|
||||
|
||||
createReq := cosmossdk.DockerDockerServiceCreateRequest{
|
||||
Services: &services,
|
||||
}
|
||||
|
||||
httpResp, err := r.client.Raw.PostApiDockerService(ctx, createReq)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error creating docker service", err.Error())
|
||||
return
|
||||
}
|
||||
// This endpoint streams progress text (pull logs, status messages) before
|
||||
// the final result. We read the full body and check for success markers
|
||||
// rather than trying to parse it as a standard JSON envelope.
|
||||
if err := client.CheckStreamingResponse(httpResp); err != nil {
|
||||
resp.Diagnostics.AddError("Error creating docker service", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Read back the created container to populate computed fields.
|
||||
if !r.readContainer(ctx, containerName, &plan, &resp.Diagnostics) {
|
||||
resp.Diagnostics.AddError("Error reading docker service", "Container not found after creation")
|
||||
return
|
||||
}
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||
}
|
||||
|
||||
func (r *dockerServiceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||
var state dockerServiceModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
found := r.readContainer(ctx, state.Name.ValueString(), &state, &resp.Diagnostics)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
if !found {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
|
||||
}
|
||||
|
||||
func (r *dockerServiceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||
var plan dockerServiceModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
containerName := plan.Name.ValueString()
|
||||
image := plan.Image.ValueString()
|
||||
|
||||
// Build the update body from service_json or a minimal config.
|
||||
var updateBody map[string]interface{}
|
||||
if !plan.ServiceJSON.IsNull() && !plan.ServiceJSON.IsUnknown() && plan.ServiceJSON.ValueString() != "" {
|
||||
if err := json.Unmarshal([]byte(plan.ServiceJSON.ValueString()), &updateBody); err != nil {
|
||||
resp.Diagnostics.AddError("Error parsing service_json for update", err.Error())
|
||||
return
|
||||
}
|
||||
} else {
|
||||
updateBody = make(map[string]interface{})
|
||||
}
|
||||
|
||||
// Always ensure the image is set.
|
||||
updateBody["image"] = image
|
||||
updateBody["container_name"] = containerName
|
||||
|
||||
jsonBytes, err := json.Marshal(updateBody)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error marshalling update body", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
httpResp, err := r.client.Raw.PostApiServappsContainerIdUpdateWithBody(
|
||||
ctx, containerName, "application/json", bytes.NewReader(jsonBytes),
|
||||
)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error updating docker service", err.Error())
|
||||
return
|
||||
}
|
||||
if err := client.CheckStreamingResponse(httpResp); err != nil {
|
||||
resp.Diagnostics.AddError("Error updating docker service", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Read back the updated container.
|
||||
if !r.readContainer(ctx, containerName, &plan, &resp.Diagnostics) {
|
||||
resp.Diagnostics.AddError("Error reading docker service", "Container not found after update")
|
||||
return
|
||||
}
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||
}
|
||||
|
||||
func (r *dockerServiceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||
var state dockerServiceModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
containerName := state.Name.ValueString()
|
||||
|
||||
// Stop the container first — Docker won't remove a running container.
|
||||
stopAction := cosmossdk.GetApiServappsContainerIdManageActionParamsAction("stop")
|
||||
httpResp, err := r.client.Raw.GetApiServappsContainerIdManageAction(ctx, containerName, stopAction)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error stopping docker service", err.Error())
|
||||
return
|
||||
}
|
||||
if err := client.CheckStreamingResponse(httpResp); err != nil {
|
||||
resp.Diagnostics.AddError("Error stopping docker service", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Now remove it.
|
||||
removeAction := cosmossdk.GetApiServappsContainerIdManageActionParamsAction("remove")
|
||||
httpResp, err = r.client.Raw.GetApiServappsContainerIdManageAction(ctx, containerName, removeAction)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error deleting docker service", err.Error())
|
||||
return
|
||||
}
|
||||
if err := client.CheckStreamingResponse(httpResp); err != nil {
|
||||
resp.Diagnostics.AddError("Error deleting docker service", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (r *dockerServiceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||
resource.ImportStatePassthroughID(ctx, path.Root("name"), req, resp)
|
||||
}
|
||||
|
||||
// ---------- helpers ----------
|
||||
|
||||
// readContainer fetches a container by name and populates the model.
|
||||
// It returns false if the container was not found (404), in which case no
|
||||
// diagnostic error is added — the caller should remove the resource from state.
|
||||
func (r *dockerServiceResource) readContainer(ctx context.Context, containerName string, model *dockerServiceModel, diags interface {
|
||||
AddError(string, string)
|
||||
HasError() bool
|
||||
}) bool {
|
||||
httpResp, err := r.client.Raw.GetApiServappsContainerId(ctx, containerName)
|
||||
if err != nil {
|
||||
diags.AddError("Error reading docker service", err.Error())
|
||||
return true
|
||||
}
|
||||
|
||||
rawData, err := client.ParseRawResponse(httpResp)
|
||||
if err != nil {
|
||||
if client.IsNotFound(err) {
|
||||
return false
|
||||
}
|
||||
diags.AddError("Error reading docker service", err.Error())
|
||||
return true
|
||||
}
|
||||
if rawData == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Parse as a generic map to extract fields and store the full JSON.
|
||||
var containerData map[string]interface{}
|
||||
if err := json.Unmarshal(rawData, &containerData); err != nil {
|
||||
diags.AddError("Error parsing docker service response", err.Error())
|
||||
return true
|
||||
}
|
||||
|
||||
model.Name = types.StringValue(containerName)
|
||||
|
||||
// Extract image from the Config block if available (docker inspect format),
|
||||
// otherwise fall back to top-level "image" field.
|
||||
if cfg, ok := containerData["Config"].(map[string]interface{}); ok {
|
||||
if img, ok := cfg["Image"].(string); ok {
|
||||
model.Image = types.StringValue(img)
|
||||
}
|
||||
} else if img, ok := containerData["image"].(string); ok {
|
||||
model.Image = types.StringValue(img)
|
||||
}
|
||||
|
||||
// Don't overwrite service_json — it's a user-provided config input, not
|
||||
// server state. Preserve whatever the user originally set.
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
cosmossdk "github.com/azukaar/cosmos-server/go-sdk"
|
||||
"github.com/azukaar/terraform-provider-cosmos/internal/client"
|
||||
"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/stringdefault"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
)
|
||||
|
||||
// Ensure interface compliance.
|
||||
var (
|
||||
_ resource.Resource = &dockerVolumeResource{}
|
||||
_ resource.ResourceWithImportState = &dockerVolumeResource{}
|
||||
)
|
||||
|
||||
// NewDockerVolumeResource returns a new resource.Resource for Docker volumes.
|
||||
func NewDockerVolumeResource() resource.Resource {
|
||||
return &dockerVolumeResource{}
|
||||
}
|
||||
|
||||
type dockerVolumeResource struct {
|
||||
client *client.CosmosClient
|
||||
}
|
||||
|
||||
type dockerVolumeResourceModel struct {
|
||||
Name types.String `tfsdk:"name"`
|
||||
Driver types.String `tfsdk:"driver"`
|
||||
}
|
||||
|
||||
func (r *dockerVolumeResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_docker_volume"
|
||||
}
|
||||
|
||||
func (r *dockerVolumeResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "Manages a Docker volume through Cosmos Server.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"name": schema.StringAttribute{
|
||||
Description: "The name of the Docker volume (used as the resource ID). Changing this forces re-creation.",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
"driver": schema.StringAttribute{
|
||||
Description: "The volume driver to use. Default: local. Changing this forces re-creation.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Default: stringdefault.StaticString("local"),
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *dockerVolumeResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
c, ok := req.ProviderData.(*client.CosmosClient)
|
||||
if !ok {
|
||||
resp.Diagnostics.AddError(
|
||||
"Unexpected Resource Configure Type",
|
||||
fmt.Sprintf("Expected *client.CosmosClient, got: %T", req.ProviderData),
|
||||
)
|
||||
return
|
||||
}
|
||||
r.client = c
|
||||
}
|
||||
|
||||
func (r *dockerVolumeResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||
var plan dockerVolumeResourceModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
payload := cosmossdk.DockerVolumeCreateRequest{
|
||||
Name: plan.Name.ValueString(),
|
||||
}
|
||||
if !plan.Driver.IsNull() && !plan.Driver.IsUnknown() {
|
||||
payload.Driver = client.StringPtr(plan.Driver.ValueString())
|
||||
}
|
||||
|
||||
httpResp, err := r.client.Raw.PostApiVolumes(ctx, payload)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error creating Docker volume", err.Error())
|
||||
return
|
||||
}
|
||||
if err := client.CheckResponse(httpResp); err != nil {
|
||||
resp.Diagnostics.AddError("Error creating Docker volume", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||
}
|
||||
|
||||
func (r *dockerVolumeResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||
var state dockerVolumeResourceModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
name := state.Name.ValueString()
|
||||
|
||||
httpResp, err := r.client.Raw.GetApiVolumes(ctx)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error reading Docker volumes", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
rawData, err := client.ParseRawResponse(httpResp)
|
||||
if err != nil {
|
||||
if client.IsNotFound(err) {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
resp.Diagnostics.AddError("Error reading Docker volumes", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// The API returns {"Volumes": [...], "Warnings": ...}
|
||||
var volumeList struct {
|
||||
Volumes []volumeInfo `json:"Volumes"`
|
||||
}
|
||||
if err := json.Unmarshal(rawData, &volumeList); err != nil {
|
||||
resp.Diagnostics.AddError("Error parsing Docker volumes response", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var found *volumeInfo
|
||||
for i := range volumeList.Volumes {
|
||||
if volumeList.Volumes[i].Name == name {
|
||||
found = &volumeList.Volumes[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if found == nil {
|
||||
// Volume no longer exists; remove from state.
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
state.Name = types.StringValue(found.Name)
|
||||
|
||||
// Only update driver from API if the user originally set it.
|
||||
// Otherwise leave it as null to avoid spurious replacements
|
||||
// (e.g. API returns "local" but user didn't specify driver).
|
||||
if !state.Driver.IsNull() {
|
||||
if found.Driver != "" {
|
||||
state.Driver = types.StringValue(found.Driver)
|
||||
}
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
|
||||
}
|
||||
|
||||
func (r *dockerVolumeResource) Update(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||
// All attributes require replacement; no in-place update is supported.
|
||||
resp.Diagnostics.AddError(
|
||||
"Update not supported",
|
||||
"All attributes of cosmos_docker_volume require replacement. This method should never be called.",
|
||||
)
|
||||
}
|
||||
|
||||
func (r *dockerVolumeResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||
var state dockerVolumeResourceModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
httpResp, err := r.client.Raw.DeleteApiVolumeVolumeName(ctx, state.Name.ValueString())
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error deleting Docker volume", err.Error())
|
||||
return
|
||||
}
|
||||
if err := client.CheckResponse(httpResp); err != nil {
|
||||
resp.Diagnostics.AddError("Error deleting Docker volume", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (r *dockerVolumeResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||
resource.ImportStatePassthroughID(ctx, path.Root("name"), req, resp)
|
||||
}
|
||||
|
||||
// ---------- helpers ----------
|
||||
|
||||
// volumeInfo represents the minimal fields returned by the volume list API.
|
||||
type volumeInfo struct {
|
||||
Name string `json:"Name"`
|
||||
Driver string `json:"Driver"`
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
cosmossdk "github.com/azukaar/cosmos-server/go-sdk"
|
||||
"github.com/azukaar/terraform-provider-cosmos/internal/client"
|
||||
"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"
|
||||
)
|
||||
|
||||
// Ensure interface compliance.
|
||||
var (
|
||||
_ resource.Resource = &openIDClientResource{}
|
||||
_ resource.ResourceWithImportState = &openIDClientResource{}
|
||||
)
|
||||
|
||||
// NewOpenIDClientResource returns a new resource.Resource for OpenID clients.
|
||||
func NewOpenIDClientResource() resource.Resource {
|
||||
return &openIDClientResource{}
|
||||
}
|
||||
|
||||
type openIDClientResource struct {
|
||||
client *client.CosmosClient
|
||||
}
|
||||
|
||||
// openIDClientModel maps the resource schema to a Go struct.
|
||||
type openIDClientModel struct {
|
||||
ID types.String `tfsdk:"id"`
|
||||
Secret types.String `tfsdk:"secret"`
|
||||
Redirect types.String `tfsdk:"redirect"`
|
||||
}
|
||||
|
||||
func (r *openIDClientResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_openid_client"
|
||||
}
|
||||
|
||||
func (r *openIDClientResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "Manages a Cosmos Server OpenID Connect client.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"id": schema.StringAttribute{
|
||||
Description: "The unique identifier of the OpenID client. Changing this forces re-creation.",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
"secret": schema.StringAttribute{
|
||||
Description: "The client secret for this OpenID client. Returned on creation only — subsequent reads will not return it.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Sensitive: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
},
|
||||
},
|
||||
"redirect": schema.StringAttribute{
|
||||
Description: "The redirect URI for this OpenID client.",
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *openIDClientResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
c, ok := req.ProviderData.(*client.CosmosClient)
|
||||
if !ok {
|
||||
resp.Diagnostics.AddError(
|
||||
"Unexpected Resource Configure Type",
|
||||
fmt.Sprintf("Expected *client.CosmosClient, got: %T", req.ProviderData),
|
||||
)
|
||||
return
|
||||
}
|
||||
r.client = c
|
||||
}
|
||||
|
||||
func (r *openIDClientResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||
var plan openIDClientModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
createReq := cosmossdk.UtilsOpenIDClient{
|
||||
Id: plan.ID.ValueString(),
|
||||
}
|
||||
|
||||
if !plan.Secret.IsNull() && !plan.Secret.IsUnknown() {
|
||||
createReq.Secret = client.StringPtr(plan.Secret.ValueString())
|
||||
}
|
||||
if !plan.Redirect.IsNull() && !plan.Redirect.IsUnknown() {
|
||||
createReq.Redirect = client.StringPtr(plan.Redirect.ValueString())
|
||||
}
|
||||
|
||||
httpResp, err := r.client.Raw.PostApiOpenid(ctx, createReq)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error creating OpenID client", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Capture the secret from the create response — the GET endpoint strips it.
|
||||
result, err := client.ParseResponse[cosmossdk.UtilsOpenIDClient](httpResp)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error creating OpenID client", err.Error())
|
||||
return
|
||||
}
|
||||
if result != nil && result.Secret != nil && *result.Secret != "" {
|
||||
plan.Secret = types.StringValue(*result.Secret)
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||
}
|
||||
|
||||
func (r *openIDClientResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||
var state openIDClientModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
id := state.ID.ValueString()
|
||||
|
||||
httpResp, err := r.client.Raw.GetApiOpenidId(ctx, id)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error reading OpenID client", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
result, err := client.ParseResponse[cosmossdk.UtilsOpenIDClient](httpResp)
|
||||
if err != nil {
|
||||
if client.IsNotFound(err) {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
resp.Diagnostics.AddError("Error parsing OpenID client response", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
state.ID = types.StringValue(result.Id)
|
||||
|
||||
// Don't read secret back from API — preserve whatever is in state.
|
||||
// The secret should ideally not be returned by the server at all.
|
||||
|
||||
if result.Redirect != nil {
|
||||
state.Redirect = types.StringValue(*result.Redirect)
|
||||
} else {
|
||||
state.Redirect = types.StringNull()
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
|
||||
}
|
||||
|
||||
func (r *openIDClientResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||
var plan openIDClientModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
id := plan.ID.ValueString()
|
||||
|
||||
updateReq := cosmossdk.UtilsOpenIDClient{
|
||||
Id: id,
|
||||
}
|
||||
|
||||
if !plan.Secret.IsNull() && !plan.Secret.IsUnknown() {
|
||||
updateReq.Secret = client.StringPtr(plan.Secret.ValueString())
|
||||
}
|
||||
if !plan.Redirect.IsNull() && !plan.Redirect.IsUnknown() {
|
||||
updateReq.Redirect = client.StringPtr(plan.Redirect.ValueString())
|
||||
}
|
||||
|
||||
httpResp, err := r.client.Raw.PutApiOpenidId(ctx, id, updateReq)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error updating OpenID client", err.Error())
|
||||
return
|
||||
}
|
||||
if err := client.CheckResponse(httpResp); err != nil {
|
||||
resp.Diagnostics.AddError("Error updating OpenID client", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||
}
|
||||
|
||||
func (r *openIDClientResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||
var state openIDClientModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
id := state.ID.ValueString()
|
||||
|
||||
httpResp, err := r.client.Raw.DeleteApiOpenidId(ctx, id)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error deleting OpenID client", err.Error())
|
||||
return
|
||||
}
|
||||
if err := client.CheckResponse(httpResp); err != nil {
|
||||
resp.Diagnostics.AddError("Error deleting OpenID client", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (r *openIDClientResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
|
||||
}
|
||||
@@ -0,0 +1,763 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
cosmossdk "github.com/azukaar/cosmos-server/go-sdk"
|
||||
"github.com/azukaar/terraform-provider-cosmos/internal/client"
|
||||
"github.com/hashicorp/terraform-plugin-framework/attr"
|
||||
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||
"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"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
|
||||
)
|
||||
|
||||
var (
|
||||
_ resource.Resource = &routeResource{}
|
||||
_ resource.ResourceWithImportState = &routeResource{}
|
||||
)
|
||||
|
||||
func NewRouteResource() resource.Resource {
|
||||
return &routeResource{}
|
||||
}
|
||||
|
||||
type routeResource struct {
|
||||
client *client.CosmosClient
|
||||
}
|
||||
|
||||
// ---------- Terraform model ----------
|
||||
|
||||
type routeSmartShieldModel struct {
|
||||
Enabled types.Bool `tfsdk:"enabled"`
|
||||
MaxGlobalSimultaneous types.Int64 `tfsdk:"max_global_simultaneous"`
|
||||
PerUserByteLimit types.Int64 `tfsdk:"per_user_byte_limit"`
|
||||
PerUserRequestLimit types.Int64 `tfsdk:"per_user_request_limit"`
|
||||
PerUserSimultaneous types.Int64 `tfsdk:"per_user_simultaneous"`
|
||||
PerUserTimeBudget types.Float32 `tfsdk:"per_user_time_budget"`
|
||||
PolicyStrictness types.Int64 `tfsdk:"policy_strictness"`
|
||||
PrivilegedGroups types.Int64 `tfsdk:"privileged_groups"`
|
||||
}
|
||||
|
||||
var smartShieldAttrTypes = map[string]attr.Type{
|
||||
"enabled": types.BoolType,
|
||||
"max_global_simultaneous": types.Int64Type,
|
||||
"per_user_byte_limit": types.Int64Type,
|
||||
"per_user_request_limit": types.Int64Type,
|
||||
"per_user_simultaneous": types.Int64Type,
|
||||
"per_user_time_budget": types.Float32Type,
|
||||
"policy_strictness": types.Int64Type,
|
||||
"privileged_groups": types.Int64Type,
|
||||
}
|
||||
|
||||
type routeAdditionalFilterModel struct {
|
||||
Name types.String `tfsdk:"name"`
|
||||
Type types.String `tfsdk:"type"`
|
||||
Value types.String `tfsdk:"value"`
|
||||
}
|
||||
|
||||
var additionalFilterAttrTypes = map[string]attr.Type{
|
||||
"name": types.StringType,
|
||||
"type": types.StringType,
|
||||
"value": types.StringType,
|
||||
}
|
||||
|
||||
type routeModel struct {
|
||||
Name types.String `tfsdk:"name"`
|
||||
Target types.String `tfsdk:"target"`
|
||||
Host types.String `tfsdk:"host"`
|
||||
Mode types.String `tfsdk:"mode"`
|
||||
Description types.String `tfsdk:"description"`
|
||||
AuthEnabled types.Bool `tfsdk:"auth_enabled"`
|
||||
AdminOnly types.Bool `tfsdk:"admin_only"`
|
||||
Disabled types.Bool `tfsdk:"disabled"`
|
||||
HideFromDashboard types.Bool `tfsdk:"hide_from_dashboard"`
|
||||
RestrictToConstellation types.Bool `tfsdk:"restrict_to_constellation"`
|
||||
BlockCommonBots types.Bool `tfsdk:"block_common_bots"`
|
||||
BlockAPIAbuse types.Bool `tfsdk:"block_api_abuse"`
|
||||
DisableHeaderHardening types.Bool `tfsdk:"disable_header_hardening"`
|
||||
SpoofHostname types.Bool `tfsdk:"spoof_hostname"`
|
||||
AcceptInsecureHTTPSTarget types.Bool `tfsdk:"accept_insecure_https_target"`
|
||||
SkipURLClean types.Bool `tfsdk:"skip_url_clean"`
|
||||
StripPathPrefix types.Bool `tfsdk:"strip_path_prefix"`
|
||||
Corsorigin types.String `tfsdk:"cors_origin"`
|
||||
Icon types.String `tfsdk:"icon"`
|
||||
MaxBandwith types.Int64 `tfsdk:"max_bandwidth"`
|
||||
OverwriteHostHeader types.String `tfsdk:"overwrite_host_header"`
|
||||
PathPrefix types.String `tfsdk:"path_prefix"`
|
||||
ThrottlePerMinute types.Int64 `tfsdk:"throttle_per_minute"`
|
||||
Timeout types.Int64 `tfsdk:"timeout"`
|
||||
Tunnel types.String `tfsdk:"tunnel"`
|
||||
TunneledHost types.String `tfsdk:"tunneled_host"`
|
||||
LBMode types.String `tfsdk:"lb_mode"`
|
||||
LBStickyMode types.Bool `tfsdk:"lb_sticky_mode"`
|
||||
UseHost types.Bool `tfsdk:"use_host"`
|
||||
UseH2C types.Bool `tfsdk:"use_h2c"`
|
||||
UsePathPrefix types.Bool `tfsdk:"use_path_prefix"`
|
||||
ExtraHeaders types.Map `tfsdk:"extra_headers"`
|
||||
WhitelistInboundIPs types.List `tfsdk:"whitelist_inbound_ips"`
|
||||
AdditionalFilters types.List `tfsdk:"additional_filters"`
|
||||
SmartShield types.Object `tfsdk:"smart_shield"`
|
||||
}
|
||||
|
||||
// ---------- resource.Resource ----------
|
||||
|
||||
func (r *routeResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_route"
|
||||
}
|
||||
|
||||
func (r *routeResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "Manages a Cosmos Server proxy route.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"name": schema.StringAttribute{
|
||||
Description: "Unique name of the route. Used as the resource ID.",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
"target": schema.StringAttribute{
|
||||
Description: "Target URL the route proxies to.",
|
||||
Required: true,
|
||||
},
|
||||
"host": schema.StringAttribute{
|
||||
Description: "Hostname the route listens on.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"mode": schema.StringAttribute{
|
||||
Description: "Proxy mode (e.g. PROXY, SPA, STATIC, REDIRECT, SERVAPP).",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"description": schema.StringAttribute{
|
||||
Description: "Human-readable description of the route.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"auth_enabled": schema.BoolAttribute{
|
||||
Description: "Whether authentication is required.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"admin_only": schema.BoolAttribute{
|
||||
Description: "Restrict access to admin users.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"disabled": schema.BoolAttribute{
|
||||
Description: "Whether the route is disabled.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"hide_from_dashboard": schema.BoolAttribute{
|
||||
Description: "Hide the route from the Cosmos dashboard.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"restrict_to_constellation": schema.BoolAttribute{
|
||||
Description: "Only allow access from the Constellation VPN.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"block_common_bots": schema.BoolAttribute{
|
||||
Description: "Block common bots and crawlers.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"block_api_abuse": schema.BoolAttribute{
|
||||
Description: "Block API abuse patterns.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"disable_header_hardening": schema.BoolAttribute{
|
||||
Description: "Disable security header hardening.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"spoof_hostname": schema.BoolAttribute{
|
||||
Description: "Spoof the hostname in proxied requests.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"accept_insecure_https_target": schema.BoolAttribute{
|
||||
Description: "Accept self-signed certificates on the target.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"skip_url_clean": schema.BoolAttribute{
|
||||
Description: "Skip URL cleaning/normalization.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"strip_path_prefix": schema.BoolAttribute{
|
||||
Description: "Strip the path prefix before forwarding.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"cors_origin": schema.StringAttribute{
|
||||
Description: "Allowed CORS origin.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"icon": schema.StringAttribute{
|
||||
Description: "Icon identifier for the dashboard.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"max_bandwidth": schema.Int64Attribute{
|
||||
Description: "Maximum bandwidth in bytes per second (0 = unlimited).",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"overwrite_host_header": schema.StringAttribute{
|
||||
Description: "Override the Host header sent to the target.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"path_prefix": schema.StringAttribute{
|
||||
Description: "Path prefix to match for this route.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"throttle_per_minute": schema.Int64Attribute{
|
||||
Description: "Maximum requests per minute (0 = unlimited).",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"timeout": schema.Int64Attribute{
|
||||
Description: "Request timeout in nanoseconds.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"tunnel": schema.StringAttribute{
|
||||
Description: "Tunnel identifier for Constellation tunneling.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"tunneled_host": schema.StringAttribute{
|
||||
Description: "Hostname to use when tunneling through Constellation.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"lb_mode": schema.StringAttribute{
|
||||
Description: "Load balancer mode.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"lb_sticky_mode": schema.BoolAttribute{
|
||||
Description: "Enable sticky sessions for load balancing.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"use_host": schema.BoolAttribute{
|
||||
Description: "Whether the route matches by hostname.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"use_h2c": schema.BoolAttribute{
|
||||
Description: "Use HTTP/2 cleartext (h2c) for the upstream connection.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"use_path_prefix": schema.BoolAttribute{
|
||||
Description: "Whether the route matches by path prefix.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"extra_headers": schema.MapAttribute{
|
||||
Description: "Additional HTTP headers to add to proxied requests.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
ElementType: types.StringType,
|
||||
},
|
||||
"whitelist_inbound_ips": schema.ListAttribute{
|
||||
Description: "List of IP addresses or CIDRs allowed to access this route.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
ElementType: types.StringType,
|
||||
},
|
||||
"additional_filters": schema.ListNestedAttribute{
|
||||
Description: "Additional request filters.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
NestedObject: schema.NestedAttributeObject{
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"name": schema.StringAttribute{
|
||||
Description: "Filter name.",
|
||||
Optional: true,
|
||||
},
|
||||
"type": schema.StringAttribute{
|
||||
Description: "Filter type.",
|
||||
Optional: true,
|
||||
},
|
||||
"value": schema.StringAttribute{
|
||||
Description: "Filter value.",
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"smart_shield": schema.SingleNestedAttribute{
|
||||
Description: "SmartShield DDoS protection policy.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"enabled": schema.BoolAttribute{
|
||||
Description: "Enable SmartShield protection.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"max_global_simultaneous": schema.Int64Attribute{
|
||||
Description: "Maximum global simultaneous connections.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"per_user_byte_limit": schema.Int64Attribute{
|
||||
Description: "Per-user byte limit.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"per_user_request_limit": schema.Int64Attribute{
|
||||
Description: "Per-user request limit.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"per_user_simultaneous": schema.Int64Attribute{
|
||||
Description: "Per-user simultaneous connections limit.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"per_user_time_budget": schema.Float32Attribute{
|
||||
Description: "Per-user time budget in seconds.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"policy_strictness": schema.Int64Attribute{
|
||||
Description: "Policy strictness level.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"privileged_groups": schema.Int64Attribute{
|
||||
Description: "Privileged groups bitmask.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *routeResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
c, ok := req.ProviderData.(*client.CosmosClient)
|
||||
if !ok {
|
||||
resp.Diagnostics.AddError(
|
||||
"Unexpected Resource Configure Type",
|
||||
fmt.Sprintf("Expected *client.CosmosClient, got %T", req.ProviderData),
|
||||
)
|
||||
return
|
||||
}
|
||||
r.client = c
|
||||
}
|
||||
|
||||
// ---------- CRUD ----------
|
||||
|
||||
func (r *routeResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||
var plan routeModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
apiReq := routeModelToSDK(ctx, plan, &resp.Diagnostics)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
httpResp, err := r.client.Raw.PostApiRoutes(ctx, apiReq)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error creating route", err.Error())
|
||||
return
|
||||
}
|
||||
if err := client.CheckResponse(httpResp); err != nil {
|
||||
resp.Diagnostics.AddError("Error creating route", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if !readRouteIntoModel(ctx, r.client, plan.Name.ValueString(), &plan, &resp.Diagnostics) {
|
||||
resp.Diagnostics.AddError("Error reading route", "Route not found after creation")
|
||||
return
|
||||
}
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||
}
|
||||
|
||||
func (r *routeResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||
var state routeModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
found := readRouteIntoModel(ctx, r.client, state.Name.ValueString(), &state, &resp.Diagnostics)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
if !found {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
|
||||
}
|
||||
|
||||
func (r *routeResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||
var plan routeModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
var state routeModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
apiReq := routeModelToSDK(ctx, plan, &resp.Diagnostics)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
httpResp, err := r.client.Raw.PutApiRoutesName(ctx, state.Name.ValueString(), apiReq)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error updating route", err.Error())
|
||||
return
|
||||
}
|
||||
if err := client.CheckResponse(httpResp); err != nil {
|
||||
resp.Diagnostics.AddError("Error updating route", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if !readRouteIntoModel(ctx, r.client, plan.Name.ValueString(), &plan, &resp.Diagnostics) {
|
||||
resp.Diagnostics.AddError("Error reading route", "Route not found after update")
|
||||
return
|
||||
}
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||
}
|
||||
|
||||
func (r *routeResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||
var state routeModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
httpResp, err := r.client.Raw.DeleteApiRoutesName(ctx, state.Name.ValueString())
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error deleting route", err.Error())
|
||||
return
|
||||
}
|
||||
if err := client.CheckResponse(httpResp); err != nil {
|
||||
resp.Diagnostics.AddError("Error deleting route", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (r *routeResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||
resource.ImportStatePassthroughID(ctx, path.Root("name"), req, resp)
|
||||
}
|
||||
|
||||
// ---------- helpers ----------
|
||||
|
||||
// readRouteIntoModel fetches a route by name from the API and populates the model.
|
||||
// It returns false if the route was not found (404), in which case no diagnostic
|
||||
// error is added — the caller should remove the resource from state.
|
||||
func readRouteIntoModel(ctx context.Context, c *client.CosmosClient, name string, model *routeModel, diags *diag.Diagnostics) bool {
|
||||
httpResp, err := c.Raw.GetApiRoutesName(ctx, name)
|
||||
if err != nil {
|
||||
diags.AddError("Error reading route", err.Error())
|
||||
return true
|
||||
}
|
||||
route, err := client.ParseResponse[cosmossdk.UtilsProxyRouteConfig](httpResp)
|
||||
if err != nil {
|
||||
if client.IsNotFound(err) {
|
||||
return false
|
||||
}
|
||||
diags.AddError("Error reading route", err.Error())
|
||||
return true
|
||||
}
|
||||
if route == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
sdkToRouteModel(ctx, route, model, diags)
|
||||
return true
|
||||
}
|
||||
|
||||
func routeModelToSDK(ctx context.Context, m routeModel, diags *diag.Diagnostics) cosmossdk.UtilsProxyRouteConfig {
|
||||
cfg := cosmossdk.UtilsProxyRouteConfig{
|
||||
Name: m.Name.ValueString(),
|
||||
Target: m.Target.ValueString(),
|
||||
}
|
||||
|
||||
if !m.Host.IsNull() && !m.Host.IsUnknown() {
|
||||
cfg.Host = client.StringPtr(m.Host.ValueString())
|
||||
}
|
||||
if !m.Mode.IsNull() && !m.Mode.IsUnknown() {
|
||||
cfg.Mode = client.StringPtr(m.Mode.ValueString())
|
||||
}
|
||||
if !m.Description.IsNull() && !m.Description.IsUnknown() {
|
||||
cfg.Description = client.StringPtr(m.Description.ValueString())
|
||||
}
|
||||
if !m.AuthEnabled.IsNull() && !m.AuthEnabled.IsUnknown() {
|
||||
cfg.AuthEnabled = client.BoolPtr(m.AuthEnabled.ValueBool())
|
||||
}
|
||||
if !m.AdminOnly.IsNull() && !m.AdminOnly.IsUnknown() {
|
||||
cfg.AdminOnly = client.BoolPtr(m.AdminOnly.ValueBool())
|
||||
}
|
||||
if !m.Disabled.IsNull() && !m.Disabled.IsUnknown() {
|
||||
cfg.Disabled = client.BoolPtr(m.Disabled.ValueBool())
|
||||
}
|
||||
if !m.HideFromDashboard.IsNull() && !m.HideFromDashboard.IsUnknown() {
|
||||
cfg.HideFromDashboard = client.BoolPtr(m.HideFromDashboard.ValueBool())
|
||||
}
|
||||
if !m.RestrictToConstellation.IsNull() && !m.RestrictToConstellation.IsUnknown() {
|
||||
cfg.RestrictToConstellation = client.BoolPtr(m.RestrictToConstellation.ValueBool())
|
||||
}
|
||||
if !m.BlockCommonBots.IsNull() && !m.BlockCommonBots.IsUnknown() {
|
||||
cfg.BlockCommonBots = client.BoolPtr(m.BlockCommonBots.ValueBool())
|
||||
}
|
||||
if !m.BlockAPIAbuse.IsNull() && !m.BlockAPIAbuse.IsUnknown() {
|
||||
cfg.BlockAPIAbuse = client.BoolPtr(m.BlockAPIAbuse.ValueBool())
|
||||
}
|
||||
if !m.DisableHeaderHardening.IsNull() && !m.DisableHeaderHardening.IsUnknown() {
|
||||
cfg.DisableHeaderHardening = client.BoolPtr(m.DisableHeaderHardening.ValueBool())
|
||||
}
|
||||
if !m.SpoofHostname.IsNull() && !m.SpoofHostname.IsUnknown() {
|
||||
cfg.SpoofHostname = client.BoolPtr(m.SpoofHostname.ValueBool())
|
||||
}
|
||||
if !m.AcceptInsecureHTTPSTarget.IsNull() && !m.AcceptInsecureHTTPSTarget.IsUnknown() {
|
||||
cfg.AcceptInsecureHTTPSTarget = client.BoolPtr(m.AcceptInsecureHTTPSTarget.ValueBool())
|
||||
}
|
||||
if !m.SkipURLClean.IsNull() && !m.SkipURLClean.IsUnknown() {
|
||||
cfg.SkipURLClean = client.BoolPtr(m.SkipURLClean.ValueBool())
|
||||
}
|
||||
if !m.StripPathPrefix.IsNull() && !m.StripPathPrefix.IsUnknown() {
|
||||
cfg.StripPathPrefix = client.BoolPtr(m.StripPathPrefix.ValueBool())
|
||||
}
|
||||
if !m.Corsorigin.IsNull() && !m.Corsorigin.IsUnknown() {
|
||||
cfg.Corsorigin = client.StringPtr(m.Corsorigin.ValueString())
|
||||
}
|
||||
if !m.Icon.IsNull() && !m.Icon.IsUnknown() {
|
||||
cfg.Icon = client.StringPtr(m.Icon.ValueString())
|
||||
}
|
||||
if !m.MaxBandwith.IsNull() && !m.MaxBandwith.IsUnknown() {
|
||||
cfg.MaxBandwith = client.IntPtr(int(m.MaxBandwith.ValueInt64()))
|
||||
}
|
||||
if !m.OverwriteHostHeader.IsNull() && !m.OverwriteHostHeader.IsUnknown() {
|
||||
cfg.OverwriteHostHeader = client.StringPtr(m.OverwriteHostHeader.ValueString())
|
||||
}
|
||||
if !m.PathPrefix.IsNull() && !m.PathPrefix.IsUnknown() {
|
||||
cfg.PathPrefix = client.StringPtr(m.PathPrefix.ValueString())
|
||||
}
|
||||
if !m.ThrottlePerMinute.IsNull() && !m.ThrottlePerMinute.IsUnknown() {
|
||||
cfg.ThrottlePerMinute = client.IntPtr(int(m.ThrottlePerMinute.ValueInt64()))
|
||||
}
|
||||
if !m.Timeout.IsNull() && !m.Timeout.IsUnknown() {
|
||||
t := cosmossdk.TimeDuration(m.Timeout.ValueInt64())
|
||||
cfg.Timeout = &t
|
||||
}
|
||||
if !m.Tunnel.IsNull() && !m.Tunnel.IsUnknown() {
|
||||
cfg.Tunnel = client.StringPtr(m.Tunnel.ValueString())
|
||||
}
|
||||
if !m.TunneledHost.IsNull() && !m.TunneledHost.IsUnknown() {
|
||||
cfg.TunneledHost = client.StringPtr(m.TunneledHost.ValueString())
|
||||
}
|
||||
if !m.LBMode.IsNull() && !m.LBMode.IsUnknown() {
|
||||
cfg.LBMode = client.StringPtr(m.LBMode.ValueString())
|
||||
}
|
||||
if !m.LBStickyMode.IsNull() && !m.LBStickyMode.IsUnknown() {
|
||||
cfg.LBStickyMode = client.BoolPtr(m.LBStickyMode.ValueBool())
|
||||
}
|
||||
if !m.UseHost.IsNull() && !m.UseHost.IsUnknown() {
|
||||
cfg.UseHost = client.BoolPtr(m.UseHost.ValueBool())
|
||||
}
|
||||
if !m.UseH2C.IsNull() && !m.UseH2C.IsUnknown() {
|
||||
cfg.UseH2C = client.BoolPtr(m.UseH2C.ValueBool())
|
||||
}
|
||||
if !m.UsePathPrefix.IsNull() && !m.UsePathPrefix.IsUnknown() {
|
||||
cfg.UsePathPrefix = client.BoolPtr(m.UsePathPrefix.ValueBool())
|
||||
}
|
||||
if !m.ExtraHeaders.IsNull() && !m.ExtraHeaders.IsUnknown() {
|
||||
headers := make(map[string]string)
|
||||
diags.Append(m.ExtraHeaders.ElementsAs(ctx, &headers, false)...)
|
||||
cfg.ExtraHeaders = &headers
|
||||
}
|
||||
if !m.WhitelistInboundIPs.IsNull() && !m.WhitelistInboundIPs.IsUnknown() {
|
||||
var ips []string
|
||||
diags.Append(m.WhitelistInboundIPs.ElementsAs(ctx, &ips, false)...)
|
||||
cfg.WhitelistInboundIPs = &ips
|
||||
}
|
||||
if !m.AdditionalFilters.IsNull() && !m.AdditionalFilters.IsUnknown() {
|
||||
var filterModels []routeAdditionalFilterModel
|
||||
diags.Append(m.AdditionalFilters.ElementsAs(ctx, &filterModels, false)...)
|
||||
filters := make([]cosmossdk.UtilsAddionalFiltersConfig, len(filterModels))
|
||||
for i, fm := range filterModels {
|
||||
if !fm.Name.IsNull() {
|
||||
filters[i].Name = client.StringPtr(fm.Name.ValueString())
|
||||
}
|
||||
if !fm.Type.IsNull() {
|
||||
filters[i].Type = client.StringPtr(fm.Type.ValueString())
|
||||
}
|
||||
if !fm.Value.IsNull() {
|
||||
filters[i].Value = client.StringPtr(fm.Value.ValueString())
|
||||
}
|
||||
}
|
||||
cfg.AddionalFilters = &filters
|
||||
}
|
||||
|
||||
if !m.SmartShield.IsNull() && !m.SmartShield.IsUnknown() {
|
||||
var ssModel routeSmartShieldModel
|
||||
d := m.SmartShield.As(ctx, &ssModel, basetypes.ObjectAsOptions{})
|
||||
diags.Append(d...)
|
||||
if !d.HasError() {
|
||||
ss := &cosmossdk.UtilsSmartShieldPolicy{}
|
||||
if !ssModel.Enabled.IsNull() && !ssModel.Enabled.IsUnknown() {
|
||||
ss.Enabled = client.BoolPtr(ssModel.Enabled.ValueBool())
|
||||
}
|
||||
if !ssModel.MaxGlobalSimultaneous.IsNull() && !ssModel.MaxGlobalSimultaneous.IsUnknown() {
|
||||
ss.MaxGlobalSimultaneous = client.IntPtr(int(ssModel.MaxGlobalSimultaneous.ValueInt64()))
|
||||
}
|
||||
if !ssModel.PerUserByteLimit.IsNull() && !ssModel.PerUserByteLimit.IsUnknown() {
|
||||
ss.PerUserByteLimit = client.IntPtr(int(ssModel.PerUserByteLimit.ValueInt64()))
|
||||
}
|
||||
if !ssModel.PerUserRequestLimit.IsNull() && !ssModel.PerUserRequestLimit.IsUnknown() {
|
||||
ss.PerUserRequestLimit = client.IntPtr(int(ssModel.PerUserRequestLimit.ValueInt64()))
|
||||
}
|
||||
if !ssModel.PerUserSimultaneous.IsNull() && !ssModel.PerUserSimultaneous.IsUnknown() {
|
||||
ss.PerUserSimultaneous = client.IntPtr(int(ssModel.PerUserSimultaneous.ValueInt64()))
|
||||
}
|
||||
if !ssModel.PerUserTimeBudget.IsNull() && !ssModel.PerUserTimeBudget.IsUnknown() {
|
||||
ss.PerUserTimeBudget = client.Float32Ptr(ssModel.PerUserTimeBudget.ValueFloat32())
|
||||
}
|
||||
if !ssModel.PolicyStrictness.IsNull() && !ssModel.PolicyStrictness.IsUnknown() {
|
||||
ss.PolicyStrictness = client.IntPtr(int(ssModel.PolicyStrictness.ValueInt64()))
|
||||
}
|
||||
if !ssModel.PrivilegedGroups.IsNull() && !ssModel.PrivilegedGroups.IsUnknown() {
|
||||
ss.PrivilegedGroups = client.IntPtr(int(ssModel.PrivilegedGroups.ValueInt64()))
|
||||
}
|
||||
cfg.SmartShield = ss
|
||||
}
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func sdkToRouteModel(ctx context.Context, route *cosmossdk.UtilsProxyRouteConfig, m *routeModel, diags *diag.Diagnostics) {
|
||||
m.Name = types.StringValue(route.Name)
|
||||
m.Target = types.StringValue(route.Target)
|
||||
m.Host = types.StringValue(client.StringPtrVal(route.Host))
|
||||
m.Mode = types.StringValue(client.StringPtrVal(route.Mode))
|
||||
m.Description = types.StringValue(client.StringPtrVal(route.Description))
|
||||
m.AuthEnabled = types.BoolValue(client.BoolPtrVal(route.AuthEnabled))
|
||||
m.AdminOnly = types.BoolValue(client.BoolPtrVal(route.AdminOnly))
|
||||
m.Disabled = types.BoolValue(client.BoolPtrVal(route.Disabled))
|
||||
m.HideFromDashboard = types.BoolValue(client.BoolPtrVal(route.HideFromDashboard))
|
||||
m.RestrictToConstellation = types.BoolValue(client.BoolPtrVal(route.RestrictToConstellation))
|
||||
m.BlockCommonBots = types.BoolValue(client.BoolPtrVal(route.BlockCommonBots))
|
||||
m.BlockAPIAbuse = types.BoolValue(client.BoolPtrVal(route.BlockAPIAbuse))
|
||||
m.DisableHeaderHardening = types.BoolValue(client.BoolPtrVal(route.DisableHeaderHardening))
|
||||
m.SpoofHostname = types.BoolValue(client.BoolPtrVal(route.SpoofHostname))
|
||||
m.AcceptInsecureHTTPSTarget = types.BoolValue(client.BoolPtrVal(route.AcceptInsecureHTTPSTarget))
|
||||
m.SkipURLClean = types.BoolValue(client.BoolPtrVal(route.SkipURLClean))
|
||||
m.StripPathPrefix = types.BoolValue(client.BoolPtrVal(route.StripPathPrefix))
|
||||
m.Corsorigin = types.StringValue(client.StringPtrVal(route.Corsorigin))
|
||||
m.Icon = types.StringValue(client.StringPtrVal(route.Icon))
|
||||
m.MaxBandwith = types.Int64Value(int64(client.IntPtrVal(route.MaxBandwith)))
|
||||
m.OverwriteHostHeader = types.StringValue(client.StringPtrVal(route.OverwriteHostHeader))
|
||||
m.PathPrefix = types.StringValue(client.StringPtrVal(route.PathPrefix))
|
||||
m.ThrottlePerMinute = types.Int64Value(int64(client.IntPtrVal(route.ThrottlePerMinute)))
|
||||
if route.Timeout != nil {
|
||||
m.Timeout = types.Int64Value(int64(*route.Timeout))
|
||||
} else {
|
||||
m.Timeout = types.Int64Value(0)
|
||||
}
|
||||
m.Tunnel = types.StringValue(client.StringPtrVal(route.Tunnel))
|
||||
m.TunneledHost = types.StringValue(client.StringPtrVal(route.TunneledHost))
|
||||
m.LBMode = types.StringValue(client.StringPtrVal(route.LBMode))
|
||||
m.LBStickyMode = types.BoolValue(client.BoolPtrVal(route.LBStickyMode))
|
||||
m.UseHost = types.BoolValue(client.BoolPtrVal(route.UseHost))
|
||||
m.UseH2C = types.BoolValue(client.BoolPtrVal(route.UseH2C))
|
||||
m.UsePathPrefix = types.BoolValue(client.BoolPtrVal(route.UsePathPrefix))
|
||||
|
||||
// ExtraHeaders
|
||||
if route.ExtraHeaders != nil && len(*route.ExtraHeaders) > 0 {
|
||||
mapVal, d := types.MapValueFrom(ctx, types.StringType, *route.ExtraHeaders)
|
||||
diags.Append(d...)
|
||||
m.ExtraHeaders = mapVal
|
||||
} else {
|
||||
m.ExtraHeaders = types.MapNull(types.StringType)
|
||||
}
|
||||
|
||||
// WhitelistInboundIPs
|
||||
if route.WhitelistInboundIPs != nil && len(*route.WhitelistInboundIPs) > 0 {
|
||||
listVal, d := types.ListValueFrom(ctx, types.StringType, *route.WhitelistInboundIPs)
|
||||
diags.Append(d...)
|
||||
m.WhitelistInboundIPs = listVal
|
||||
} else {
|
||||
m.WhitelistInboundIPs = types.ListNull(types.StringType)
|
||||
}
|
||||
|
||||
// AdditionalFilters
|
||||
if route.AddionalFilters != nil && len(*route.AddionalFilters) > 0 {
|
||||
filterModels := make([]routeAdditionalFilterModel, len(*route.AddionalFilters))
|
||||
for i, f := range *route.AddionalFilters {
|
||||
filterModels[i] = routeAdditionalFilterModel{
|
||||
Name: types.StringValue(client.StringPtrVal(f.Name)),
|
||||
Type: types.StringValue(client.StringPtrVal(f.Type)),
|
||||
Value: types.StringValue(client.StringPtrVal(f.Value)),
|
||||
}
|
||||
}
|
||||
listVal, d := types.ListValueFrom(ctx, types.ObjectType{AttrTypes: additionalFilterAttrTypes}, filterModels)
|
||||
diags.Append(d...)
|
||||
m.AdditionalFilters = listVal
|
||||
} else {
|
||||
m.AdditionalFilters = types.ListNull(types.ObjectType{AttrTypes: additionalFilterAttrTypes})
|
||||
}
|
||||
|
||||
// SmartShield
|
||||
if route.SmartShield != nil {
|
||||
ssModel := routeSmartShieldModel{
|
||||
Enabled: types.BoolValue(client.BoolPtrVal(route.SmartShield.Enabled)),
|
||||
MaxGlobalSimultaneous: types.Int64Value(int64(client.IntPtrVal(route.SmartShield.MaxGlobalSimultaneous))),
|
||||
PerUserByteLimit: types.Int64Value(int64(client.IntPtrVal(route.SmartShield.PerUserByteLimit))),
|
||||
PerUserRequestLimit: types.Int64Value(int64(client.IntPtrVal(route.SmartShield.PerUserRequestLimit))),
|
||||
PerUserSimultaneous: types.Int64Value(int64(client.IntPtrVal(route.SmartShield.PerUserSimultaneous))),
|
||||
PerUserTimeBudget: types.Float32Value(client.Float32PtrVal(route.SmartShield.PerUserTimeBudget)),
|
||||
PolicyStrictness: types.Int64Value(int64(client.IntPtrVal(route.SmartShield.PolicyStrictness))),
|
||||
PrivilegedGroups: types.Int64Value(int64(client.IntPtrVal(route.SmartShield.PrivilegedGroups))),
|
||||
}
|
||||
objVal, d := types.ObjectValueFrom(ctx, smartShieldAttrTypes, ssModel)
|
||||
diags.Append(d...)
|
||||
m.SmartShield = objVal
|
||||
} else {
|
||||
m.SmartShield = types.ObjectNull(smartShieldAttrTypes)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
cosmossdk "github.com/azukaar/cosmos-server/go-sdk"
|
||||
"github.com/azukaar/terraform-provider-cosmos/internal/client"
|
||||
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||
"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"
|
||||
)
|
||||
|
||||
var (
|
||||
_ resource.Resource = &snapRAIDResource{}
|
||||
_ resource.ResourceWithImportState = &snapRAIDResource{}
|
||||
)
|
||||
|
||||
func NewSnapRAIDResource() resource.Resource {
|
||||
return &snapRAIDResource{}
|
||||
}
|
||||
|
||||
type snapRAIDResource struct {
|
||||
client *client.CosmosClient
|
||||
}
|
||||
|
||||
// ---------- Terraform model ----------
|
||||
|
||||
type snapRAIDModel struct {
|
||||
Name types.String `tfsdk:"name"`
|
||||
Enabled types.Bool `tfsdk:"enabled"`
|
||||
Data types.Map `tfsdk:"data"`
|
||||
Parity types.List `tfsdk:"parity"`
|
||||
SyncCrontab types.String `tfsdk:"sync_crontab"`
|
||||
ScrubCrontab types.String `tfsdk:"scrub_crontab"`
|
||||
CheckOnFix types.Bool `tfsdk:"check_on_fix"`
|
||||
}
|
||||
|
||||
// ---------- resource.Resource ----------
|
||||
|
||||
func (r *snapRAIDResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_snapraid"
|
||||
}
|
||||
|
||||
func (r *snapRAIDResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "Manages a SnapRAID configuration in Cosmos Server.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"name": schema.StringAttribute{
|
||||
Description: "The unique name of the SnapRAID configuration. Used as the resource ID.",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
"enabled": schema.BoolAttribute{
|
||||
Description: "Whether the SnapRAID configuration is enabled.",
|
||||
Optional: true,
|
||||
},
|
||||
"data": schema.MapAttribute{
|
||||
Description: "Map of data disk names to their paths.",
|
||||
Optional: true,
|
||||
ElementType: types.StringType,
|
||||
},
|
||||
"parity": schema.ListAttribute{
|
||||
Description: "List of parity file paths.",
|
||||
Optional: true,
|
||||
ElementType: types.StringType,
|
||||
},
|
||||
"sync_crontab": schema.StringAttribute{
|
||||
Description: "Cron schedule for SnapRAID sync operations.",
|
||||
Optional: true,
|
||||
},
|
||||
"scrub_crontab": schema.StringAttribute{
|
||||
Description: "Cron schedule for SnapRAID scrub operations.",
|
||||
Optional: true,
|
||||
},
|
||||
"check_on_fix": schema.BoolAttribute{
|
||||
Description: "Whether to run a check before fixing.",
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *snapRAIDResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
c, ok := req.ProviderData.(*client.CosmosClient)
|
||||
if !ok {
|
||||
resp.Diagnostics.AddError(
|
||||
"Unexpected Resource Configure Type",
|
||||
fmt.Sprintf("Expected *client.CosmosClient, got: %T", req.ProviderData),
|
||||
)
|
||||
return
|
||||
}
|
||||
r.client = c
|
||||
}
|
||||
|
||||
// ---------- CRUD ----------
|
||||
|
||||
func (r *snapRAIDResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||
var plan snapRAIDModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
payload := r.modelToSDK(ctx, &plan, &resp.Diagnostics)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
httpResp, err := r.client.Raw.PostApiSnapraid(ctx, payload)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error creating SnapRAID config", err.Error())
|
||||
return
|
||||
}
|
||||
if err := client.CheckResponse(httpResp); err != nil {
|
||||
resp.Diagnostics.AddError("Error creating SnapRAID config", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Read back to populate any server-set defaults.
|
||||
if !r.readSnapRAID(ctx, plan.Name.ValueString(), &plan, &resp.Diagnostics) {
|
||||
resp.Diagnostics.AddError("Error reading SnapRAID config", "SnapRAID config not found after creation")
|
||||
return
|
||||
}
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||
}
|
||||
|
||||
func (r *snapRAIDResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||
var state snapRAIDModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
found := r.readSnapRAID(ctx, state.Name.ValueString(), &state, &resp.Diagnostics)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
if !found {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
|
||||
}
|
||||
|
||||
func (r *snapRAIDResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||
var plan snapRAIDModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
name := plan.Name.ValueString()
|
||||
payload := r.modelToSDK(ctx, &plan, &resp.Diagnostics)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
httpResp, err := r.client.Raw.PostApiSnapraidName(ctx, name, payload)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error updating SnapRAID config", err.Error())
|
||||
return
|
||||
}
|
||||
if err := client.CheckResponse(httpResp); err != nil {
|
||||
resp.Diagnostics.AddError("Error updating SnapRAID config", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Read back to capture any server-side changes.
|
||||
if !r.readSnapRAID(ctx, name, &plan, &resp.Diagnostics) {
|
||||
resp.Diagnostics.AddError("Error reading SnapRAID config", "SnapRAID config not found after update")
|
||||
return
|
||||
}
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||
}
|
||||
|
||||
func (r *snapRAIDResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||
var state snapRAIDModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
name := state.Name.ValueString()
|
||||
body := cosmossdk.UtilsSnapRAIDConfig{
|
||||
Name: client.StringPtr(name),
|
||||
}
|
||||
|
||||
httpResp, err := r.client.Raw.DeleteApiSnapraidName(ctx, name, body)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error deleting SnapRAID config", err.Error())
|
||||
return
|
||||
}
|
||||
if err := client.CheckResponse(httpResp); err != nil {
|
||||
resp.Diagnostics.AddError("Error deleting SnapRAID config", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (r *snapRAIDResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||
resource.ImportStatePassthroughID(ctx, path.Root("name"), req, resp)
|
||||
}
|
||||
|
||||
// ---------- helpers ----------
|
||||
|
||||
// readSnapRAID fetches the SnapRAID config by name and populates the model.
|
||||
// It returns false if the config was not found (404), in which case no diagnostic
|
||||
// error is added — the caller should remove the resource from state.
|
||||
func (r *snapRAIDResource) readSnapRAID(ctx context.Context, name string, model *snapRAIDModel, diags interface {
|
||||
AddError(string, string)
|
||||
HasError() bool
|
||||
}) bool {
|
||||
body := cosmossdk.UtilsSnapRAIDConfig{
|
||||
Name: client.StringPtr(name),
|
||||
}
|
||||
|
||||
httpResp, err := r.client.Raw.GetApiSnapraid(ctx, body)
|
||||
if err != nil {
|
||||
diags.AddError("Error reading SnapRAID config", err.Error())
|
||||
return true
|
||||
}
|
||||
|
||||
rawData, err := client.ParseRawResponse(httpResp)
|
||||
if err != nil {
|
||||
if client.IsNotFound(err) {
|
||||
return false
|
||||
}
|
||||
diags.AddError("Error reading SnapRAID config", err.Error())
|
||||
return true
|
||||
}
|
||||
if rawData == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
var config cosmossdk.UtilsSnapRAIDConfig
|
||||
if err := json.Unmarshal(rawData, &config); err != nil {
|
||||
diags.AddError("Error parsing SnapRAID config response", err.Error())
|
||||
return true
|
||||
}
|
||||
|
||||
r.sdkToModel(&config, model)
|
||||
return true
|
||||
}
|
||||
|
||||
// modelToSDK converts the Terraform model to an SDK UtilsSnapRAIDConfig.
|
||||
func (r *snapRAIDResource) modelToSDK(ctx context.Context, model *snapRAIDModel, diags *diag.Diagnostics) cosmossdk.UtilsSnapRAIDConfig {
|
||||
config := cosmossdk.UtilsSnapRAIDConfig{
|
||||
Name: client.StringPtr(model.Name.ValueString()),
|
||||
}
|
||||
|
||||
if !model.Enabled.IsNull() && !model.Enabled.IsUnknown() {
|
||||
config.Enabled = client.BoolPtr(model.Enabled.ValueBool())
|
||||
}
|
||||
if !model.CheckOnFix.IsNull() && !model.CheckOnFix.IsUnknown() {
|
||||
config.CheckOnFix = client.BoolPtr(model.CheckOnFix.ValueBool())
|
||||
}
|
||||
if !model.SyncCrontab.IsNull() && !model.SyncCrontab.IsUnknown() {
|
||||
config.SyncCrontab = client.StringPtr(model.SyncCrontab.ValueString())
|
||||
}
|
||||
if !model.ScrubCrontab.IsNull() && !model.ScrubCrontab.IsUnknown() {
|
||||
config.ScrubCrontab = client.StringPtr(model.ScrubCrontab.ValueString())
|
||||
}
|
||||
|
||||
if !model.Data.IsNull() && !model.Data.IsUnknown() {
|
||||
dataMap := make(map[string]string)
|
||||
for k, v := range model.Data.Elements() {
|
||||
if sv, ok := v.(types.String); ok {
|
||||
dataMap[k] = sv.ValueString()
|
||||
}
|
||||
}
|
||||
config.Data = &dataMap
|
||||
}
|
||||
|
||||
if !model.Parity.IsNull() && !model.Parity.IsUnknown() {
|
||||
var parityList []string
|
||||
for _, v := range model.Parity.Elements() {
|
||||
if sv, ok := v.(types.String); ok {
|
||||
parityList = append(parityList, sv.ValueString())
|
||||
}
|
||||
}
|
||||
config.Parity = &parityList
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
// sdkToModel converts an SDK UtilsSnapRAIDConfig to the Terraform model.
|
||||
func (r *snapRAIDResource) sdkToModel(config *cosmossdk.UtilsSnapRAIDConfig, model *snapRAIDModel) {
|
||||
model.Name = types.StringValue(client.StringPtrVal(config.Name))
|
||||
|
||||
if config.Enabled != nil {
|
||||
model.Enabled = types.BoolValue(*config.Enabled)
|
||||
} else {
|
||||
model.Enabled = types.BoolNull()
|
||||
}
|
||||
|
||||
if config.CheckOnFix != nil {
|
||||
model.CheckOnFix = types.BoolValue(*config.CheckOnFix)
|
||||
} else {
|
||||
model.CheckOnFix = types.BoolNull()
|
||||
}
|
||||
|
||||
if config.SyncCrontab != nil {
|
||||
model.SyncCrontab = types.StringValue(*config.SyncCrontab)
|
||||
} else {
|
||||
model.SyncCrontab = types.StringNull()
|
||||
}
|
||||
|
||||
if config.ScrubCrontab != nil {
|
||||
model.ScrubCrontab = types.StringValue(*config.ScrubCrontab)
|
||||
} else {
|
||||
model.ScrubCrontab = types.StringNull()
|
||||
}
|
||||
|
||||
if config.Data != nil {
|
||||
mapVal, diags := types.MapValueFrom(context.Background(), types.StringType, *config.Data)
|
||||
if !diags.HasError() {
|
||||
model.Data = mapVal
|
||||
} else {
|
||||
model.Data = types.MapNull(types.StringType)
|
||||
}
|
||||
} else {
|
||||
model.Data = types.MapNull(types.StringType)
|
||||
}
|
||||
|
||||
if config.Parity != nil {
|
||||
listVal, diags := types.ListValueFrom(context.Background(), types.StringType, *config.Parity)
|
||||
if !diags.HasError() {
|
||||
model.Parity = listVal
|
||||
} else {
|
||||
model.Parity = types.ListNull(types.StringType)
|
||||
}
|
||||
} else {
|
||||
model.Parity = types.ListNull(types.StringType)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
cosmossdk "github.com/azukaar/cosmos-server/go-sdk"
|
||||
"github.com/azukaar/terraform-provider-cosmos/internal/client"
|
||||
"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/boolplanmodifier"
|
||||
"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"
|
||||
)
|
||||
|
||||
var (
|
||||
_ resource.Resource = &storageMountResource{}
|
||||
_ resource.ResourceWithImportState = &storageMountResource{}
|
||||
)
|
||||
|
||||
func NewStorageMountResource() resource.Resource {
|
||||
return &storageMountResource{}
|
||||
}
|
||||
|
||||
type storageMountResource struct {
|
||||
client *client.CosmosClient
|
||||
}
|
||||
|
||||
// ---------- Terraform model ----------
|
||||
|
||||
type storageMountModel struct {
|
||||
Path types.String `tfsdk:"path"`
|
||||
MountPoint types.String `tfsdk:"mount_point"`
|
||||
Permanent types.Bool `tfsdk:"permanent"`
|
||||
NetDisk types.Bool `tfsdk:"net_disk"`
|
||||
Chown types.String `tfsdk:"chown"`
|
||||
}
|
||||
|
||||
// ---------- resource.Resource ----------
|
||||
|
||||
func (r *storageMountResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_storage_mount"
|
||||
}
|
||||
|
||||
func (r *storageMountResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "Manages a storage mount in Cosmos Server.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"path": schema.StringAttribute{
|
||||
Description: "The source path to mount.",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
"mount_point": schema.StringAttribute{
|
||||
Description: "The mount point destination. Used as the resource ID.",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
"permanent": schema.BoolAttribute{
|
||||
Description: "Whether the mount should persist across reboots.",
|
||||
Optional: true,
|
||||
PlanModifiers: []planmodifier.Bool{
|
||||
boolplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
"net_disk": schema.BoolAttribute{
|
||||
Description: "Whether this is a network disk mount.",
|
||||
Optional: true,
|
||||
PlanModifiers: []planmodifier.Bool{
|
||||
boolplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
"chown": schema.StringAttribute{
|
||||
Description: "Change ownership of the mount point (e.g. \"1000:1000\").",
|
||||
Optional: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *storageMountResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
c, ok := req.ProviderData.(*client.CosmosClient)
|
||||
if !ok {
|
||||
resp.Diagnostics.AddError(
|
||||
"Unexpected Resource Configure Type",
|
||||
fmt.Sprintf("Expected *client.CosmosClient, got: %T", req.ProviderData),
|
||||
)
|
||||
return
|
||||
}
|
||||
r.client = c
|
||||
}
|
||||
|
||||
// ---------- CRUD ----------
|
||||
|
||||
func (r *storageMountResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||
var plan storageMountModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
payload := r.modelToSDK(&plan)
|
||||
|
||||
httpResp, err := r.client.Raw.PostApiMount(ctx, payload)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error creating storage mount", err.Error())
|
||||
return
|
||||
}
|
||||
if err := client.CheckResponse(httpResp); err != nil {
|
||||
resp.Diagnostics.AddError("Error creating storage mount", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Read back to verify mount was created and populate state.
|
||||
r.readMount(ctx, plan.MountPoint.ValueString(), &plan, &resp.Diagnostics)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||
}
|
||||
|
||||
func (r *storageMountResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||
var state storageMountModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
mountPoint := state.MountPoint.ValueString()
|
||||
found := r.readMount(ctx, mountPoint, &state, &resp.Diagnostics)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
if !found {
|
||||
// Mount no longer exists; remove from state.
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
|
||||
}
|
||||
|
||||
func (r *storageMountResource) Update(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||
// All attributes have RequiresReplace, so Update should never be called.
|
||||
resp.Diagnostics.AddError(
|
||||
"Update not supported",
|
||||
"All storage mount attributes require replacement. This is an internal error.",
|
||||
)
|
||||
}
|
||||
|
||||
func (r *storageMountResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||
var state storageMountModel
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
payload := cosmossdk.StorageMountRequest{
|
||||
Path: state.Path.ValueString(),
|
||||
MountPoint: state.MountPoint.ValueString(),
|
||||
}
|
||||
|
||||
httpResp, err := r.client.Raw.PostApiUnmount(ctx, payload)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error deleting storage mount", err.Error())
|
||||
return
|
||||
}
|
||||
if err := client.CheckResponse(httpResp); err != nil {
|
||||
resp.Diagnostics.AddError("Error deleting storage mount", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (r *storageMountResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||
resource.ImportStatePassthroughID(ctx, path.Root("mount_point"), req, resp)
|
||||
}
|
||||
|
||||
// ---------- helpers ----------
|
||||
|
||||
// readMount fetches all mounts and finds the one matching the given mount_point.
|
||||
// Returns true if the mount was found, false otherwise.
|
||||
func (r *storageMountResource) readMount(ctx context.Context, mountPoint string, model *storageMountModel, diags interface {
|
||||
AddError(string, string)
|
||||
HasError() bool
|
||||
}) bool {
|
||||
httpResp, err := r.client.Raw.GetApiMounts(ctx)
|
||||
if err != nil {
|
||||
diags.AddError("Error reading storage mounts", err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
rawData, err := client.ParseRawResponse(httpResp)
|
||||
if err != nil {
|
||||
if client.IsNotFound(err) {
|
||||
return false
|
||||
}
|
||||
diags.AddError("Error reading storage mounts", err.Error())
|
||||
return false
|
||||
}
|
||||
if rawData == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// The response may be an array of mount objects or a map. Try array first.
|
||||
var mounts []map[string]interface{}
|
||||
if err := json.Unmarshal(rawData, &mounts); err != nil {
|
||||
// Try as a map keyed by mount point.
|
||||
var mountMap map[string]map[string]interface{}
|
||||
if err2 := json.Unmarshal(rawData, &mountMap); err2 != nil {
|
||||
diags.AddError("Error parsing storage mounts response", fmt.Sprintf("array: %s, map: %s", err.Error(), err2.Error()))
|
||||
return false
|
||||
}
|
||||
// Convert map to slice for uniform handling.
|
||||
for mp, m := range mountMap {
|
||||
m["mountPoint"] = mp
|
||||
mounts = append(mounts, m)
|
||||
}
|
||||
}
|
||||
|
||||
for _, m := range mounts {
|
||||
mp, _ := m["mountPoint"].(string)
|
||||
if mp == "" {
|
||||
mp, _ = m["mount_point"].(string)
|
||||
}
|
||||
if mp != mountPoint {
|
||||
continue
|
||||
}
|
||||
|
||||
// Found the mount. Populate path if available.
|
||||
if p, ok := m["path"].(string); ok {
|
||||
model.Path = types.StringValue(p)
|
||||
}
|
||||
model.MountPoint = types.StringValue(mountPoint)
|
||||
|
||||
if perm, ok := m["permanent"].(bool); ok {
|
||||
model.Permanent = types.BoolValue(perm)
|
||||
} else if !model.Permanent.IsNull() {
|
||||
// Preserve existing state value if not returned.
|
||||
}
|
||||
|
||||
if nd, ok := m["netDisk"].(bool); ok {
|
||||
model.NetDisk = types.BoolValue(nd)
|
||||
} else if ndAlt, ok := m["net_disk"].(bool); ok {
|
||||
model.NetDisk = types.BoolValue(ndAlt)
|
||||
}
|
||||
|
||||
if ch, ok := m["chown"].(string); ok && ch != "" {
|
||||
model.Chown = types.StringValue(ch)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// modelToSDK converts the Terraform model to an SDK StorageMountRequest.
|
||||
func (r *storageMountResource) modelToSDK(model *storageMountModel) cosmossdk.StorageMountRequest {
|
||||
req := cosmossdk.StorageMountRequest{
|
||||
Path: model.Path.ValueString(),
|
||||
MountPoint: model.MountPoint.ValueString(),
|
||||
}
|
||||
|
||||
if !model.Permanent.IsNull() && !model.Permanent.IsUnknown() {
|
||||
req.Permanent = client.BoolPtr(model.Permanent.ValueBool())
|
||||
}
|
||||
if !model.NetDisk.IsNull() && !model.NetDisk.IsUnknown() {
|
||||
req.NetDisk = client.BoolPtr(model.NetDisk.ValueBool())
|
||||
}
|
||||
if !model.Chown.IsNull() && !model.Chown.IsUnknown() {
|
||||
req.Chown = client.StringPtr(model.Chown.ValueString())
|
||||
}
|
||||
|
||||
return req
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
cosmossdk "github.com/azukaar/cosmos-server/go-sdk"
|
||||
"github.com/azukaar/terraform-provider-cosmos/internal/client"
|
||||
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||
"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"
|
||||
)
|
||||
|
||||
var (
|
||||
_ resource.Resource = &userResource{}
|
||||
_ resource.ResourceWithImportState = &userResource{}
|
||||
)
|
||||
|
||||
func NewUserResource() resource.Resource {
|
||||
return &userResource{}
|
||||
}
|
||||
|
||||
type userResource struct {
|
||||
client *client.CosmosClient
|
||||
}
|
||||
|
||||
// ---------- Terraform model ----------
|
||||
|
||||
type userModel struct {
|
||||
Nickname types.String `tfsdk:"nickname"`
|
||||
Email types.String `tfsdk:"email"`
|
||||
Role types.Int64 `tfsdk:"role"`
|
||||
Password types.String `tfsdk:"password"`
|
||||
CreatedAt types.String `tfsdk:"created_at"`
|
||||
LastLogin types.String `tfsdk:"last_login"`
|
||||
}
|
||||
|
||||
// ---------- resource.Resource ----------
|
||||
|
||||
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: "Manages a Cosmos Server user account.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"nickname": schema.StringAttribute{
|
||||
Description: "Unique nickname of the user. Used as the resource ID.",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
"email": schema.StringAttribute{
|
||||
Description: "Email address of the user.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"role": schema.Int64Attribute{
|
||||
Description: "Role of the user (e.g. 1 = admin, 2 = user).",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"password": schema.StringAttribute{
|
||||
Description: "Password for the user. Only used during creation; never read back from the API.",
|
||||
Optional: true,
|
||||
Sensitive: true,
|
||||
},
|
||||
"created_at": schema.StringAttribute{
|
||||
Description: "Timestamp when the user was created.",
|
||||
Computed: true,
|
||||
},
|
||||
"last_login": schema.StringAttribute{
|
||||
Description: "Timestamp of the user's last login.",
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *userResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
c, ok := req.ProviderData.(*client.CosmosClient)
|
||||
if !ok {
|
||||
resp.Diagnostics.AddError(
|
||||
"Unexpected Resource Configure Type",
|
||||
fmt.Sprintf("Expected *client.CosmosClient, got %T", req.ProviderData),
|
||||
)
|
||||
return
|
||||
}
|
||||
r.client = c
|
||||
}
|
||||
|
||||
// ---------- CRUD ----------
|
||||
|
||||
func (r *userResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||
var plan userModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
createReq := cosmossdk.UserCreateRequestJSON{
|
||||
Nickname: plan.Nickname.ValueString(),
|
||||
}
|
||||
if !plan.Email.IsNull() && !plan.Email.IsUnknown() {
|
||||
createReq.Email = client.StringPtr(plan.Email.ValueString())
|
||||
}
|
||||
if !plan.Role.IsNull() && !plan.Role.IsUnknown() {
|
||||
createReq.Role = client.IntPtr(int(plan.Role.ValueInt64()))
|
||||
}
|
||||
|
||||
httpResp, err := r.client.Raw.PostApiUsers(ctx, createReq)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error creating user", err.Error())
|
||||
return
|
||||
}
|
||||
if err := client.CheckResponse(httpResp); err != nil {
|
||||
resp.Diagnostics.AddError("Error creating user", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Read back the created user to populate computed fields.
|
||||
if !readUserIntoModel(ctx, r.client, plan.Nickname.ValueString(), &plan, &resp.Diagnostics) {
|
||||
resp.Diagnostics.AddError("Error reading user", "User not found after creation")
|
||||
return
|
||||
}
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
found := readUserIntoModel(ctx, r.client, state.Nickname.ValueString(), &state, &resp.Diagnostics)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
if !found {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
|
||||
}
|
||||
|
||||
func (r *userResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||
var plan userModel
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
editReq := cosmossdk.UserEditRequestJSON{}
|
||||
if !plan.Email.IsNull() && !plan.Email.IsUnknown() {
|
||||
editReq.Email = client.StringPtr(plan.Email.ValueString())
|
||||
}
|
||||
if !plan.Role.IsNull() && !plan.Role.IsUnknown() {
|
||||
editReq.Role = client.IntPtr(int(plan.Role.ValueInt64()))
|
||||
}
|
||||
|
||||
httpResp, err := r.client.Raw.PatchApiUsersNickname(ctx, plan.Nickname.ValueString(), editReq)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error updating user", err.Error())
|
||||
return
|
||||
}
|
||||
if err := client.CheckResponse(httpResp); err != nil {
|
||||
resp.Diagnostics.AddError("Error updating user", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Read back to capture any server-side changes.
|
||||
if !readUserIntoModel(ctx, r.client, plan.Nickname.ValueString(), &plan, &resp.Diagnostics) {
|
||||
resp.Diagnostics.AddError("Error reading user", "User not found after update")
|
||||
return
|
||||
}
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
httpResp, err := r.client.Raw.DeleteApiUsersNickname(ctx, state.Nickname.ValueString())
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Error deleting user", err.Error())
|
||||
return
|
||||
}
|
||||
if err := client.CheckResponse(httpResp); err != nil {
|
||||
resp.Diagnostics.AddError("Error deleting user", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (r *userResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||
resource.ImportStatePassthroughID(ctx, path.Root("nickname"), req, resp)
|
||||
}
|
||||
|
||||
// ---------- helpers ----------
|
||||
|
||||
// readUserIntoModel fetches a user by nickname from the API and populates the model.
|
||||
// It returns false if the user was not found (404), in which case no diagnostic
|
||||
// error is added — the caller should remove the resource from state.
|
||||
func readUserIntoModel(ctx context.Context, c *client.CosmosClient, nickname string, model *userModel, diags *diag.Diagnostics) bool {
|
||||
httpResp, err := c.Raw.GetApiUsersNickname(ctx, nickname)
|
||||
if err != nil {
|
||||
diags.AddError("Error reading user", err.Error())
|
||||
return true
|
||||
}
|
||||
user, err := client.ParseResponse[cosmossdk.UtilsUser](httpResp)
|
||||
if err != nil {
|
||||
if client.IsNotFound(err) {
|
||||
return false
|
||||
}
|
||||
diags.AddError("Error reading user", err.Error())
|
||||
return true
|
||||
}
|
||||
if user == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
model.Nickname = types.StringValue(user.Nickname)
|
||||
model.Email = types.StringValue(client.StringPtrVal(user.Email))
|
||||
model.Role = types.Int64Value(int64(user.Role))
|
||||
model.CreatedAt = types.StringValue(client.StringPtrVal(user.CreatedAt))
|
||||
model.LastLogin = types.StringValue(client.StringPtrVal(user.LastLogin))
|
||||
// Password is write-only; never populated from API responses.
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"log"
|
||||
|
||||
"github.com/azukaar/terraform-provider-cosmos/internal/provider"
|
||||
"github.com/hashicorp/terraform-plugin-framework/providerserver"
|
||||
)
|
||||
|
||||
var version = "dev"
|
||||
|
||||
func main() {
|
||||
var debug bool
|
||||
flag.BoolVar(&debug, "debug", false, "set to true to run the provider with support for debuggers like delve")
|
||||
flag.Parse()
|
||||
|
||||
opts := providerserver.ServeOpts{
|
||||
Address: "cosmos-cloud.io/azukaar/cosmos",
|
||||
Debug: debug,
|
||||
}
|
||||
|
||||
err := providerserver.Serve(context.Background(), provider.New(version), opts)
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user