Skip to content

Commit 52994ec

Browse files
authored
feat(dns): Onboard lookup command (#1732)
STACKITCLI-454
1 parent 5a3644a commit 52994ec

5 files changed

Lines changed: 661 additions & 0 deletions

File tree

‎docs/stackit_dns.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ stackit dns [flags]
3030
### SEE ALSO
3131

3232
* [stackit](./stackit.md) - Manage STACKIT resources using the command line
33+
* [stackit dns lookup](./stackit_dns_lookup.md) - Looks up a domain on the STACKIT DNS resolvers
3334
* [stackit dns record-set](./stackit_dns_record-set.md) - Provides functionality for DNS record set
3435
* [stackit dns zone](./stackit_dns_zone.md) - Provides functionality for DNS zones
3536

‎docs/stackit_dns_lookup.md‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
## stackit dns lookup
2+
3+
Looks up a domain on the STACKIT DNS resolvers
4+
5+
### Synopsis
6+
7+
Looks up a domain on the STACKIT DNS resolvers.
8+
9+
```
10+
stackit dns lookup DOMAIN [flags]
11+
```
12+
13+
### Examples
14+
15+
```
16+
Look up domain "www.example.com"
17+
$ stackit dns lookup www.example.com
18+
19+
Look up the AAAA records of domain "www.example.com" on all STACKIT DNS resolvers
20+
$ stackit dns lookup www.example.com --record-type AAAA --all-resolvers
21+
22+
Look up domain "www.example.com" including the raw DNS response in JSON format
23+
$ stackit dns lookup www.example.com --include-raw-response --output-format json
24+
```
25+
26+
### Options
27+
28+
```
29+
--all-resolvers[=true|false] If set, all STACKIT DNS resolvers are queried instead of a single one
30+
-h, --help[=true|false] Help for "stackit dns lookup"
31+
--include-raw-response[=true|false] If set, the raw DNS response is included in the output. Only shown in JSON and YAML output
32+
--record-type string Record type to look up. If not set, defaults to A (one of: [A, AAAA, SOA, CNAME, NS, MX, TXT, SRV, PTR, ALIAS, DNAME, CAA, DNSKEY, DS, LOC, NAPTR, SSHFP, TLSA, URI, CERT, SVCB, TYPE, CSYNC, HINFO, HTTPS, ANY])
33+
```
34+
35+
### Options inherited from parent commands
36+
37+
```
38+
-y, --assume-yes[=true|false] If set, skips all confirmation prompts
39+
--async[=true|false] If set, runs the command asynchronously
40+
-o, --output-format string Output format, (one of: [json, pretty, none, yaml])
41+
-p, --project-id string Project ID
42+
--region string Target region for region-specific requests
43+
--verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info")
44+
```
45+
46+
### SEE ALSO
47+
48+
* [stackit dns](./stackit_dns.md) - Provides functionality for DNS
49+

‎internal/cmd/dns/dns.go‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package dns
22

33
import (
4+
"github.com/stackitcloud/stackit-cli/internal/cmd/dns/lookup"
45
recordset "github.com/stackitcloud/stackit-cli/internal/cmd/dns/record-set"
56
"github.com/stackitcloud/stackit-cli/internal/cmd/dns/zone"
67
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
@@ -25,4 +26,5 @@ func NewCmd(params *types.CmdParams) *cobra.Command {
2526
func addSubcommands(cmd *cobra.Command, params *types.CmdParams) {
2627
cmd.AddCommand(zone.NewCmd(params))
2728
cmd.AddCommand(recordset.NewCmd(params))
29+
cmd.AddCommand(lookup.NewCmd(params))
2830
}

‎internal/cmd/dns/lookup/lookup.go‎

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package lookup
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/stackitcloud/stackit-cli/internal/pkg/types"
8+
9+
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
10+
"github.com/stackitcloud/stackit-cli/internal/pkg/examples"
11+
"github.com/stackitcloud/stackit-cli/internal/pkg/flags"
12+
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
13+
"github.com/stackitcloud/stackit-cli/internal/pkg/print"
14+
"github.com/stackitcloud/stackit-cli/internal/pkg/services/dns/client"
15+
"github.com/stackitcloud/stackit-cli/internal/pkg/tables"
16+
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
17+
18+
"github.com/spf13/cobra"
19+
dns "github.com/stackitcloud/stackit-sdk-go/services/dns/v1api"
20+
)
21+
22+
const (
23+
domainArg = "DOMAIN"
24+
25+
allResolversFlag = "all-resolvers"
26+
includeRawResponseFlag = "include-raw-response"
27+
)
28+
29+
var recordTypeFlag = flags.StringEnumFlag(
30+
"record-type",
31+
dns.AllowedLookupDomainPayloadRecordTypeEnumValues,
32+
"Record type to look up. If not set, defaults to A",
33+
)
34+
35+
type inputModel struct {
36+
*globalflags.GlobalFlagModel
37+
Domain string
38+
RecordType *dns.LookupDomainPayloadRecordType
39+
AllResolvers bool
40+
IncludeRawResponse bool
41+
}
42+
43+
func NewCmd(params *types.CmdParams) *cobra.Command {
44+
cmd := &cobra.Command{
45+
Use: fmt.Sprintf("lookup %s", domainArg),
46+
Short: "Looks up a domain on the STACKIT DNS resolvers",
47+
Long: "Looks up a domain on the STACKIT DNS resolvers.",
48+
Args: args.SingleArg(domainArg, nil),
49+
Example: examples.Build(
50+
examples.NewExample(
51+
`Look up domain "www.example.com"`,
52+
"$ stackit dns lookup www.example.com"),
53+
examples.NewExample(
54+
`Look up the AAAA records of domain "www.example.com" on all STACKIT DNS resolvers`,
55+
"$ stackit dns lookup www.example.com --record-type AAAA --all-resolvers"),
56+
examples.NewExample(
57+
`Look up domain "www.example.com" including the raw DNS response in JSON format`,
58+
"$ stackit dns lookup www.example.com --include-raw-response --output-format json"),
59+
),
60+
RunE: func(cmd *cobra.Command, args []string) error {
61+
ctx := context.Background()
62+
model, err := parseInput(params.Printer, cmd, args)
63+
if err != nil {
64+
return err
65+
}
66+
67+
// Configure API client
68+
apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion)
69+
if err != nil {
70+
return err
71+
}
72+
73+
// Call API
74+
req := buildRequest(ctx, model, apiClient)
75+
resp, err := req.Execute()
76+
if err != nil {
77+
return fmt.Errorf("look up DNS domain: %w", err)
78+
}
79+
80+
return outputResult(params.Printer, model, resp)
81+
},
82+
}
83+
configureFlags(cmd)
84+
return cmd
85+
}
86+
87+
func configureFlags(cmd *cobra.Command) {
88+
recordTypeFlag.Register(cmd.Flags())
89+
cmd.Flags().Bool(allResolversFlag, false, "If set, all STACKIT DNS resolvers are queried instead of a single one")
90+
cmd.Flags().Bool(includeRawResponseFlag, false, "If set, the raw DNS response is included in the output. Only shown in JSON and YAML output")
91+
}
92+
93+
func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
94+
domain := inputArgs[0]
95+
globalFlags := globalflags.Parse(p, cmd)
96+
97+
model := inputModel{
98+
GlobalFlagModel: globalFlags,
99+
Domain: domain,
100+
RecordType: recordTypeFlag.Ptr(),
101+
AllResolvers: flags.FlagToBoolValue(p, cmd, allResolversFlag),
102+
IncludeRawResponse: flags.FlagToBoolValue(p, cmd, includeRawResponseFlag),
103+
}
104+
105+
if model.IncludeRawResponse && model.OutputFormat != print.JSONOutputFormat && model.OutputFormat != print.YAMLOutputFormat {
106+
p.Warn("--%s is only shown in JSON or YAML output, set --output-format json or --output-format yaml to see the raw DNS response\n", includeRawResponseFlag)
107+
}
108+
109+
p.DebugInputModel(model)
110+
return &model, nil
111+
}
112+
113+
func buildRequest(ctx context.Context, model *inputModel, apiClient *dns.APIClient) dns.ApiLookupDomainRequest {
114+
payload := dns.LookupDomainPayload{
115+
Address: utils.Ptr(model.Domain),
116+
RecordType: model.RecordType,
117+
}
118+
if model.AllResolvers {
119+
payload.Type = utils.Ptr(dns.LOOKUPDOMAINPAYLOADTYPE_ALL)
120+
}
121+
if model.IncludeRawResponse {
122+
payload.IncludeRawResponse = utils.Ptr(true)
123+
}
124+
if model.Region != "" {
125+
payload.Region = utils.Ptr(model.Region)
126+
}
127+
128+
req := apiClient.DefaultAPI.LookupDomain(ctx)
129+
req = req.LookupDomainPayload(payload)
130+
return req
131+
}
132+
133+
func outputResult(p *print.Printer, model *inputModel, resp *dns.LookupDomainResponse) error {
134+
return p.OutputResult(model.OutputFormat, resp, func() error {
135+
if resp == nil {
136+
return fmt.Errorf("look up DNS domain response is empty")
137+
}
138+
table := tables.NewTable()
139+
table.SetHeader("RESOLVER IP", "NAME", "TYPE", "TTL", "DATA")
140+
rows := 0
141+
for _, result := range resp.State {
142+
// When querying all resolvers, also list the ones without records,
143+
// so it's visible which resolvers answered
144+
if model.AllResolvers && len(result.Records) == 0 {
145+
table.AddRow(utils.PtrString(result.ResolverIP), "", "", "", "No records found")
146+
rows++
147+
continue
148+
}
149+
for _, record := range result.Records {
150+
table.AddRow(
151+
utils.PtrString(result.ResolverIP),
152+
utils.PtrString(record.Name),
153+
utils.PtrString(record.Type),
154+
utils.PtrString(record.Ttl),
155+
utils.PtrString(record.Data),
156+
)
157+
rows++
158+
}
159+
}
160+
if rows == 0 {
161+
p.Outputf("No records found for domain %q\n", model.Domain)
162+
return nil
163+
}
164+
165+
table.EnableAutoMergeOnColumns(1)
166+
err := table.Display(p)
167+
if err != nil {
168+
return fmt.Errorf("render table: %w", err)
169+
}
170+
return nil
171+
})
172+
}

0 commit comments

Comments
 (0)