diff --git a/api/resource_dns.go b/api/resource_dns.go new file mode 100644 index 0000000000..5b5a2b2d28 --- /dev/null +++ b/api/resource_dns.go @@ -0,0 +1,208 @@ +package api + +func (c *Client) GetDNSZones(slug string) ([]*DNSZone, error) { + query := ` + query($slug: String!) { + organization(slug: $slug) { + dnsZones { + nodes { + id + domain + createdAt + } + } + } + } + ` + + req := c.NewRequest(query) + + req.Var("slug", slug) + + data, err := c.Run(req) + if err != nil { + return nil, err + } + + return *data.Organization.DNSZones.Nodes, nil +} + +func (c *Client) FindDNSZone(organizationSlug string, domain string) (*DNSZone, error) { + query := ` + query($slug: String!, $domain: String!) { + organization(slug: $slug) { + dnsZone(domain: $domain) { + id + domain + createdAt + organization { + id + slug + name + } + } + } + } + ` + + req := c.NewRequest(query) + + req.Var("slug", organizationSlug) + req.Var("domain", domain) + + data, err := c.Run(req) + if err != nil { + return nil, err + } + + if data.Organization == nil || data.Organization.DNSZone == nil { + return nil, ErrNotFound + } + + return data.Organization.DNSZone, nil +} + +func (c *Client) CreateDNSZone(organizationID string, domain string) (*DNSZone, error) { + query := ` + mutation($input: CreateDnsZoneInput!) { + createDnsZone(input: $input) { + zone { + id + domain + createdAt + } + } + } + ` + + req := c.NewRequest(query) + + req.Var("input", map[string]interface{}{ + "organizationId": organizationID, + "domain": domain, + }) + + data, err := c.Run(req) + if err != nil { + return nil, err + } + + return data.CreateDnsZone.Zone, nil +} + +func (c *Client) DeleteDNSZone(zoneID string) error { + query := ` + mutation($input: DeleteDnsZoneInput!) { + deleteDnsZone(input: $input) { + clientMutationId + } + } + ` + + req := c.NewRequest(query) + + req.Var("input", map[string]interface{}{ + "dnsZoneId": zoneID, + }) + + _, err := c.Run(req) + if err != nil { + return err + } + + return nil +} + +func (c *Client) GetDNSRecords(zoneID string) ([]*DNSRecord, error) { + query := ` + query($zoneId: ID!) { + dnsZone: node(id: $zoneId) { + ... on DnsZone { + records { + nodes { + id + fqdn + name + type + ttl + values + isApex + isWildcard + isSystem + createdAt + updatedAt + } + } + } + } + } + ` + + req := c.NewRequest(query) + + req.Var("zoneId", zoneID) + + data, err := c.Run(req) + if err != nil { + return nil, err + } + + if data.DNSZone == nil { + return nil, ErrNotFound + } + + return *data.DNSZone.Records.Nodes, nil +} + +func (c *Client) ExportDNSRecords(zoneID string) (string, error) { + query := ` + mutation($input: ExportDnsZoneInput!) { + exportDnsZone(input: $input) { + contents + } + } + ` + + req := c.NewRequest(query) + + req.Var("input", map[string]interface{}{ + "dnsZoneId": zoneID, + }) + + data, err := c.Run(req) + if err != nil { + return "", err + } + + return data.ExportDnsZone.Contents, nil +} + +func (c *Client) ImportDNSRecords(zoneID string, zonefile string) ([]ImportDnsRecordTypeResult, error) { + query := ` + mutation($input: ImportDnsZoneInput!) { + importDnsZone(input: $input) { + results { + created + deleted + updated + skipped + type + } + } + } + ` + + req := c.NewRequest(query) + + req.Var("input", map[string]interface{}{ + "dnsZoneId": zoneID, + "zonefile": zonefile, + }) + + data, err := c.Run(req) + if err != nil { + return nil, err + } + + return data.ImportDnsZone.Results, nil +} diff --git a/api/resource_organizations.go b/api/resource_organizations.go index 73dd189323..4b28c913a4 100644 --- a/api/resource_organizations.go +++ b/api/resource_organizations.go @@ -24,6 +24,30 @@ func (client *Client) GetOrganizations() ([]Organization, error) { return data.Organizations.Nodes, nil } +func (client *Client) FindOrganizationBySlug(slug string) (*Organization, error) { + q := ` + query($slug: String!) { + organization(slug: $slug) { + id + slug + name + type + } + } + ` + + req := client.NewRequest(q) + + req.Var("slug", slug) + + data, err := client.Run(req) + if err != nil { + return nil, err + } + + return data.Organization, nil +} + func (client *Client) GetCurrentOrganizations() (Organization, []Organization, error) { query := ` query { diff --git a/api/types.go b/api/types.go index 47579d458c..995cac9b71 100644 --- a/api/types.go +++ b/api/types.go @@ -19,15 +19,23 @@ type Query struct { Organizations struct { Nodes []Organization } + + Organization *Organization UserOrganizations UserOrganizations OrganizationDetails OrganizationDetails Build Build + Node interface{} + Nodes []interface{} + Platform struct { Regions []Region VMSizes []VMSize } + // hack to let us alias node to a type + DNSZone *DNSZone + // mutations CreateApp struct { App App @@ -108,6 +116,17 @@ type Query struct { App App } + CreateDnsZone struct { + Zone *DNSZone + } + + ExportDnsZone struct { + Contents string + } + + ImportDnsZone struct { + Results []ImportDnsRecordTypeResult + } CreateOrganization CreateOrganizationPayload DeleteOrganization DeleteOrganizationPayload } @@ -208,6 +227,16 @@ type Organization struct { Name string Slug string Type string + + DNSZones struct { + Nodes *[]*DNSZone + Edges *[]*struct { + Cursor *string + Node *DNSZone + } + } + + DNSZone *DNSZone } type OrganizationDetails struct { @@ -635,3 +664,35 @@ type Extensions struct { Query string Variables map[string]string } + +type DNSZone struct { + ID string + Domain string + CreatedAt time.Time + Organization *Organization + Records *struct { + Nodes *[]*DNSRecord + } +} + +type DNSRecord struct { + ID string + Name string + FQDN string + IsApex bool + IsWildcard bool + IsSystem bool + TTL int + Type string + Values []string + CreatedAt time.Time + UpdatedAt time.Time +} + +type ImportDnsRecordTypeResult struct { + Created int + Deleted int + Skipped int + Updated int + Type string +} diff --git a/cmd/dns.go b/cmd/dns.go new file mode 100644 index 0000000000..21f773a424 --- /dev/null +++ b/cmd/dns.go @@ -0,0 +1,180 @@ +package cmd + +import ( + "fmt" + "io/ioutil" + "os" + "strings" + + "github.com/spf13/cobra" + "github.com/superfly/flyctl/cmdctx" + "github.com/superfly/flyctl/docstrings" +) + +func newDnsCommand() *Command { + dnsStrings := docstrings.Get("dns") + cmd := &Command{ + Command: &cobra.Command{ + Use: dnsStrings.Usage, + Short: dnsStrings.Short, + Long: dnsStrings.Long, + }, + } + + zonesStrings := docstrings.Get("dns.zones") + zonesCmd := &Command{ + Command: &cobra.Command{ + Use: zonesStrings.Usage, + Short: zonesStrings.Short, + Long: zonesStrings.Long, + }, + } + cmd.AddCommand(zonesCmd) + + zonesListStrings := docstrings.Get("dns.zones.list") + zonesListCmd := BuildCommand(zonesCmd, runZonesList, zonesListStrings.Usage, zonesListStrings.Short, zonesListStrings.Long, os.Stdout, requireSession) + zonesListCmd.Args = cobra.ExactArgs(1) + + zonesCreateStrings := docstrings.Get("dns.zones.create") + zonesCreateCmd := BuildCommand(zonesCmd, runZonesCreate, zonesCreateStrings.Usage, zonesCreateStrings.Short, zonesCreateStrings.Long, os.Stdout, requireSession) + zonesCreateCmd.Args = cobra.ExactArgs(2) + + zonesDeleteStrings := docstrings.Get("dns.zones.delete") + zonesDeleteCmd := BuildCommand(zonesCmd, runZonesDelete, zonesDeleteStrings.Usage, zonesDeleteStrings.Short, zonesDeleteStrings.Long, os.Stdout, requireSession) + zonesDeleteCmd.Args = cobra.ExactArgs(2) + + recordsStrings := docstrings.Get("dns.records") + recordsCmd := &Command{ + Command: &cobra.Command{ + Use: recordsStrings.Usage, + Short: recordsStrings.Short, + Long: recordsStrings.Long, + }, + } + cmd.AddCommand(recordsCmd) + + recordsListStrings := docstrings.Get("dns.records.list") + recordsListCmd := BuildCommand(recordsCmd, runRecordsList, recordsListStrings.Usage, recordsListStrings.Short, recordsListStrings.Long, os.Stdout, requireSession) + recordsListCmd.Args = cobra.ExactArgs(2) + + recordsExportStrings := docstrings.Get("dns.records.export") + recordsExportCmd := BuildCommand(recordsCmd, runRecordsExport, recordsExportStrings.Usage, recordsExportStrings.Short, recordsExportStrings.Long, os.Stdout, requireSession) + recordsExportCmd.Args = cobra.ExactArgs(2) + + recordsImportStrings := docstrings.Get("dns.records.import") + recordsImportCmd := BuildCommand(recordsCmd, runRecordsImport, recordsImportStrings.Usage, recordsImportStrings.Short, recordsImportStrings.Long, os.Stdout, requireSession) + recordsImportCmd.Args = cobra.ExactArgs(3) + + return cmd +} + +func runZonesList(ctx *cmdctx.CmdContext) error { + orgSlug := ctx.Args[0] + zones, err := ctx.Client.API().GetDNSZones(orgSlug) + if err != nil { + return err + } + + for _, zone := range zones { + fmt.Println(zone.ID, zone.Domain, zone.CreatedAt) + } + return nil +} + +func runZonesCreate(ctx *cmdctx.CmdContext) error { + org, err := ctx.Client.API().FindOrganizationBySlug(ctx.Args[0]) + if err != nil { + return err + } + domain := ctx.Args[1] + + fmt.Printf("Creating zone %s in organization %s\n", domain, org.Slug) + + zone, err := ctx.Client.API().CreateDNSZone(org.ID, domain) + if err != nil { + return err + } + + fmt.Println("Created zone", zone.Domain) + + return nil +} + +func runZonesDelete(ctx *cmdctx.CmdContext) error { + zone, err := ctx.Client.API().FindDNSZone(ctx.Args[0], ctx.Args[1]) + if err != nil { + return err + } + + fmt.Printf("Deleting zone %s in organization %s\n", zone.Domain, zone.Organization.Slug) + + err = ctx.Client.API().DeleteDNSZone(zone.ID) + if err != nil { + return err + } + + fmt.Println("Deleted zone", zone.Domain) + + return nil +} + +func runRecordsList(ctx *cmdctx.CmdContext) error { + zone, err := ctx.Client.API().FindDNSZone(ctx.Args[0], ctx.Args[1]) + if err != nil { + return err + } + + fmt.Printf("Records for zone %s in organization %s\n", zone.Domain, zone.Organization.Slug) + + records, err := ctx.Client.API().GetDNSRecords(zone.ID) + if err != nil { + return err + } + + for _, record := range records { + fmt.Println(record.FQDN, record.TTL, record.Type, strings.Join(record.Values, ",")) + } + + return nil +} + +func runRecordsExport(ctx *cmdctx.CmdContext) error { + zone, err := ctx.Client.API().FindDNSZone(ctx.Args[0], ctx.Args[1]) + if err != nil { + return err + } + + records, err := ctx.Client.API().ExportDNSRecords(zone.ID) + if err != nil { + return err + } + + fmt.Println(records) + + return nil +} + +func runRecordsImport(ctx *cmdctx.CmdContext) error { + zone, err := ctx.Client.API().FindDNSZone(ctx.Args[0], ctx.Args[1]) + if err != nil { + return err + } + + data, err := ioutil.ReadFile(ctx.Args[2]) + if err != nil { + return err + } + + results, err := ctx.Client.API().ImportDNSRecords(zone.ID, string(data)) + if err != nil { + return err + } + + fmt.Println("zonefile imported") + + for _, result := range results { + fmt.Printf("%s created: %d, updated: %d, deleted: %d, skipped: %d\n", result.Type, result.Created, result.Updated, result.Deleted, result.Skipped) + } + + return nil +} diff --git a/cmd/root.go b/cmd/root.go index 5d2f71d1a6..dc298c3af8 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -91,6 +91,7 @@ func init() { newStatusCommand(), newSuspendCommand(), newVersionCommand(), + newDnsCommand(), newOrgsCommand(), ) diff --git a/helpgen/flyctlhelp.toml b/helpgen/flyctlhelp.toml index 4a1f60fb9c..5f253e58a4 100644 --- a/helpgen/flyctlhelp.toml +++ b/helpgen/flyctlhelp.toml @@ -298,6 +298,51 @@ than monitoring the deployment progress. Use flyctl monitor to restart monitoring deployment progress """ +[dns] +usage="dns" +shortHelp="Manage DNS" +longHelp="Manage DNS" + +[dns.zones] +usage="zones" +shortHelp="Manage DNS zones" +longHelp="Manage DNS zones" + +[dns.zones.list] +usage="list " +shortHelp="List DNS zones" +longHelp="List DNS zones" + +[dns.zones.create] +usage="create " +shortHelp="Create a DNS zone" +longHelp="Create a DNS zone" + +[dns.zones.delete] +usage="delete " +shortHelp="Delete a DNS zone" +longHelp="Delete a DNS zone" + +[dns.records] +usage="records" +shortHelp="Manage DNS records" +longHelp="Manage DNS records" + +[dns.records.list] +usage="list " +shortHelp="List DNS records" +longHelp="List DNS records" + +[dns.records.export] +usage="export " +shortHelp="Export DNS records" +longHelp="Export DNS records" + +[dns.records.import] +usage="import " +shortHelp="Import DNS records" +longHelp="Import DNS records" + [docs] usage="docs" shortHelp="View Fly documentation"