Skip to content

Commit 3b02d05

Browse files
bchoCopilotCopilot
authored
feat: implement management command support (#51)
* feat: implement msi auth support * doc: document msi resource id usage support * refactor: expose `cmd` package for library usage * feat: implement management command * test: add test for management command * doc: update readme * Update internal/kusto/management.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Initial plan * Initial plan to fix debug message for management commands Co-authored-by: bcho <1975118+bcho@users.noreply.github.com> * Fix debug message for management commands from 'file ingestion settings' to 'management command settings' Co-authored-by: bcho <1975118+bcho@users.noreply.github.com> * Remove kusto-ingest binary from version control and add to .gitignore Co-authored-by: bcho <1975118+bcho@users.noreply.github.com> * fix: replace deprecreated opts --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: bcho <1975118+bcho@users.noreply.github.com>
1 parent a1be771 commit 3b02d05

7 files changed

Lines changed: 251 additions & 11 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
*.so
99
*.dylib
1010

11+
# Built binary
12+
kusto-ingest
13+
1114
# Test binary, built with `go test -c`
1215
*.test
1316

README.md

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22

33
Ingest logs data to Kusto with `github.com/Azure/azure-kusto-go`.
44

5-
## Usage - Ingest file
5+
6+
## Usage
7+
8+
### Ingest file
69

710
```
811
$ kusto-ingest file ./testdata/logs.multijson \
@@ -13,32 +16,58 @@ $ kusto-ingest file ./testdata/logs.multijson \
1316
--kusto-table="TestTable"
1417
```
1518

16-
### Authentication - AZCLI
19+
### Management commands
20+
21+
Run Kusto management commands from a file (e.g., create tables, update policies):
22+
23+
```
24+
$ kusto-ingest management ./testdata/commands.kql \
25+
--auth-azcli \
26+
--kusto-endpoint="https://test.kusto.windows.net" \
27+
--kusto-database="Test"
28+
```
1729

18-
`kusto-ingest` supports using azcli for authentication. This option is helpful for OIDC based pipeline usage.
19-
You can use `--auth-azcli` to enable it.
30+
Where `./testdata/commands.kql` contains Kusto management commands, e.g.:
31+
32+
```
33+
.create table TestTable (Timestamp: datetime, Message: string)
34+
.alter table TestTable policy update @'{"SoftDeletePeriod": "P365D"}'
35+
```
36+
37+
#### Options for management subcommand
38+
39+
- `--auth-azcli` or other authentication options (see below)
40+
- `--kusto-endpoint` (required)
41+
- `--kusto-database` (required)
42+
43+
44+
### Authentication
45+
46+
#### AZCLI
47+
48+
Use Azure CLI for authentication (helpful for OIDC-based pipeline usage):
2049

2150
```
2251
$ kusto-ingest file ./testdata/logs.multijson \
2352
# ... other options
2453
--auth-azcli
2554
```
2655

27-
### Authentication - Managed Identity
2856

29-
`kusto-ingest` supports using managed identity for authentication. This option is helpful for Azure services running in Azure.
30-
You can use `--auth-managed-identity-resource-id=<mi-resource-id>` to enable it.
57+
#### Managed Identity
58+
59+
Use managed identity for authentication (helpful for Azure services running in Azure):
3160

3261
```
3362
$ kusto-ingest file ./testdata/logs.multijson \
3463
# ... other options
3564
--auth-managed-identity-resource-id=<mi-resource-id>
3665
```
3766

38-
### Authentication - Service Principal ID and Secret (not recommended)
3967

40-
`kusto-ingest` supports using service principal ID and secret for authentication. This is helpful for existing
41-
pipeline usage with service principal ID and secret. But this is not recommended for new pipeline usage.
68+
#### Service Principal ID and Secret (not recommended)
69+
70+
Use service principal ID and secret for authentication (not recommended for new pipelines):
4271

4372
```
4473
$ kusto-ingest file ./testdata/logs.multijson \

cmd/cli.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import (
1010
var CLI struct {
1111
Verbose bool `short:"v" help:"Enable verbose logging."`
1212

13-
File kusto.FileIngestOptions `cmd:"" help:"Ingest data from local file."`
13+
File kusto.FileIngestOptions `cmd:"" help:"Ingest data from local file."`
14+
Management kusto.ManagementOptions `cmd:"" aliases:"mgmt" help:"Run Kusto management commands from a file."`
1415
}
1516

1617
// Main is the entry point for the CLI application.

internal/kusto/management.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package kusto
2+
3+
import (
4+
"fmt"
5+
"time"
6+
7+
"github.com/Azure/azure-kusto-go/kusto/kql"
8+
"github.com/Azure/kusto-ingest/internal/cli"
9+
)
10+
11+
func (m ManagementOptions) Run(cli cli.Provider) error {
12+
cli.Logger().Debug(
13+
"management command settings",
14+
"target.endpoint", m.KustoTarget.Endpoint,
15+
"target.database", m.KustoTarget.Database,
16+
"auth.tenant", m.Auth.TenantID,
17+
"auth.clientID", m.Auth.ClientID,
18+
)
19+
20+
queryer, err := m.createQueryClient(m.KustoTarget, m.Auth)
21+
if err != nil {
22+
return fmt.Errorf("create Kusto query client: %w", err)
23+
}
24+
defer func() { _ = queryer.Close() }()
25+
26+
ctx, cancel := cli.Context()
27+
defer cancel()
28+
29+
cli.Logger().Info("executing management commands")
30+
start := time.Now()
31+
stmt := kql.New("").AddUnsafe(string(m.Source))
32+
_, err = queryer.Mgmt(ctx, m.KustoTarget.Database, stmt)
33+
if err != nil {
34+
return fmt.Errorf("execute management commands: %w", err)
35+
}
36+
cli.Logger().Info("management commands executed", "duration", time.Since(start))
37+
38+
return nil
39+
}

internal/kusto/management_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package kusto
2+
3+
import (
4+
"context"
5+
"errors"
6+
"testing"
7+
8+
"github.com/Azure/azure-kusto-go/kusto"
9+
"github.com/Azure/azure-kusto-go/kusto/ingest"
10+
"github.com/Azure/kusto-ingest/internal/cli/testingcli"
11+
"github.com/Azure/kusto-ingest/internal/kusto/testingkusto"
12+
"github.com/stretchr/testify/assert"
13+
)
14+
15+
func Test_ManagementOptions_Run_Success(t *testing.T) {
16+
cli := testingcli.New()
17+
18+
executed := false
19+
q := testingkusto.NewQueryClient(func(qc *testingkusto.QueryClient) {
20+
qc.MgmtFn = func(_ context.Context, db string, stmt kusto.Statement, _ ...kusto.QueryOption) (*kusto.RowIterator, error) {
21+
executed = true
22+
assert.Equal(t, "TestDatabase", db)
23+
return nil, nil
24+
}
25+
})
26+
27+
opts := ManagementOptions{
28+
Source: []byte(".show tables"),
29+
Auth: newTestAuth(),
30+
KustoTarget: newTestKustoTarget(),
31+
ingestorBuildSettings: ingestorBuildSettings{
32+
CreateQueryClient: func(target KustoTargetOptions, auth AuthOptions) (ingest.QueryClient, error) {
33+
return q, nil
34+
},
35+
},
36+
}
37+
38+
err := opts.Run(cli)
39+
assert.NoError(t, err)
40+
assert.True(t, executed, "expected management command to execute")
41+
}
42+
43+
func Test_ManagementOptions_Run_CreateClientError(t *testing.T) {
44+
cli := testingcli.New()
45+
46+
opts := ManagementOptions{
47+
Source: []byte(".show tables"),
48+
Auth: newTestAuth(),
49+
KustoTarget: newTestKustoTarget(),
50+
ingestorBuildSettings: ingestorBuildSettings{
51+
CreateQueryClient: func(target KustoTargetOptions, auth AuthOptions) (ingest.QueryClient, error) {
52+
return nil, errors.New("boom")
53+
},
54+
},
55+
}
56+
57+
err := opts.Run(cli)
58+
assert.Error(t, err)
59+
assert.Contains(t, err.Error(), "create Kusto query client")
60+
}
61+
62+
func Test_ManagementOptions_Run_MgmtError(t *testing.T) {
63+
cli := testingcli.New()
64+
65+
q := testingkusto.NewQueryClient(func(qc *testingkusto.QueryClient) {
66+
qc.MgmtFn = func(_ context.Context, db string, stmt kusto.Statement, _ ...kusto.QueryOption) (*kusto.RowIterator, error) {
67+
return nil, errors.New("mgmt failed")
68+
}
69+
})
70+
71+
opts := ManagementOptions{
72+
Source: []byte(".show tables"),
73+
Auth: newTestAuth(),
74+
KustoTarget: newTestKustoTarget(),
75+
ingestorBuildSettings: ingestorBuildSettings{
76+
CreateQueryClient: func(target KustoTargetOptions, auth AuthOptions) (ingest.QueryClient, error) {
77+
return q, nil
78+
},
79+
},
80+
}
81+
82+
err := opts.Run(cli)
83+
assert.Error(t, err)
84+
assert.Contains(t, err.Error(), "execute management commands")
85+
}

internal/kusto/options.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,14 @@ type FileIngestOptions struct {
3131
// for unit test
3232
ingestorBuildSettings `kong:"-"`
3333
}
34+
35+
// ManagementOptions provides the configuration for management commands.
36+
type ManagementOptions struct {
37+
Source []byte `arg:"" type:"filecontent" required:"" help:"The source file to execute."`
38+
39+
Auth AuthOptions `embed:"" prefix:"auth-"`
40+
KustoTarget KustoTargetOptions `embed:"" prefix:"kusto-"`
41+
42+
// for unit test
43+
ingestorBuildSettings `kong:"-"`
44+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package testingkusto
2+
3+
import (
4+
"context"
5+
"net/http"
6+
7+
"github.com/Azure/azure-kusto-go/kusto"
8+
"github.com/Azure/azure-kusto-go/kusto/ingest"
9+
)
10+
11+
// QueryClient is a fake implementation of ingest.QueryClient for tests.
12+
type QueryClient struct {
13+
MgmtFn func(ctx context.Context, db string, query kusto.Statement, options ...kusto.QueryOption) (*kusto.RowIterator, error)
14+
QueryFn func(ctx context.Context, db string, query kusto.Statement, options ...kusto.QueryOption) (*kusto.RowIterator, error)
15+
CloseFn func() error
16+
17+
// Captured calls for assertions.
18+
MgmtCalls []MgmtCall
19+
QueryCalls []QueryCall
20+
}
21+
22+
// MgmtCall captures a Mgmt invocation.
23+
type MgmtCall struct {
24+
DB string
25+
Query kusto.Statement
26+
}
27+
28+
// QueryCall captures a Query invocation.
29+
type QueryCall struct {
30+
DB string
31+
Query kusto.Statement
32+
}
33+
34+
var _ ingest.QueryClient = (*QueryClient)(nil)
35+
36+
func (qc *QueryClient) Close() error {
37+
if qc.CloseFn != nil {
38+
return qc.CloseFn()
39+
}
40+
return nil
41+
}
42+
43+
func (qc *QueryClient) Auth() kusto.Authorization { return kusto.Authorization{} }
44+
func (qc *QueryClient) Endpoint() string { return "https://example.kusto.windows.net" }
45+
46+
func (qc *QueryClient) Query(ctx context.Context, db string, query kusto.Statement, options ...kusto.QueryOption) (*kusto.RowIterator, error) {
47+
qc.QueryCalls = append(qc.QueryCalls, QueryCall{DB: db, Query: query})
48+
if qc.QueryFn != nil {
49+
return qc.QueryFn(ctx, db, query, options...)
50+
}
51+
return nil, nil
52+
}
53+
54+
func (qc *QueryClient) Mgmt(ctx context.Context, db string, query kusto.Statement, options ...kusto.QueryOption) (*kusto.RowIterator, error) {
55+
qc.MgmtCalls = append(qc.MgmtCalls, MgmtCall{DB: db, Query: query})
56+
if qc.MgmtFn != nil {
57+
return qc.MgmtFn(ctx, db, query, options...)
58+
}
59+
return nil, nil
60+
}
61+
62+
func (qc *QueryClient) HttpClient() *http.Client { return nil }
63+
func (qc *QueryClient) ClientDetails() *kusto.ClientDetails { return &kusto.ClientDetails{} }
64+
65+
// NewQueryClient creates a QueryClient with optional mutators.
66+
func NewQueryClient(ms ...func(*QueryClient)) *QueryClient {
67+
qc := &QueryClient{}
68+
for _, m := range ms {
69+
m(qc)
70+
}
71+
return qc
72+
}

0 commit comments

Comments
 (0)