diff --git a/messages/dnssec/errors.go b/messages/dnssec/errors.go new file mode 100644 index 00000000..0bcd610b --- /dev/null +++ b/messages/dnssec/errors.go @@ -0,0 +1,13 @@ +package dnssec + +import ( + "errors" +) + +var ( + ErrorGetDNSSEC = errors.New("Failed to describe the DNSSEC: %s. Check your settings and try again. If the error persists, contact Azion support.") + ErrorUpdateDNSSEC = errors.New("Failed to update the DNSSEC: %s. Check your settings and try again. If the error persists, contact Azion support.") + + ErrorConvertIdZone = errors.New("The DNS zone ID you provided is invalid. The value must be an integer. You may run the 'azion list dns-zone' command to check your DNS zones' IDs") + ErrorConvertEnabled = errors.New("The value provided for '--enabled' is invalid. The value must be a boolean (true or false)") +) diff --git a/messages/dnssec/messages.go b/messages/dnssec/messages.go new file mode 100644 index 00000000..f54b97fd --- /dev/null +++ b/messages/dnssec/messages.go @@ -0,0 +1,32 @@ +package dnssec + +var ( + // [ dnssec ] + DNSSECUsage = "dnssec" + DNSSECShortDescription = "Manages DNSSEC for an Intelligent DNS zone" + DNSSECLongDescription = "DNSSEC adds cryptographic signatures to your Intelligent DNS zone records, protecting against DNS spoofing and cache poisoning attacks." + DNSSECFlagHelp = "Displays more information about the dnssec command" + + // [ describe ] + DNSSECDescribeUsage = "describe --zone-id [flags]" + DNSSECDescribeShortDescription = "Returns the DNSSEC information of a specific DNS zone" + DNSSECDescribeLongDescription = "Returns the DNSSEC information of a specific DNS zone, informed through the flag '--zone-id', in detail" + DNSSECDescribeFlagOut = "Exports the output of the subcommand 'describe' to the given file path " + DNSSECDescribeFlagFormat = "Changes the output format passing the json value to the flag. Example '--format json'" + DNSSECDescribeHelpFlag = "Displays more information about the describe subcommand" + DNSSECDescribeAskInputZoneID = "Enter the ID of the DNS zone whose DNSSEC you wish to describe:" + + // [ update ] + DNSSECUpdateUsage = "update --zone-id --enabled [flags]" + DNSSECUpdateShortDescription = "Updates the DNSSEC of a DNS zone" + DNSSECUpdateLongDescription = "Enables or disables DNSSEC for a DNS zone based on the given attributes" + DNSSECUpdateFlagEnabled = "Whether DNSSEC should be enabled for the DNS zone" + DNSSECUpdateFlagIn = "Path to a JSON file containing the attributes of the DNSSEC that will be updated; you can use - for reading from stdin" + DNSSECUpdateOutputSuccess = "DNSSEC of DNS zone %d was updated\n" + DNSSECUpdateHelpFlag = "Displays more information about the update subcommand" + DNSSECUpdateAskInputZoneID = "Enter the ID of the DNS zone whose DNSSEC you wish to update:" + DNSSECUpdateAskInputEnabled = "Enter whether DNSSEC should be enabled (true/false):" + + // [ flags ] + DNSSECFlagZoneID = "Unique identifier for a DNS zone. The '--zone-id' flag is required" +) diff --git a/pkg/api/dnssec/client.go b/pkg/api/dnssec/client.go new file mode 100644 index 00000000..e374ce76 --- /dev/null +++ b/pkg/api/dnssec/client.go @@ -0,0 +1,29 @@ +package dnssec + +import ( + "net/http" + "time" + + "github.com/aziontech/azion-cli/pkg/cmd/version" + sdk "github.com/aziontech/azionapi-v4-go-sdk-dev/azion-api" +) + +type Client struct { + apiClient *sdk.APIClient +} + +func NewClient(c *http.Client, url string, token string) *Client { + conf := sdk.NewConfiguration() + conf.HTTPClient = c + conf.AddDefaultHeader("Authorization", "token "+token) + conf.AddDefaultHeader("Accept", "application/json") + conf.UserAgent = "Azion_CLI/" + version.BinVersion + conf.Servers = sdk.ServerConfigurations{ + {URL: url}, + } + conf.HTTPClient.Timeout = 50 * time.Second + + return &Client{ + apiClient: sdk.NewAPIClient(conf), + } +} diff --git a/pkg/api/dnssec/dnssec.go b/pkg/api/dnssec/dnssec.go new file mode 100644 index 00000000..f6921dd9 --- /dev/null +++ b/pkg/api/dnssec/dnssec.go @@ -0,0 +1,49 @@ +package dnssec + +import ( + "context" + + "github.com/aziontech/azion-cli/pkg/logger" + "github.com/aziontech/azion-cli/utils" + sdk "github.com/aziontech/azionapi-v4-go-sdk-dev/azion-api" + "go.uber.org/zap" +) + +func (c *Client) Get(ctx context.Context, zoneID int64) (sdk.DNSSEC, error) { + logger.Debug("Get DNSSEC") + + resp, httpResp, err := c.apiClient.DNSDNSSECAPI. + RetrieveDnssec(ctx, zoneID). + Execute() + if err != nil { + logger.Debug("Error while getting the DNSSEC", zap.Any("ID", zoneID), zap.Error(err)) + errBody, err := utils.LogAndRewindBodyV4(httpResp) + if err != nil { + return sdk.DNSSEC{}, err + } + + return sdk.DNSSEC{}, utils.ErrorPerStatusCodeV4(errBody, httpResp, err) + } + + return resp.GetData(), nil +} + +func (c *Client) Update(ctx context.Context, req sdk.PatchedDNSSECRequest, zoneID int64) (sdk.DNSSEC, error) { + logger.Debug("Update DNSSEC") + + request := c.apiClient.DNSDNSSECAPI. + PartialUpdateDnssec(ctx, zoneID). + PatchedDNSSECRequest(req) + resp, httpResp, err := request.Execute() + if err != nil { + logger.Debug("Error while updating the DNSSEC", zap.Any("ID", zoneID), zap.Error(err)) + errBody, err := utils.LogAndRewindBodyV4(httpResp) + if err != nil { + return sdk.DNSSEC{}, err + } + + return sdk.DNSSEC{}, utils.ErrorPerStatusCodeV4(errBody, httpResp, err) + } + + return resp.GetData(), nil +} diff --git a/pkg/cmd/describe/describe.go b/pkg/cmd/describe/describe.go index 472ff1b2..612c2dfe 100644 --- a/pkg/cmd/describe/describe.go +++ b/pkg/cmd/describe/describe.go @@ -12,6 +12,7 @@ import ( digitalcertificate "github.com/aziontech/azion-cli/pkg/cmd/describe/digital_certificate" dnsRecord "github.com/aziontech/azion-cli/pkg/cmd/describe/dns_record" dnsZone "github.com/aziontech/azion-cli/pkg/cmd/describe/dns_zone" + dnssec "github.com/aziontech/azion-cli/pkg/cmd/describe/dnssec" firewall "github.com/aziontech/azion-cli/pkg/cmd/describe/firewall" firewallinstance "github.com/aziontech/azion-cli/pkg/cmd/describe/firewall_instance" firewallrules "github.com/aziontech/azion-cli/pkg/cmd/describe/firewall_rules" @@ -58,6 +59,7 @@ func NewCmd(f *cmdutil.Factory) *cobra.Command { cmd.AddCommand(deviceGroups.NewCmd(f)) cmd.AddCommand(dnsZone.NewCmd(f)) cmd.AddCommand(dnsRecord.NewCmd(f)) + cmd.AddCommand(dnssec.NewCmd(f)) cmd.AddCommand(function.NewCmd(f)) cmd.AddCommand(variables.NewCmd(f)) cmd.AddCommand(edgeStorage.NewCmd(f)) diff --git a/pkg/cmd/describe/dnssec/dnssec.go b/pkg/cmd/describe/dnssec/dnssec.go new file mode 100644 index 00000000..8efb3be9 --- /dev/null +++ b/pkg/cmd/describe/dnssec/dnssec.go @@ -0,0 +1,104 @@ +package dnssec + +import ( + "context" + "fmt" + "path/filepath" + "strconv" + + "github.com/MakeNowJust/heredoc" + "go.uber.org/zap" + + msg "github.com/aziontech/azion-cli/messages/dnssec" + api "github.com/aziontech/azion-cli/pkg/api/dnssec" + "github.com/aziontech/azion-cli/pkg/cmdutil" + "github.com/aziontech/azion-cli/pkg/contracts" + "github.com/aziontech/azion-cli/pkg/iostreams" + "github.com/aziontech/azion-cli/pkg/logger" + "github.com/aziontech/azion-cli/pkg/output" + "github.com/aziontech/azion-cli/utils" + sdk "github.com/aziontech/azionapi-v4-go-sdk-dev/azion-api" + "github.com/spf13/cobra" +) + +var zoneID int64 + +type DescribeCmd struct { + Io *iostreams.IOStreams + AskInput func(string) (string, error) + Get func(context.Context, int64) (sdk.DNSSEC, error) +} + +func NewDescribeCmd(f *cmdutil.Factory) *DescribeCmd { + return &DescribeCmd{ + Io: f.IOStreams, + AskInput: func(prompt string) (string, error) { + return utils.AskInput(prompt) + }, + Get: func(ctx context.Context, id int64) (sdk.DNSSEC, error) { + client := api.NewClient(f.HttpClient, f.Config.GetString("api_v4_url"), f.Config.GetString("token")) + return client.Get(ctx, id) + }, + } +} + +func NewCobraCmd(describe *DescribeCmd, f *cmdutil.Factory) *cobra.Command { + opts := &contracts.DescribeOptions{} + cobraCmd := &cobra.Command{ + Use: msg.DNSSECUsage, + Short: msg.DNSSECDescribeShortDescription, + Long: msg.DNSSECDescribeLongDescription, + SilenceUsage: true, + SilenceErrors: true, + Example: heredoc.Doc(` + $ azion describe dnssec --zone-id 107313 + $ azion describe dnssec --zone-id 107313 --format json + $ azion describe dnssec --zone-id 107313 --out "./tmp/test.json" + `), + RunE: func(cmd *cobra.Command, args []string) error { + if !cmd.Flags().Changed("zone-id") { + answer, err := describe.AskInput(msg.DNSSECDescribeAskInputZoneID) + if err != nil { + return err + } + + num, err := strconv.ParseInt(answer, 10, 64) + if err != nil { + logger.Debug("Error while converting answer to int64", zap.Error(err)) + return msg.ErrorConvertIdZone + } + + zoneID = num + } + + ctx := context.Background() + resp, err := describe.Get(ctx, zoneID) + if err != nil { + return fmt.Errorf(msg.ErrorGetDNSSEC.Error(), err) + } + + fields := make(map[string]string, 0) + fields["Enabled"] = "Enabled" + fields["Status"] = "Status" + + describeOut := output.DescribeOutput{ + GeneralOutput: output.GeneralOutput{ + Out: f.IOStreams.Out, + Msg: filepath.Clean(opts.OutPath), + Flags: f.Flags, + }, + Fields: fields, + Values: &resp, + } + return output.Print(&describeOut) + }, + } + + cobraCmd.Flags().Int64Var(&zoneID, "zone-id", 0, msg.DNSSECFlagZoneID) + cobraCmd.Flags().BoolP("help", "h", false, msg.DNSSECDescribeHelpFlag) + return cobraCmd +} + +func NewCmd(f *cmdutil.Factory) *cobra.Command { + return NewCobraCmd(NewDescribeCmd(f), f) +} diff --git a/pkg/cmd/describe/dnssec/dnssec_test.go b/pkg/cmd/describe/dnssec/dnssec_test.go new file mode 100644 index 00000000..07c9262d --- /dev/null +++ b/pkg/cmd/describe/dnssec/dnssec_test.go @@ -0,0 +1,71 @@ +package dnssec + +import ( + "net/http" + "testing" + + "github.com/aziontech/azion-cli/pkg/httpmock" + "github.com/aziontech/azion-cli/pkg/logger" + "github.com/aziontech/azion-cli/pkg/testutils" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zapcore" +) + +func TestDescribe(t *testing.T) { + logger.New(zapcore.DebugLevel) + + tests := []struct { + name string + request httpmock.Matcher + response httpmock.Responder + args []string + expectErr bool + mockInput func(string) (string, error) + }{ + { + name: "describe DNSSEC", + request: httpmock.REST("GET", "workspace/dns/zones/1337/dnssec"), + response: httpmock.JSONFromFile("./fixtures/response.json"), + args: []string{"--zone-id", "1337"}, + expectErr: false, + }, + { + name: "describe DNSSEC - no zone id, ask input", + request: httpmock.REST("GET", "workspace/dns/zones/1337/dnssec"), + response: httpmock.JSONFromFile("./fixtures/response.json"), + expectErr: false, + mockInput: func(s string) (string, error) { + return "1337", nil + }, + }, + { + name: "not found", + request: httpmock.REST("GET", "workspace/dns/zones/1234/dnssec"), + response: httpmock.StatusStringResponse(http.StatusNotFound, "Not Found"), + args: []string{"--zone-id", "1234"}, + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mock := &httpmock.Registry{} + mock.Register(tt.request, tt.response) + + f, _, _ := testutils.NewFactory(mock) + descCmd := NewDescribeCmd(f) + if tt.mockInput != nil { + descCmd.AskInput = tt.mockInput + } + cmd := NewCobraCmd(descCmd, f) + cmd.SetArgs(tt.args) + + err := cmd.Execute() + if tt.expectErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/pkg/cmd/describe/dnssec/fixtures/response.json b/pkg/cmd/describe/dnssec/fixtures/response.json new file mode 100644 index 00000000..14470e89 --- /dev/null +++ b/pkg/cmd/describe/dnssec/fixtures/response.json @@ -0,0 +1,19 @@ +{ + "state": "executed", + "data": { + "enabled": true, + "status": "active", + "delegation_signer": { + "algorithm_type": { + "id": 13, + "slug": "ecdsap256sha256" + }, + "digest": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "digest_type": { + "id": 2, + "slug": "sha256" + }, + "key_tag": 12345 + } + } +} diff --git a/pkg/cmd/update/dnssec/dnssec.go b/pkg/cmd/update/dnssec/dnssec.go new file mode 100644 index 00000000..dcec3a7e --- /dev/null +++ b/pkg/cmd/update/dnssec/dnssec.go @@ -0,0 +1,113 @@ +package dnssec + +import ( + "context" + "fmt" + "strconv" + + "github.com/MakeNowJust/heredoc" + "go.uber.org/zap" + + msg "github.com/aziontech/azion-cli/messages/dnssec" + api "github.com/aziontech/azion-cli/pkg/api/dnssec" + "github.com/aziontech/azion-cli/pkg/cmdutil" + "github.com/aziontech/azion-cli/pkg/logger" + "github.com/aziontech/azion-cli/pkg/output" + "github.com/aziontech/azion-cli/utils" + sdk "github.com/aziontech/azionapi-v4-go-sdk-dev/azion-api" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +type Fields struct { + ZoneID int64 + Enabled bool + Path string +} + +func NewCmd(f *cmdutil.Factory) *cobra.Command { + fields := &Fields{} + + cmd := &cobra.Command{ + Use: msg.DNSSECUsage, + Short: msg.DNSSECUpdateShortDescription, + Long: msg.DNSSECUpdateLongDescription, + SilenceUsage: true, + SilenceErrors: true, + Example: heredoc.Doc(` + $ azion update dnssec --zone-id 12312 --enabled true + $ azion update dnssec --zone-id 12312 --enabled false + $ azion update dnssec --zone-id 12312 --file "update.json" + `), + RunE: func(cmd *cobra.Command, args []string) error { + client := api.NewClient(f.HttpClient, f.Config.GetString("api_v4_url"), f.Config.GetString("token")) + + request := sdk.PatchedDNSSECRequest{} + + if !cmd.Flags().Changed("zone-id") { + answers, err := utils.AskInput(msg.DNSSECUpdateAskInputZoneID) + if err != nil { + logger.Debug("Error while parsing answer", zap.Error(err)) + return utils.ErrorParseResponse + } + + id, err := strconv.Atoi(answers) + if err != nil { + logger.Debug("Error while parsing string to integer", zap.Error(err)) + return msg.ErrorConvertIdZone + } + + fields.ZoneID = int64(id) + } + + if cmd.Flags().Changed("file") { + err := utils.FlagFileUnmarshalJSON(fields.Path, &request) + if err != nil { + logger.Debug("Error while parsing <"+fields.Path+"> file", zap.Error(err)) + return utils.ErrorUnmarshalReader + } + } else { + if !cmd.Flags().Changed("enabled") { + answer, err := utils.AskInput(msg.DNSSECUpdateAskInputEnabled) + if err != nil { + logger.Debug("Error while parsing answer", zap.Error(err)) + return utils.ErrorParseResponse + } + + enabled, err := strconv.ParseBool(answer) + if err != nil { + logger.Debug("Error while parsing string to boolean", zap.Error(err)) + return msg.ErrorConvertEnabled + } + + fields.Enabled = enabled + } + + request.SetEnabled(fields.Enabled) + } + + _, err := client.Update(context.Background(), request, fields.ZoneID) + if err != nil { + return fmt.Errorf(msg.ErrorUpdateDNSSEC.Error(), err) + } + + updateOut := output.GeneralOutput{ + Msg: fmt.Sprintf(msg.DNSSECUpdateOutputSuccess, fields.ZoneID), + Out: f.IOStreams.Out, + Flags: f.Flags, + } + return output.Print(&updateOut) + }, + } + + flags := cmd.Flags() + addFlags(flags, fields) + return cmd +} + +func addFlags(flags *pflag.FlagSet, fields *Fields) { + flags.Int64Var(&fields.ZoneID, "zone-id", 0, msg.DNSSECFlagZoneID) + flags.BoolVar(&fields.Enabled, "enabled", false, msg.DNSSECUpdateFlagEnabled) + flags.StringVar(&fields.Path, "file", "", msg.DNSSECUpdateFlagIn) + flags.BoolP("help", "h", false, msg.DNSSECUpdateHelpFlag) +} diff --git a/pkg/cmd/update/dnssec/dnssec_test.go b/pkg/cmd/update/dnssec/dnssec_test.go new file mode 100644 index 00000000..abff53b4 --- /dev/null +++ b/pkg/cmd/update/dnssec/dnssec_test.go @@ -0,0 +1,86 @@ +package dnssec + +import ( + "fmt" + "net/http" + "testing" + + "github.com/aziontech/azion-cli/pkg/logger" + "go.uber.org/zap/zapcore" + + msg "github.com/aziontech/azion-cli/messages/dnssec" + "github.com/aziontech/azion-cli/pkg/httpmock" + "github.com/aziontech/azion-cli/pkg/testutils" + "github.com/stretchr/testify/require" +) + +func TestUpdate(t *testing.T) { + logger.New(zapcore.DebugLevel) + + t.Run("enable DNSSEC", func(t *testing.T) { + mock := &httpmock.Registry{} + mock.Register( + httpmock.REST("PATCH", "workspace/dns/zones/1337/dnssec"), + httpmock.JSONFromFile("./fixtures/response.json"), + ) + + f, stdout, _ := testutils.NewFactory(mock) + cmd := NewCmd(f) + cmd.SetArgs([]string{"--zone-id", "1337", "--enabled", "true"}) + + err := cmd.Execute() + + require.NoError(t, err) + require.Equal(t, fmt.Sprintf(msg.DNSSECUpdateOutputSuccess, 1337), stdout.String()) + }) + + t.Run("disable DNSSEC", func(t *testing.T) { + mock := &httpmock.Registry{} + mock.Register( + httpmock.REST("PATCH", "workspace/dns/zones/1337/dnssec"), + httpmock.JSONFromFile("./fixtures/response.json"), + ) + + f, stdout, _ := testutils.NewFactory(mock) + cmd := NewCmd(f) + cmd.SetArgs([]string{"--zone-id", "1337", "--enabled", "false"}) + + err := cmd.Execute() + + require.NoError(t, err) + require.Equal(t, fmt.Sprintf(msg.DNSSECUpdateOutputSuccess, 1337), stdout.String()) + }) + + t.Run("update with file", func(t *testing.T) { + mock := &httpmock.Registry{} + mock.Register( + httpmock.REST("PATCH", "workspace/dns/zones/1337/dnssec"), + httpmock.JSONFromFile("./fixtures/response.json"), + ) + + f, stdout, _ := testutils.NewFactory(mock) + cmd := NewCmd(f) + cmd.SetArgs([]string{"--zone-id", "1337", "--file", "./fixtures/update.json"}) + + err := cmd.Execute() + + require.NoError(t, err) + require.Equal(t, fmt.Sprintf(msg.DNSSECUpdateOutputSuccess, 1337), stdout.String()) + }) + + t.Run("not found", func(t *testing.T) { + mock := &httpmock.Registry{} + mock.Register( + httpmock.REST("PATCH", "workspace/dns/zones/1234/dnssec"), + httpmock.StatusStringResponse(http.StatusNotFound, "Not Found"), + ) + + f, _, _ := testutils.NewFactory(mock) + cmd := NewCmd(f) + cmd.SetArgs([]string{"--zone-id", "1234", "--enabled", "true"}) + + err := cmd.Execute() + + require.Error(t, err) + }) +} diff --git a/pkg/cmd/update/dnssec/fixtures/response.json b/pkg/cmd/update/dnssec/fixtures/response.json new file mode 100644 index 00000000..14470e89 --- /dev/null +++ b/pkg/cmd/update/dnssec/fixtures/response.json @@ -0,0 +1,19 @@ +{ + "state": "executed", + "data": { + "enabled": true, + "status": "active", + "delegation_signer": { + "algorithm_type": { + "id": 13, + "slug": "ecdsap256sha256" + }, + "digest": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "digest_type": { + "id": 2, + "slug": "sha256" + }, + "key_tag": 12345 + } + } +} diff --git a/pkg/cmd/update/dnssec/fixtures/update.json b/pkg/cmd/update/dnssec/fixtures/update.json new file mode 100644 index 00000000..4e609c71 --- /dev/null +++ b/pkg/cmd/update/dnssec/fixtures/update.json @@ -0,0 +1,3 @@ +{ + "enabled": true +} diff --git a/pkg/cmd/update/update.go b/pkg/cmd/update/update.go index f5b6b814..cb31c47e 100644 --- a/pkg/cmd/update/update.go +++ b/pkg/cmd/update/update.go @@ -11,6 +11,7 @@ import ( digitalcertificate "github.com/aziontech/azion-cli/pkg/cmd/update/digital_certificate" dnsRecord "github.com/aziontech/azion-cli/pkg/cmd/update/dns_record" dnsZone "github.com/aziontech/azion-cli/pkg/cmd/update/dns_zone" + dnssec "github.com/aziontech/azion-cli/pkg/cmd/update/dnssec" firewall "github.com/aziontech/azion-cli/pkg/cmd/update/firewall" firewallInstance "github.com/aziontech/azion-cli/pkg/cmd/update/firewall_instance" firewallRuleOrder "github.com/aziontech/azion-cli/pkg/cmd/update/firewall_rule_order" @@ -57,6 +58,7 @@ func NewCmd(f *cmdutil.Factory) *cobra.Command { cmd.AddCommand(deviceGroups.NewCmd(f)) cmd.AddCommand(dnsZone.NewCmd(f)) cmd.AddCommand(dnsRecord.NewCmd(f)) + cmd.AddCommand(dnssec.NewCmd(f)) cmd.AddCommand(variables.NewCmd(f)) cmd.AddCommand(storage.NewCmd(f)) cmd.AddCommand(workloads.NewCmd(f))