Skip to content

Commit 1fc7648

Browse files
kprokopenkoclaude
andauthored
Add ydb_secret resource with inherit_permissions support (#60)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2cfb5f0 commit 1fc7648

19 files changed

Lines changed: 613 additions & 18 deletions

File tree

.github/workflows/lint.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
- name: golangci-lint
1818
uses: golangci/golangci-lint-action@v8
1919
with:
20-
version: v2.1.6
20+
version: v2.11.4
2121
autoformatter:
2222
name: autoformat check
2323
concurrency:
@@ -30,7 +30,7 @@ jobs:
3030
- name: Install Go
3131
uses: actions/setup-go@v5
3232
with:
33-
go-version: 1.22
33+
go-version: 1.25.x
3434

3535
- name: Install utilities
3636
run: |

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
strategy:
1515
fail-fast: false
1616
matrix:
17-
go-version: [1.21.x, 1.22.x]
17+
go-version: [1.25.x, 1.26.x]
1818
os: [ubuntu, windows, macOS]
1919
env:
2020
OS: ${{ matrix.os }}-latest

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,7 @@ This provider can be used for managing YDB schema resources in managed or on-pre
77
## Available resources
88
- [ydb_table](./internal/resources/table/README.md)
99
- [ydb_table_index](./internal/resources/table/index/README.md)
10-
- [ydb_table_changefeed](./internal/resources/changefeed/README.md)
10+
- [ydb_table_changefeed](./internal/resources/changefeed/README.md)
11+
- [ydb_external_data_source](./internal/resources/externaldatasource/README.md)
12+
- [ydb_external_table](./internal/resources/externaltable/README.md)
13+
- [ydb_secret](./internal/resources/secret/README.md)

go.mod

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
module github.com/ydb-platform/terraform-provider-ydb
22

3-
go 1.24.0
3+
go 1.24.13
44

55
require (
66
github.com/golang/mock v1.6.0
77
github.com/hashicorp/terraform-plugin-sdk/v2 v2.14.0
88
github.com/stretchr/testify v1.10.0
99
github.com/ydb-platform/ydb-go-sdk/v3 v3.134.0
10+
golang.org/x/crypto v0.48.0
1011
)
1112

1213
require google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
@@ -52,10 +53,10 @@ require (
5253
github.com/vmihailenco/tagparser v0.1.1 // indirect
5354
github.com/ydb-platform/ydb-go-genproto v0.0.0-20260311095541-ebbf792c1180
5455
github.com/zclconf/go-cty v1.10.0 // indirect
55-
golang.org/x/net v0.48.0 // indirect
56+
golang.org/x/net v0.49.0 // indirect
5657
golang.org/x/sync v0.19.0 // indirect
57-
golang.org/x/sys v0.39.0 // indirect
58-
golang.org/x/text v0.32.0 // indirect
58+
golang.org/x/sys v0.41.0 // indirect
59+
golang.org/x/text v0.34.0 // indirect
5960
google.golang.org/appengine v1.6.8 // indirect
6061
google.golang.org/grpc v1.78.0
6162
google.golang.org/protobuf v1.36.10

go.sum

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,8 @@ golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8U
203203
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
204204
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
205205
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
206+
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
207+
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
206208
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
207209
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
208210
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@@ -227,8 +229,8 @@ golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v
227229
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
228230
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
229231
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
230-
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
231-
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
232+
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
233+
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
232234
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
233235
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
234236
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -255,8 +257,8 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc
255257
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
256258
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
257259
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
258-
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
259-
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
260+
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
261+
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
260262
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
261263
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
262264
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -265,8 +267,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
265267
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
266268
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
267269
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
268-
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
269-
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
270+
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
271+
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
270272
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
271273
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
272274
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=

internal/helpers/dataproxy.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ type ResourceDataProxy interface {
77
GetOk(key string) (interface{}, bool)
88

99
// GetOkExists and methods below are bypassed (i.e. call schema.ResourceData directly)
10+
//
1011
// Deprecated: calls a deprecated method
1112
GetOkExists(key string) (interface{}, bool)
1213

internal/helpers/helpers.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,14 @@ func ParseYDBDatabaseEndpoint(endpoint string) (baseEP, databasePath string, use
5151
return parts[2], dbSplit[1], useTLS, nil
5252
}
5353

54+
func EscapeYQLString(s string) string {
55+
return strings.ReplaceAll(s, "'", "\\'")
56+
}
57+
58+
func EscapeYQLIdentifier(s string) string {
59+
return strings.ReplaceAll(s, "`", "``")
60+
}
61+
5462
func AppendWithEscape(buf []byte, s string) []byte {
5563
for i := 0; i < len(s); i++ {
5664
if s[i] == '"' || s[i] == '/' {
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# ydb_secret resource
2+
3+
`ydb_secret` resource is used to manage YDB secret objects.
4+
5+
## Example
6+
7+
### Static value
8+
9+
```tf
10+
resource "ydb_secret" "secret" {
11+
connection_string = "grpc://localhost:2136/?database=/local"
12+
name = "my_secret"
13+
value = "s3cr3t_v4lue"
14+
inherit_permissions = true
15+
}
16+
```
17+
18+
### Value from command
19+
20+
```tf
21+
resource "ydb_secret" "from_script" {
22+
connection_string = "grpc://localhost:2136/?database=/local"
23+
name = "my_secret"
24+
25+
command {
26+
path = "/usr/bin/bash"
27+
args = ["-c", "cat /run/secrets/db_password"]
28+
}
29+
}
30+
```
31+
32+
## Argument Reference
33+
34+
- `connection_string` (Required) - Connection string for YDB database.
35+
- `name` (Required) - Secret name.
36+
- `value` (Optional, Sensitive) - Secret value. Stored as a scrypt hash in Terraform state. Mutually exclusive with `command`.
37+
- `command` (Optional) - Command to execute to generate the secret value. The command's stdout is used as the value. Mutually exclusive with `value`.
38+
- `path` (Required) - Path to the executable.
39+
- `args` (Optional) - List of arguments to pass to the command.
40+
- `env` (Optional) - Map of environment variables to set for the command.
41+
- `inherit_permissions` (Optional, Default: `false`) - If `true`, the secret inherits access rights from its parent directory. If `false`, only `DESCRIBE SCHEMA` permission is inherited.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package secret
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"os/exec"
8+
9+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
10+
)
11+
12+
func resolveSecretValue(ctx context.Context, d *schema.ResourceData) (string, error) {
13+
if v, ok := d.GetOk("value"); ok {
14+
return v.(string), nil
15+
}
16+
17+
cmdList := d.Get("command").([]interface{})
18+
if len(cmdList) == 0 {
19+
return "", fmt.Errorf("either 'value' or 'command' must be specified")
20+
}
21+
22+
cmdMap := cmdList[0].(map[string]interface{})
23+
return runCommand(ctx, cmdMap)
24+
}
25+
26+
func runCommand(ctx context.Context, cmdMap map[string]interface{}) (string, error) {
27+
path := cmdMap["path"].(string)
28+
29+
var args []string
30+
if rawArgs, ok := cmdMap["args"].([]interface{}); ok {
31+
for _, a := range rawArgs {
32+
args = append(args, a.(string))
33+
}
34+
}
35+
36+
cmd := exec.CommandContext(ctx, path, args...)
37+
38+
if rawEnv, ok := cmdMap["env"].(map[string]interface{}); ok {
39+
for k, v := range rawEnv {
40+
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, v.(string)))
41+
}
42+
}
43+
44+
var stdout, stderr bytes.Buffer
45+
cmd.Stdout = &stdout
46+
cmd.Stderr = &stderr
47+
48+
if err := cmd.Run(); err != nil {
49+
return "", fmt.Errorf("command %q failed: %w\nstdout: %s\nstderr: %s", path, err, stdout.String(), stderr.String())
50+
}
51+
52+
return stdout.String(), nil
53+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package secret
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
8+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
9+
10+
"github.com/ydb-platform/terraform-provider-ydb/internal/helpers"
11+
tbl "github.com/ydb-platform/terraform-provider-ydb/internal/table"
12+
)
13+
14+
func (h *handler) Create(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
15+
connectionString := d.Get("connection_string").(string)
16+
name := d.Get("name").(string)
17+
inheritPermissions := d.Get("inherit_permissions").(bool)
18+
19+
value, err := resolveSecretValue(ctx, d)
20+
if err != nil {
21+
return diag.FromErr(err)
22+
}
23+
24+
db, err := tbl.CreateDBConnection(ctx, tbl.ClientParams{
25+
DatabaseEndpoint: connectionString,
26+
AuthCreds: h.authCreds,
27+
})
28+
if err != nil {
29+
return diag.Diagnostics{
30+
{Severity: diag.Error, Summary: "failed to initialize DB connection", Detail: err.Error()},
31+
}
32+
}
33+
defer func() {
34+
_ = db.Close(ctx)
35+
}()
36+
37+
escapedName := helpers.EscapeYQLIdentifier(name)
38+
escapedValue := helpers.EscapeYQLString(value)
39+
q := fmt.Sprintf("CREATE SECRET `%s` WITH (value = '%s')", escapedName, escapedValue)
40+
if inheritPermissions {
41+
q = fmt.Sprintf("CREATE SECRET `%s` WITH (value = '%s', inherit_permissions = True)", escapedName, escapedValue)
42+
}
43+
err = db.Query().Exec(ctx, q)
44+
if err != nil {
45+
return diag.Diagnostics{
46+
{Severity: diag.Error, Summary: "failed to executing `" + q + "`", Detail: err.Error()},
47+
}
48+
}
49+
50+
d.SetId(connectionString + "?path=" + name)
51+
52+
return h.Read(ctx, d, meta)
53+
}

0 commit comments

Comments
 (0)