From 707ffa2536d0d366a9a0e7d52152f9e3e60997ea Mon Sep 17 00:00:00 2001 From: Christoph Herzog Date: Tue, 12 Mar 2024 18:54:20 +0100 Subject: [PATCH 1/7] feat(backend-api): Add DnsRecord types and queries --- lib/backend-api/README.md | 2 +- lib/backend-api/schema.graphql | 717 +++++++++++++++++++++++++++------ lib/backend-api/src/query.rs | 66 +++ lib/backend-api/src/types.rs | 360 +++++++++++++++++ 4 files changed, 1026 insertions(+), 119 deletions(-) diff --git a/lib/backend-api/README.md b/lib/backend-api/README.md index 7aa1360c914..a5c283914c2 100644 --- a/lib/backend-api/README.md +++ b/lib/backend-api/README.md @@ -42,5 +42,5 @@ This is not always sensible though, depending on which nested data you want to fetch. [cynic-api-docs]: https://docs.rs/cynic/latest/cynic/ -[cynic-web-ui]: https://docs.rs/cynic/latest/cynic/ +[cynic-web-ui]: https://generator.cynic-rs.dev/ [cynic-website]: https://cynic-rs.dev diff --git a/lib/backend-api/schema.graphql b/lib/backend-api/schema.graphql index c8db4f698ea..ddfc8824f13 100644 --- a/lib/backend-api/schema.graphql +++ b/lib/backend-api/schema.graphql @@ -1,3 +1,13 @@ +""" +Directs the executor to include this field or fragment only when the user is not logged in. +""" +directive @includeIfLoggedIn on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + +""" +Directs the executor to skip this field or fragment when the user is not logged in. +""" +directive @skipIfLoggedIn on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + interface Node { """The ID of the object""" id: ID! @@ -47,6 +57,7 @@ type User implements Node & PackageOwner & Owner { packages(collaborating: Boolean = false, offset: Int, before: String, after: String, first: Int, last: Int): PackageConnection! apps(collaborating: Boolean = false, sortBy: DeployAppsSortBy, offset: Int, before: String, after: String, first: Int, last: Int): DeployAppConnection! usageMetrics(forRange: MetricRange!, variant: MetricType!): [UsageMetric]! + domains(offset: Int, before: String, after: String, first: Int, last: Int): DNSDomainConnection! isStaff: Boolean packageVersions(offset: Int, before: String, after: String, first: Int, last: Int): PackageVersionConnection! packageTransfersIncoming(offset: Int, before: String, after: String, first: Int, last: Int): PackageTransferRequestConnection! @@ -199,6 +210,7 @@ type Namespace implements Node & PackageOwner & Owner { publicActivity(before: String, after: String, first: Int, last: Int): ActivityEventConnection! pendingInvites(offset: Int, before: String, after: String, first: Int, last: Int): NamespaceCollaboratorInviteConnection! viewerHasRole(role: GrapheneRole!): Boolean! + viewerAsCollaborator(role: GrapheneRole): NamespaceCollaborator """Whether the current user is invited to the namespace""" viewerIsInvited: Boolean! @@ -207,6 +219,7 @@ type Namespace implements Node & PackageOwner & Owner { viewerInvitation: NamespaceCollaboratorInvite packageTransfersIncoming(offset: Int, before: String, after: String, first: Int, last: Int): PackageTransferRequestConnection! usageMetrics(forRange: MetricRange!, variant: MetricType!): [UsageMetric]! + domains(offset: Int, before: String, after: String, first: Int, last: Int): DNSDomainConnection! } type NamespaceCollaboratorInviteConnection { @@ -248,6 +261,9 @@ type NamespaceCollaboratorInvite implements Node { } enum RegistryNamespaceMaintainerInviteRoleChoices { + """Owner""" + OWNER + """Admin""" ADMIN @@ -270,6 +286,9 @@ type NamespaceCollaborator implements Node { } enum RegistryNamespaceMaintainerRoleChoices { + """Owner""" + OWNER + """Admin""" ADMIN @@ -338,6 +357,8 @@ type Package implements Likeable & Node & PackageOwner { iconUpdatedAt: DateTime watchersCount: Int! webcs(offset: Int, before: String, after: String, first: Int, last: Int): WebcImageConnection! + + """List of app templates for this package""" appTemplates(offset: Int, before: String, after: String, first: Int, last: Int): AppTemplateConnection! packagewebcSet(offset: Int, before: String, after: String, first: Int, last: Int): PackageWebcConnection! versions: [PackageVersion]! @@ -366,6 +387,7 @@ type Package implements Likeable & Node & PackageOwner { collaborators(offset: Int, before: String, after: String, first: Int, last: Int): PackageCollaboratorConnection! pendingInvites(offset: Int, before: String, after: String, first: Int, last: Int): PackageCollaboratorInviteConnection! viewerHasRole(role: GrapheneRole!): Boolean! + viewerAsCollaborator(role: GrapheneRole): PackageCollaborator owner: PackageOwner! isTransferring: Boolean! activeTransferRequest: PackageTransferRequest @@ -446,6 +468,7 @@ type PackageVersion implements Node & PackageInstance { bindings: [PackageVersionLanguageBinding]! npmBindings: PackageVersionNPMBinding pythonBindings: PackageVersionPythonBinding + bindingsSet(before: String, after: String, first: Int, last: Int): PackageVersionBindingConnection hasBindings: Boolean! hasCommands: Boolean! showDeployButton: Boolean! @@ -599,6 +622,9 @@ type DeployAppVersion implements Node { """List of streams to fetch logs from. e.g. stdout, stderr.""" streams: [LogStream] + + """List of instance ids to fetch logs from.""" + instanceIds: [String] before: String after: String first: Int @@ -608,6 +634,8 @@ type DeployAppVersion implements Node { sourcePackageVersion: PackageVersion! aggregateMetrics: AggregateMetrics! volumes: [AppVersionVolume] + favicon: URL + screenshot: URL } type DeployApp implements Node & Owner { @@ -631,6 +659,8 @@ type DeployApp implements Node & Owner { aliases(offset: Int, before: String, after: String, first: Int, last: Int): AppAliasConnection! usageMetrics(forRange: MetricRange!, variant: MetricType!): [UsageMetric]! deleted: Boolean! + favicon: URL + screenshot: URL } enum DeployAppVersionsSortBy { @@ -719,8 +749,15 @@ enum MetricUnit { enum MetricRange { LAST_24_HOURS LAST_30_DAYS + LAST_1_HOUR } +""" +The `URL` scalar type represents a URL as text, represented as UTF-8 +character sequences. +""" +scalar URL + type LogConnection { """Pagination data for this connection.""" pageInfo: PageInfo! @@ -741,6 +778,7 @@ type LogEdge { enum LogStream { STDOUT STDERR + RUNTIME } type AppVersionVolume { @@ -1052,6 +1090,28 @@ type WEBCFilesystemItem { offset: Int! } +type PackageVersionBindingConnection { + """Pagination data for this connection.""" + pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [PackageVersionBindingEdge]! + + """Total number of items in the connection.""" + totalCount: Int +} + +"""A Relay edge containing a `PackageVersionBinding` and its cursor.""" +type PackageVersionBindingEdge { + """The item at the end of the edge""" + node: PackageVersionBinding + + """A cursor for use in pagination""" + cursor: String! +} + +union PackageVersionBinding = PackageVersionNPMBinding | PackageVersionPythonBinding + type WebcImageConnection { """Pagination data for this connection.""" pageInfo: PageInfo! @@ -1256,6 +1316,9 @@ type PackageCollaborator implements Node { } enum RegistryPackageMaintainerRoleChoices { + """Owner""" + OWNER + """Admin""" ADMIN @@ -1283,6 +1346,9 @@ type PackageCollaboratorInvite implements Node { } enum RegistryPackageMaintainerInviteRoleChoices { + """Owner""" + OWNER + """Admin""" ADMIN @@ -1314,6 +1380,7 @@ type PackageCollaboratorInviteEdge { } enum GrapheneRole { + OWNER ADMIN EDITOR VIEWER @@ -1419,6 +1486,311 @@ type PackageTransferRequestEdge { cursor: String! } +type DNSDomainConnection { + """Pagination data for this connection.""" + pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [DNSDomainEdge]! + + """Total number of items in the connection.""" + totalCount: Int +} + +"""A Relay edge containing a `DNSDomain` and its cursor.""" +type DNSDomainEdge { + """The item at the end of the edge""" + node: DNSDomain + + """A cursor for use in pagination""" + cursor: String! +} + +type DNSDomain implements Node { + name: String! + + """This zone will be accessible at /dns/{slug}/.""" + slug: String! + + """The ID of the object""" + id: ID! + records: [DNSRecord] + owner: Owner! +} + +union DNSRecord = ARecord | AAAARecord | CNAMERecord | TXTRecord | MXRecord | NSRecord | CAARecord | DNAMERecord | PTRRecord | SOARecord | SRVRecord | SSHFPRecord + +type ARecord implements Node & DNSRecordInterface { + createdAt: DateTime! + updatedAt: DateTime! + deletedAt: DateTime + address: String! + + """The ID of the object""" + id: ID! + name: String + ttl: Int + dnsClass: String + domain: DNSDomain! +} + +interface DNSRecordInterface { + name: String + ttl: Int + dnsClass: String + domain: DNSDomain! + createdAt: DateTime! + updatedAt: DateTime! + deletedAt: DateTime +} + +type AAAARecord implements Node & DNSRecordInterface { + createdAt: DateTime! + updatedAt: DateTime! + deletedAt: DateTime + address: String! + + """The ID of the object""" + id: ID! + name: String + ttl: Int + dnsClass: String + domain: DNSDomain! +} + +type CNAMERecord implements Node & DNSRecordInterface { + createdAt: DateTime! + updatedAt: DateTime! + deletedAt: DateTime + + """This domain name will alias to this canonical name.""" + cName: String! + + """The ID of the object""" + id: ID! + name: String + ttl: Int + dnsClass: String + domain: DNSDomain! +} + +type TXTRecord implements Node & DNSRecordInterface { + createdAt: DateTime! + updatedAt: DateTime! + deletedAt: DateTime + data: String! + + """The ID of the object""" + id: ID! + name: String + ttl: Int + dnsClass: String + domain: DNSDomain! +} + +type MXRecord implements Node & DNSRecordInterface { + createdAt: DateTime! + updatedAt: DateTime! + deletedAt: DateTime + preference: Int! + exchange: String! + + """The ID of the object""" + id: ID! + name: String + ttl: Int + dnsClass: String + domain: DNSDomain! +} + +type NSRecord implements Node & DNSRecordInterface { + createdAt: DateTime! + updatedAt: DateTime! + deletedAt: DateTime + nsdname: String! + + """The ID of the object""" + id: ID! + name: String + ttl: Int + dnsClass: String + domain: DNSDomain! +} + +type CAARecord implements Node & DNSRecordInterface { + createdAt: DateTime! + updatedAt: DateTime! + deletedAt: DateTime + flags: Int! + tag: DnsmanagerCertificationAuthorityAuthorizationRecordTagChoices! + value: String! + + """The ID of the object""" + id: ID! + name: String + ttl: Int + dnsClass: String + domain: DNSDomain! +} + +enum DnsmanagerCertificationAuthorityAuthorizationRecordTagChoices { + """issue""" + ISSUE + + """issue wildcard""" + ISSUEWILD + + """Incident object description exchange format""" + IODEF +} + +type DNAMERecord implements Node & DNSRecordInterface { + createdAt: DateTime! + updatedAt: DateTime! + deletedAt: DateTime + + """ + This domain name will alias to the entire subtree of that delegation domain. + """ + dName: String! + + """The ID of the object""" + id: ID! + name: String + ttl: Int + dnsClass: String + domain: DNSDomain! +} + +type PTRRecord implements Node & DNSRecordInterface { + createdAt: DateTime! + updatedAt: DateTime! + deletedAt: DateTime + ptrdname: String! + + """The ID of the object""" + id: ID! + name: String + ttl: Int + dnsClass: String + domain: DNSDomain! +} + +type SOARecord implements Node & DNSRecordInterface { + createdAt: DateTime! + updatedAt: DateTime! + deletedAt: DateTime + + """Primary master name server for this zone.""" + mname: String! + + """Email address of the administrator responsible for this zone.""" + rname: String! + + """ + A slave name server will initiate a zone transfer if this serial is incremented. + """ + serial: BigInt! + + """ + Number of seconds after which secondary name servers should query the master to detect zone changes. + """ + refresh: BigInt! + + """ + Number of seconds after which secondary name servers should retry to request the serial number from the master if the master does not respond. + """ + retry: BigInt! + + """ + Number of seconds after which secondary name servers should stop answering request for this zone if the master does not respond. + """ + expire: BigInt! + + """Time to live for purposes of negative caching.""" + minimum: BigInt! + + """The ID of the object""" + id: ID! + name: String + ttl: Int + dnsClass: String + domain: DNSDomain! +} + +type SRVRecord implements Node & DNSRecordInterface { + createdAt: DateTime! + updatedAt: DateTime! + deletedAt: DateTime + + """The symbolic name of the desired service.""" + service: String! + + """ + The transport protocol of the desired service, usually either TCP or UDP. + """ + protocol: String! + + """The priority of the target host, lower value means more preferred.""" + priority: Int! + + """ + A relative weight for records with the same priority, higher value means higher chance of getting picked. + """ + weight: Int! + port: Int! + + """ + The canonical hostname of the machine providing the service, ending in a dot. + """ + target: String! + + """The ID of the object""" + id: ID! + name: String + ttl: Int + dnsClass: String + domain: DNSDomain! +} + +type SSHFPRecord implements Node & DNSRecordInterface { + createdAt: DateTime! + updatedAt: DateTime! + deletedAt: DateTime + algorithm: DnsmanagerSshFingerprintRecordAlgorithmChoices! + type: DnsmanagerSshFingerprintRecordTypeChoices! + fingerprint: String! + + """The ID of the object""" + id: ID! + name: String + ttl: Int + dnsClass: String + domain: DNSDomain! +} + +enum DnsmanagerSshFingerprintRecordAlgorithmChoices { + """RSA""" + A_1 + + """DSA""" + A_2 + + """ECDSA""" + A_3 + + """Ed25519""" + A_4 +} + +enum DnsmanagerSshFingerprintRecordTypeChoices { + """SHA-1""" + A_1 + + """SHA-256""" + A_2 +} + type APITokenConnection { """Pagination data for this connection.""" pageInfo: PageInfo! @@ -1559,6 +1931,121 @@ type Signature { createdAt: DateTime! } +type StripeCustomer { + id: ID! +} + +type Billing { + stripeCustomer: StripeCustomer! + payments: [PaymentIntent]! + paymentMethods: [PaymentMethod]! +} + +type PaymentIntent implements Node { + """The datetime this object was created in stripe.""" + created: DateTime + + """Three-letter ISO currency code""" + currency: String! + + """ + Status of this PaymentIntent, one of requires_payment_method, requires_confirmation, requires_action, processing, requires_capture, canceled, or succeeded. You can read more about PaymentIntent statuses here. + """ + status: DjstripePaymentIntentStatusChoices! + + """The ID of the object""" + id: ID! + amount: String! +} + +enum DjstripePaymentIntentStatusChoices { + """ + Cancellation invalidates the intent for future confirmation and cannot be undone. + """ + CANCELED + + """Required actions have been handled.""" + PROCESSING + + """Payment Method require additional action, such as 3D secure.""" + REQUIRES_ACTION + + """Capture the funds on the cards which have been put on holds.""" + REQUIRES_CAPTURE + + """Intent is ready to be confirmed.""" + REQUIRES_CONFIRMATION + + """Intent created and requires a Payment Method to be attached.""" + REQUIRES_PAYMENT_METHOD + + """The funds are in your account.""" + SUCCEEDED +} + +union PaymentMethod = CardPaymentMethod + +type CardPaymentMethod implements Node { + """The ID of the object""" + id: ID! + brand: CardBrand! + country: String! + expMonth: Int! + expYear: Int! + funding: CardFunding! + last4: String! + isDefault: Boolean! +} + +""" +Card brand. + +Can be amex, diners, discover, jcb, mastercard, unionpay, visa, or unknown. +""" +enum CardBrand { + AMEX + DINERS + DISCOVER + JCB + MASTERCARD + UNIONPAY + VISA + UNKNOWN +} + +""" +Card funding type. + +Can be credit, debit, prepaid, or unknown. +""" +enum CardFunding { + CREDIT + DEBIT + PREPAID + UNKNOWN +} + +type Payment { + id: ID + amount: String + paidOn: DateTime +} + +"""Log entry for deploy app.""" +type Log { + """Timestamp in nanoseconds""" + timestamp: Float! + + """ISO 8601 string in UTC""" + datetime: DateTime! + + """Log message""" + message: String! + + """Log stream""" + stream: LogStream +} + type UserNotificationKindIncomingPackageTransfer { packageTransferRequest: PackageTransferRequest! } @@ -1687,131 +2174,19 @@ input WorkloadRunnerWasmSourceV1 { webc: WebcSourceV1! } -type StripeCustomer { - id: ID! -} - -type Billing { - stripeCustomer: StripeCustomer! - payments: [PaymentIntent]! - paymentMethods: [PaymentMethod]! -} - -type PaymentIntent implements Node { - """The datetime this object was created in stripe.""" - created: DateTime - - """Three-letter ISO currency code""" - currency: String! - - """ - Status of this PaymentIntent, one of requires_payment_method, requires_confirmation, requires_action, processing, requires_capture, canceled, or succeeded. You can read more about PaymentIntent statuses here. - """ - status: DjstripePaymentIntentStatusChoices! - - """The ID of the object""" - id: ID! - amount: String! -} - -enum DjstripePaymentIntentStatusChoices { - """ - Cancellation invalidates the intent for future confirmation and cannot be undone. - """ - CANCELED - - """Required actions have been handled.""" - PROCESSING - - """Payment Method require additional action, such as 3D secure.""" - REQUIRES_ACTION - - """Capture the funds on the cards which have been put on holds.""" - REQUIRES_CAPTURE - - """Intent is ready to be confirmed.""" - REQUIRES_CONFIRMATION - - """Intent created and requires a Payment Method to be attached.""" - REQUIRES_PAYMENT_METHOD - - """The funds are in your account.""" - SUCCEEDED -} - -union PaymentMethod = CardPaymentMethod - -type CardPaymentMethod implements Node { - """The ID of the object""" - id: ID! - brand: CardBrand! - country: String! - expMonth: Int! - expYear: Int! - funding: CardFunding! - last4: String! - isDefault: Boolean! -} - -""" -Card brand. - -Can be amex, diners, discover, jcb, mastercard, unionpay, visa, or unknown. -""" -enum CardBrand { - AMEX - DINERS - DISCOVER - JCB - MASTERCARD - UNIONPAY - VISA - UNKNOWN -} - -""" -Card funding type. - -Can be credit, debit, prepaid, or unknown. -""" -enum CardFunding { - CREDIT - DEBIT - PREPAID - UNKNOWN -} - -type Payment { - id: ID - amount: String - paidOn: DateTime -} - -"""Log entry for deploy app.""" -type Log { - """Timestamp in nanoseconds""" - timestamp: Float! - - """ISO 8601 string in UTC""" - datetime: DateTime! - - """Log message""" - message: String! - - """Log stream""" - stream: LogStream -} - type Query { latestTOS: TermsOfService! getDeployAppVersion(name: String!, owner: String, version: String): DeployAppVersion + getAllDomains(offset: Int, before: String, after: String, first: Int, last: Int): DNSDomainConnection! + getAllDNSRecords(sortBy: DNSRecordsSortBy, updatedAfter: DateTime, before: String, after: String, first: Int, last: Int): DNSRecordConnection! + getDomain(name: String!): DNSDomain getDeployApp(name: String!, owner: String): DeployApp getAppByGlobalAlias(alias: String!): DeployApp getDeployApps(sortBy: DeployAppsSortBy, updatedAfter: DateTime, offset: Int, before: String, after: String, first: Int, last: Int): DeployAppConnection! getAppVersions(sortBy: DeployAppVersionsSortBy, updatedAfter: DateTime, offset: Int, before: String, after: String, first: Int, last: Int): DeployAppVersionConnection! - getAppTemplates(categorySlug: String, offset: Int, before: String, after: String, first: Int, last: Int): AppTemplateConnection! - getAppTemplate(slug: String!): AppTemplate! - getAppTemplateCategories(offset: Int, before: String, after: String, first: Int, last: Int): AppTemplateCategoryConnection! + getAppTemplates(categorySlug: String, offset: Int, before: String, after: String, first: Int, last: Int): AppTemplateConnection + getAppTemplate(slug: String!): AppTemplate + getAppTemplateCategories(offset: Int, before: String, after: String, first: Int, last: Int): AppTemplateCategoryConnection viewer: User getUser(username: String!): User getPasswordResetToken(token: String!): GetPasswordResetToken @@ -1881,6 +2256,31 @@ type TermsOfService implements Node { viewerHasAccepted: Boolean! } +type DNSRecordConnection { + """Pagination data for this connection.""" + pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [DNSRecordEdge]! + + """Total number of items in the connection.""" + totalCount: Int +} + +"""A Relay edge containing a `DNSRecord` and its cursor.""" +type DNSRecordEdge { + """The item at the end of the edge""" + node: DNSRecord + + """A cursor for use in pagination""" + cursor: String! +} + +enum DNSRecordsSortBy { + NEWEST + OLDEST +} + type AppTemplateCategoryConnection { """Pagination data for this connection.""" pageInfo: PageInfo! @@ -2325,6 +2725,10 @@ type Mutation { acceptAppTransferRequest(input: AcceptAppTransferRequestInput!): AcceptAppTransferRequestPayload removeAppTransferRequest(input: RemoveAppTransferRequestInput!): RemoveAppTransferRequestPayload createRepoForAppTemplate(input: CreateRepoForAppTemplateInput!): CreateRepoForAppTemplatePayload + registerDomain(input: RegisterDomainInput!): RegisterDomainPayload + upsertDNSRecord(input: UpsertDNSRecordInput!): UpsertDNSRecordPayload + deleteDnsRecord(input: DeleteDNSRecordInput!): DeleteDNSRecordPayload + deleteDomain(input: DeleteDomainInput!): DeleteDomainPayload tokenAuth(input: ObtainJSONWebTokenInput!): ObtainJSONWebTokenPayload generateDeployToken(input: GenerateDeployTokenInput!): GenerateDeployTokenPayload verifyAccessToken(token: String): Verify @@ -2362,6 +2766,7 @@ type Mutation { watchPackage(input: WatchPackageInput!): WatchPackagePayload unwatchPackage(input: UnwatchPackageInput!): UnwatchPackagePayload archivePackage(input: ArchivePackageInput!): ArchivePackagePayload + renamePackage(input: RenamePackageInput!): RenamePackagePayload changePackageVersionArchivedStatus(input: ChangePackageVersionArchivedStatusInput!): ChangePackageVersionArchivedStatusPayload createNamespace(input: CreateNamespaceInput!): CreateNamespacePayload updateNamespace(input: UpdateNamespaceInput!): UpdateNamespacePayload @@ -2614,6 +3019,68 @@ input CreateRepoForAppTemplateInput { clientMutationId: String } +type RegisterDomainPayload { + success: Boolean! + domain: DNSDomain + clientMutationId: String +} + +input RegisterDomainInput { + name: String! + namespace: String! + clientMutationId: String +} + +type UpsertDNSRecordPayload { + success: Boolean! + clientMutationId: String +} + +input UpsertDNSRecordInput { + kind: RecordKind! + domainId: String! + name: String! + value: String! + ttl: Int + recordId: String + mx: DNSMXExtraInput + clientMutationId: String +} + +enum RecordKind { + A + AAAA + CNAME + MX + NS + TXT + DNAME +} + +input DNSMXExtraInput { + preference: Int! +} + +type DeleteDNSRecordPayload { + success: Boolean! + clientMutationId: String +} + +input DeleteDNSRecordInput { + recordId: ID! + clientMutationId: String +} + +type DeleteDomainPayload { + success: Boolean! + clientMutationId: String +} + +input DeleteDomainInput { + domainId: ID! + clientMutationId: String +} + type ObtainJSONWebTokenPayload { payload: GenericScalar! refreshExpiresIn: Int! @@ -3086,6 +3553,17 @@ input ArchivePackageInput { clientMutationId: String } +type RenamePackagePayload { + package: Package! + clientMutationId: String +} + +input RenamePackageInput { + packageId: ID! + newName: String! + clientMutationId: String +} + type ChangePackageVersionArchivedStatusPayload { packageVersion: PackageVersion! clientMutationId: String @@ -3367,6 +3845,9 @@ type Subscription { """Filter logs by stream""" streams: [LogStream] + """Filter logs by instance ids""" + instanceIds: [String] + """Search logs for this term""" searchTerm: String ): Log! diff --git a/lib/backend-api/src/query.rs b/lib/backend-api/src/query.rs index b4a1c5a5b27..59f3d3aff1e 100644 --- a/lib/backend-api/src/query.rs +++ b/lib/backend-api/src/query.rs @@ -816,6 +816,72 @@ pub async fn get_app_logs_paginated( }) } +/// Retrieve a domain by its name. +/// +/// Specify with_records to also retrieve all records for the domain. +pub async fn get_domain( + client: &WasmerClient, + domain: String, +) -> Result, anyhow::Error> { + let vars = types::GetDomainVars { domain }; + + let opt = client + .run_graphql(types::GetDomainWithRecords::build(vars)) + .await + .map_err(anyhow::Error::from)? + .get_domain; + Ok(opt) +} + +/// Retrieve all DNS records. +/// +/// NOTE: this is a privileged operation that requires extra permissions. +pub async fn get_all_dns_records( + client: &WasmerClient, + vars: types::GetAllDnsRecordsVariables, +) -> Result { + client + .run_graphql_strict(types::GetAllDnsRecords::build(vars)) + .await + .map_err(anyhow::Error::from) + .map(|x| x.get_all_dnsrecords) +} + +/// Retrieve a domain by its name. +/// +/// Specify with_records to also retrieve all records for the domain. +pub fn get_all_dns_records_stream( + client: &WasmerClient, + vars: types::GetAllDnsRecordsVariables, +) -> impl futures::Stream, anyhow::Error>> + '_ { + futures::stream::try_unfold( + Some(vars), + move |vars: Option| async move { + let vars = match vars { + Some(vars) => vars, + None => return Ok(None), + }; + + let page = get_all_dns_records(client, vars.clone()).await?; + + let end_cursor = page.page_info.end_cursor; + + let items = page + .edges + .into_iter() + .filter_map(|x| x.and_then(|x| x.node)) + .collect::>(); + + let new_vars = end_cursor.map(|c| types::GetAllDnsRecordsVariables { + after: Some(c), + ..vars + }); + + Ok(Some((items, new_vars))) + }, + ) +} + /// Convert a [`OffsetDateTime`] to a unix timestamp that the WAPM backend /// understands. fn unix_timestamp(ts: OffsetDateTime) -> f64 { diff --git a/lib/backend-api/src/types.rs b/lib/backend-api/src/types.rs index 6d6c8602b6b..216265dff5d 100644 --- a/lib/backend-api/src/types.rs +++ b/lib/backend-api/src/types.rs @@ -35,6 +35,7 @@ mod queries { #[derive(cynic::Enum, Clone, Copy, Debug)] pub enum GrapheneRole { + Owner, Admin, Editor, Viewer, @@ -608,6 +609,7 @@ mod queries { pub enum LogStream { Stdout, Stderr, + Runtime, } #[derive(cynic::QueryVariables, Debug, Clone)] @@ -730,6 +732,364 @@ mod queries { pub version: Option, } + #[derive(cynic::QueryFragment, Debug, Clone)] + #[cynic(graphql_type = "TXTRecord")] + pub struct TxtRecord { + pub id: cynic::Id, + pub created_at: DateTime, + pub updated_at: DateTime, + pub deleted_at: Option, + pub name: Option, + pub ttl: Option, + pub data: String, + + pub domain: DnsDomain, + } + + #[derive(cynic::QueryFragment, Debug, Clone)] + #[cynic(graphql_type = "SSHFPRecord")] + pub struct SshfpRecord { + pub id: cynic::Id, + pub created_at: DateTime, + pub updated_at: DateTime, + pub deleted_at: Option, + pub name: Option, + pub ttl: Option, + #[cynic(rename = "type")] + pub type_: DnsmanagerSshFingerprintRecordTypeChoices, + pub algorithm: DnsmanagerSshFingerprintRecordAlgorithmChoices, + pub fingerprint: String, + + pub domain: DnsDomain, + } + + #[derive(cynic::QueryFragment, Debug, Clone)] + #[cynic(graphql_type = "SRVRecord")] + pub struct SrvRecord { + pub id: cynic::Id, + pub created_at: DateTime, + pub updated_at: DateTime, + pub deleted_at: Option, + pub name: Option, + pub ttl: Option, + pub service: String, + pub protocol: String, + pub priority: i32, + pub weight: i32, + pub port: i32, + pub target: String, + + pub domain: DnsDomain, + } + + #[derive(cynic::QueryFragment, Debug, Clone)] + #[cynic(graphql_type = "SOARecord")] + pub struct SoaRecord { + pub id: cynic::Id, + pub created_at: DateTime, + pub updated_at: DateTime, + pub deleted_at: Option, + pub name: Option, + pub ttl: Option, + pub mname: String, + pub rname: String, + pub serial: BigInt, + pub refresh: BigInt, + pub retry: BigInt, + pub expire: BigInt, + pub minimum: BigInt, + + pub domain: DnsDomain, + } + + #[derive(cynic::Enum, Debug, Clone, Copy)] + pub enum DNSRecordsSortBy { + Newest, + Oldest, + } + + #[derive(cynic::QueryVariables, Debug, Clone)] + pub struct GetAllDnsRecordsVariables { + pub after: Option, + pub updated_after: Option, + pub sort_by: Option, + pub first: Option, + } + + #[derive(cynic::QueryFragment, Debug)] + #[cynic(graphql_type = "Query", variables = "GetAllDnsRecordsVariables")] + pub struct GetAllDnsRecords { + #[arguments( + first: $first, + after: $after, + updatedAfter: $updated_after, + sortBy: $sort_by + )] + #[cynic(rename = "getAllDNSRecords")] + pub get_all_dnsrecords: DnsRecordConnection, + } + + #[derive(cynic::QueryFragment, Debug, Clone)] + #[cynic(graphql_type = "PTRRecord")] + pub struct PtrRecord { + pub id: cynic::Id, + pub created_at: DateTime, + pub updated_at: DateTime, + pub deleted_at: Option, + pub name: Option, + pub ttl: Option, + pub ptrdname: String, + + pub domain: DnsDomain, + } + + #[derive(cynic::QueryFragment, Debug, Clone)] + #[cynic(graphql_type = "NSRecord")] + pub struct NsRecord { + pub id: cynic::Id, + pub created_at: DateTime, + pub updated_at: DateTime, + pub deleted_at: Option, + pub name: Option, + pub ttl: Option, + pub nsdname: String, + + pub domain: DnsDomain, + } + + #[derive(cynic::QueryFragment, Debug, Clone)] + #[cynic(graphql_type = "MXRecord")] + pub struct MxRecord { + pub id: cynic::Id, + pub created_at: DateTime, + pub updated_at: DateTime, + pub deleted_at: Option, + pub name: Option, + pub ttl: Option, + pub preference: i32, + pub exchange: String, + + pub domain: DnsDomain, + } + + #[derive(cynic::QueryFragment, Debug)] + #[cynic(graphql_type = "DNSRecordConnection")] + pub struct DnsRecordConnection { + pub page_info: PageInfo, + pub edges: Vec>, + } + + #[derive(cynic::QueryFragment, Debug)] + #[cynic(graphql_type = "DNSRecordEdge")] + pub struct DnsRecordEdge { + pub node: Option, + } + + #[derive(cynic::QueryFragment, Debug, Clone)] + #[cynic(graphql_type = "DNAMERecord")] + pub struct DNameRecord { + pub id: cynic::Id, + pub created_at: DateTime, + pub updated_at: DateTime, + pub deleted_at: Option, + pub name: Option, + pub ttl: Option, + pub d_name: String, + + pub domain: DnsDomain, + } + + #[derive(cynic::QueryFragment, Debug, Clone)] + #[cynic(graphql_type = "CNAMERecord")] + pub struct CNameRecord { + pub id: cynic::Id, + pub created_at: DateTime, + pub updated_at: DateTime, + pub deleted_at: Option, + pub name: Option, + pub ttl: Option, + pub c_name: String, + + pub domain: DnsDomain, + } + + #[derive(cynic::QueryFragment, Debug, Clone)] + #[cynic(graphql_type = "CAARecord")] + pub struct CaaRecord { + pub id: cynic::Id, + pub created_at: DateTime, + pub updated_at: DateTime, + pub deleted_at: Option, + pub name: Option, + pub ttl: Option, + pub value: String, + pub flags: i32, + pub tag: DnsmanagerCertificationAuthorityAuthorizationRecordTagChoices, + + pub domain: DnsDomain, + } + + #[derive(cynic::QueryFragment, Debug, Clone)] + #[cynic(graphql_type = "ARecord")] + pub struct ARecord { + pub id: cynic::Id, + pub created_at: DateTime, + pub updated_at: DateTime, + pub deleted_at: Option, + pub name: Option, + pub ttl: Option, + pub address: String, + + pub domain: DnsDomain, + } + + #[derive(cynic::QueryFragment, Debug, Clone)] + #[cynic(graphql_type = "AAAARecord")] + pub struct AaaaRecord { + pub id: cynic::Id, + pub created_at: DateTime, + pub updated_at: DateTime, + pub deleted_at: Option, + pub name: Option, + pub ttl: Option, + pub address: String, + + pub domain: DnsDomain, + } + + #[derive(cynic::InlineFragments, Debug, Clone)] + #[cynic(graphql_type = "DNSRecord")] + pub enum DnsRecord { + A(ARecord), + AAAA(AaaaRecord), + CName(CNameRecord), + Txt(TxtRecord), + Mx(MxRecord), + Ns(NsRecord), + CAA(CaaRecord), + DName(DNameRecord), + Ptr(PtrRecord), + Soa(SoaRecord), + Srv(SrvRecord), + Sshfp(SshfpRecord), + #[cynic(fallback)] + Unknown, + } + + impl DnsRecord { + pub fn id(&self) -> &str { + match self { + DnsRecord::A(record) => record.id.inner(), + DnsRecord::AAAA(record) => record.id.inner(), + DnsRecord::CName(record) => record.id.inner(), + DnsRecord::Txt(record) => record.id.inner(), + DnsRecord::Mx(record) => record.id.inner(), + DnsRecord::Ns(record) => record.id.inner(), + DnsRecord::CAA(record) => record.id.inner(), + DnsRecord::DName(record) => record.id.inner(), + DnsRecord::Ptr(record) => record.id.inner(), + DnsRecord::Soa(record) => record.id.inner(), + DnsRecord::Srv(record) => record.id.inner(), + DnsRecord::Sshfp(record) => record.id.inner(), + DnsRecord::Unknown => "", + } + } + + pub fn domain(&self) -> Option<&DnsDomain> { + match self { + DnsRecord::A(record) => Some(&record.domain), + DnsRecord::AAAA(record) => Some(&record.domain), + DnsRecord::CName(record) => Some(&record.domain), + DnsRecord::Txt(record) => Some(&record.domain), + DnsRecord::Mx(record) => Some(&record.domain), + DnsRecord::Ns(record) => Some(&record.domain), + DnsRecord::CAA(record) => Some(&record.domain), + DnsRecord::DName(record) => Some(&record.domain), + DnsRecord::Ptr(record) => Some(&record.domain), + DnsRecord::Soa(record) => Some(&record.domain), + DnsRecord::Srv(record) => Some(&record.domain), + DnsRecord::Sshfp(record) => Some(&record.domain), + DnsRecord::Unknown => None, + } + } + } + + #[derive(cynic::Enum, Clone, Copy, Debug)] + pub enum DnsmanagerCertificationAuthorityAuthorizationRecordTagChoices { + Issue, + Issuewild, + Iodef, + } + + impl DnsmanagerCertificationAuthorityAuthorizationRecordTagChoices { + pub fn as_str(self) -> &'static str { + match self { + Self::Issue => "issue", + Self::Issuewild => "issuewild", + Self::Iodef => "iodef", + } + } + } + + #[derive(cynic::Enum, Clone, Copy, Debug)] + pub enum DnsmanagerSshFingerprintRecordAlgorithmChoices { + #[cynic(rename = "A_1")] + A1, + #[cynic(rename = "A_2")] + A2, + #[cynic(rename = "A_3")] + A3, + #[cynic(rename = "A_4")] + A4, + } + + #[derive(cynic::Enum, Clone, Copy, Debug)] + pub enum DnsmanagerSshFingerprintRecordTypeChoices { + #[cynic(rename = "A_1")] + A1, + #[cynic(rename = "A_2")] + A2, + } + + #[derive(cynic::QueryVariables, Debug)] + pub struct GetDomainVars { + pub domain: String, + } + + #[derive(cynic::QueryFragment, Debug)] + #[cynic(graphql_type = "Query", variables = "GetDomainVars")] + pub struct GetDomain { + #[arguments(name: $domain)] + pub get_domain: Option, + } + + #[derive(cynic::QueryFragment, Debug)] + #[cynic(graphql_type = "Query", variables = "GetDomainVars")] + pub struct GetDomainWithRecords { + #[arguments(name: $domain)] + pub get_domain: Option, + } + + #[derive(cynic::QueryFragment, Debug, Clone)] + #[cynic(graphql_type = "DNSDomain")] + pub struct DnsDomain { + pub id: cynic::Id, + pub name: String, + pub slug: String, + } + + #[derive(cynic::QueryFragment, Debug, Clone)] + #[cynic(graphql_type = "DNSDomain")] + pub struct DnsDomainWithRecords { + pub id: cynic::Id, + pub name: String, + pub slug: String, + pub records: Option>>, + } + + #[derive(cynic::Scalar, Debug, Clone)] + pub struct BigInt(pub String); + #[derive(cynic::InlineFragments, Debug)] pub enum Node { DeployApp(Box), From 0a05e5e9ea7f6844f375da32722e5ae3c05753e5 Mon Sep 17 00:00:00 2001 From: Ayush Jha Date: Wed, 13 Mar 2024 19:56:09 +0545 Subject: [PATCH 2/7] scafolding for domains subcommand --- lib/cli/src/commands/domain/get.rs | 30 +++++++++++++++++++ lib/cli/src/commands/domain/list.rs | 22 ++++++++++++++ lib/cli/src/commands/domain/mod.rs | 37 +++++++++++++++++++++++ lib/cli/src/commands/domain/zonefile.rs | 40 +++++++++++++++++++++++++ lib/cli/src/commands/mod.rs | 6 ++++ 5 files changed, 135 insertions(+) create mode 100644 lib/cli/src/commands/domain/get.rs create mode 100644 lib/cli/src/commands/domain/list.rs create mode 100644 lib/cli/src/commands/domain/mod.rs create mode 100644 lib/cli/src/commands/domain/zonefile.rs diff --git a/lib/cli/src/commands/domain/get.rs b/lib/cli/src/commands/domain/get.rs new file mode 100644 index 00000000000..e278f5822db --- /dev/null +++ b/lib/cli/src/commands/domain/get.rs @@ -0,0 +1,30 @@ +use crate::{ + commands::AsyncCliCommand, + opts::{ApiOpts, ItemFormatOpts}, +}; + +/// Show a domain +#[derive(clap::Parser, Debug)] +pub struct CmdDomainGet { + #[clap(flatten)] + fmt: ItemFormatOpts, + #[clap(flatten)] + api: ApiOpts, + + /// Name of the domain. + name: String, + +} + +#[async_trait::async_trait] +impl AsyncCliCommand for CmdDomainGet { + type Output = (); + + async fn run_async(self) -> Result<(), anyhow::Error> { + let client = self.api.client()?; + let domain = wasmer_api::query::get_domain(&client, self.name) + .await?; + println!("{}", self.fmt.format.render(&domain)); + Ok(()) + } +} diff --git a/lib/cli/src/commands/domain/list.rs b/lib/cli/src/commands/domain/list.rs new file mode 100644 index 00000000000..66d0b673bb8 --- /dev/null +++ b/lib/cli/src/commands/domain/list.rs @@ -0,0 +1,22 @@ +use crate::{ + commands::AsyncCliCommand, + opts::{ApiOpts, ListFormatOpts}, +}; + +/// List domains. +#[derive(clap::Parser, Debug)] +pub struct CmdDomainList { + #[clap(flatten)] + fmt: ListFormatOpts, + #[clap(flatten)] + api: ApiOpts, +} + +#[async_trait::async_trait] +impl AsyncCliCommand for CmdDomainList { + type Output = (); + + async fn run_async(self) -> Result<(), anyhow::Error> { + Ok(()) + } +} diff --git a/lib/cli/src/commands/domain/mod.rs b/lib/cli/src/commands/domain/mod.rs new file mode 100644 index 00000000000..d86e6095077 --- /dev/null +++ b/lib/cli/src/commands/domain/mod.rs @@ -0,0 +1,37 @@ +pub mod list; +pub mod get; +pub mod zonefile; + +use crate::commands::AsyncCliCommand; + + +/// Manage DNS records +#[derive(clap::Subcommand, Debug)] +pub enum CmdDomain { + /// List domains + List(self::list::CmdDomainList), + + /// Get a domain + Get(self::get::CmdDomainGet), + + /// Get zone file for a domain + GetZoneFile(self::zonefile::CmdZoneFileGet), + + /// Sync local zone file with remotex + SyncZoneFile(self::zonefile::CmdZoneFileSync), + +} + +#[async_trait::async_trait] +impl AsyncCliCommand for CmdDomain { + type Output = (); + + async fn run_async(self) -> Result<(), anyhow::Error> { + match self { + CmdDomain::List(cmd) => cmd.run_async().await, + CmdDomain::Get(cmd) => cmd.run_async().await, + CmdDomain::GetZoneFile(cmd) => cmd.run_async().await, + CmdDomain::SyncZoneFile(cmd) => cmd.run_async().await, + } + } +} diff --git a/lib/cli/src/commands/domain/zonefile.rs b/lib/cli/src/commands/domain/zonefile.rs new file mode 100644 index 00000000000..2319fbeac17 --- /dev/null +++ b/lib/cli/src/commands/domain/zonefile.rs @@ -0,0 +1,40 @@ +use crate::commands::AsyncCliCommand; + +#[derive(clap::Parser, Debug)] +/// Show a zone file +pub struct CmdZoneFileGet { + /// Name of the domain. + domain_name: String, + + /// output file name to store zone file + #[clap(short='o', long="output", required = false)] + zone_file_path: Option, +} + +#[derive(clap::Parser, Debug)] +/// Show a zone file +pub struct CmdZoneFileSync { + /// filename of zone-file to sync + zone_file_path: String, +} + + +#[async_trait::async_trait] +impl AsyncCliCommand for CmdZoneFileGet { + type Output = (); + + async fn run_async(self) -> Result<(), anyhow::Error> { + Ok(()) + } +} + + + +#[async_trait::async_trait] +impl AsyncCliCommand for CmdZoneFileSync { + type Output = (); + + async fn run_async(self) -> Result<(), anyhow::Error> { + Ok(()) + } +} diff --git a/lib/cli/src/commands/mod.rs b/lib/cli/src/commands/mod.rs index 9ffc442e523..14f96423bcc 100644 --- a/lib/cli/src/commands/mod.rs +++ b/lib/cli/src/commands/mod.rs @@ -22,6 +22,7 @@ mod inspect; mod journal; mod login; pub(crate) mod namespace; +pub(crate) mod domain; mod package; mod publish; mod run; @@ -157,6 +158,7 @@ impl WasmerCmd { Some(Cmd::Journal(journal)) => journal.run(), Some(Cmd::Ssh(ssh)) => ssh.run(), Some(Cmd::Namespace(namespace)) => namespace.run(), + Some(Cmd::Domain(namespace)) => namespace.run(), None => { WasmerCmd::command().print_long_help()?; // Note: clap uses an exit code of 2 when CLI parsing fails @@ -356,6 +358,10 @@ enum Cmd { /// Manage Wasmer namespaces. #[clap(subcommand, alias = "namespaces")] Namespace(crate::commands::namespace::CmdNamespace), + + /// Manage DNS records + #[clap(subcommand, alias = "domains")] + Domain(crate::commands::domain::CmdDomain), } fn is_binfmt_interpreter() -> bool { From 85c5a040151eca86135f5278abc7422c93a1c9cd Mon Sep 17 00:00:00 2001 From: Ayush Jha Date: Thu, 14 Mar 2024 03:33:56 +0545 Subject: [PATCH 3/7] Update graphql schema, setup cynic based on the new schema --- lib/backend-api/schema.graphql | 100 ++++++++++---- lib/backend-api/src/query.rs | 76 ++++++++++- lib/backend-api/src/types.rs | 235 +++++++++++++++++++++++++++------ lib/cli/src/types.rs | 73 +++++++++- 4 files changed, 410 insertions(+), 74 deletions(-) diff --git a/lib/backend-api/schema.graphql b/lib/backend-api/schema.graphql index ddfc8824f13..1940b71e6a7 100644 --- a/lib/backend-api/schema.graphql +++ b/lib/backend-api/schema.graphql @@ -1511,6 +1511,10 @@ type DNSDomain implements Node { """This zone will be accessible at /dns/{slug}/.""" slug: String! + zoneFile: String! + createdAt: DateTime! + updatedAt: DateTime! + deletedAt: DateTime """The ID of the object""" id: ID! @@ -1528,16 +1532,18 @@ type ARecord implements Node & DNSRecordInterface { """The ID of the object""" id: ID! - name: String - ttl: Int + name: String! + ttl: Int! dnsClass: String + text: String! domain: DNSDomain! } interface DNSRecordInterface { - name: String - ttl: Int + name: String! + ttl: Int! dnsClass: String + text: String! domain: DNSDomain! createdAt: DateTime! updatedAt: DateTime! @@ -1552,9 +1558,10 @@ type AAAARecord implements Node & DNSRecordInterface { """The ID of the object""" id: ID! - name: String - ttl: Int + name: String! + ttl: Int! dnsClass: String + text: String! domain: DNSDomain! } @@ -1568,9 +1575,10 @@ type CNAMERecord implements Node & DNSRecordInterface { """The ID of the object""" id: ID! - name: String - ttl: Int + name: String! + ttl: Int! dnsClass: String + text: String! domain: DNSDomain! } @@ -1582,9 +1590,10 @@ type TXTRecord implements Node & DNSRecordInterface { """The ID of the object""" id: ID! - name: String - ttl: Int + name: String! + ttl: Int! dnsClass: String + text: String! domain: DNSDomain! } @@ -1597,9 +1606,10 @@ type MXRecord implements Node & DNSRecordInterface { """The ID of the object""" id: ID! - name: String - ttl: Int + name: String! + ttl: Int! dnsClass: String + text: String! domain: DNSDomain! } @@ -1611,9 +1621,10 @@ type NSRecord implements Node & DNSRecordInterface { """The ID of the object""" id: ID! - name: String - ttl: Int + name: String! + ttl: Int! dnsClass: String + text: String! domain: DNSDomain! } @@ -1627,9 +1638,10 @@ type CAARecord implements Node & DNSRecordInterface { """The ID of the object""" id: ID! - name: String - ttl: Int + name: String! + ttl: Int! dnsClass: String + text: String! domain: DNSDomain! } @@ -1656,9 +1668,10 @@ type DNAMERecord implements Node & DNSRecordInterface { """The ID of the object""" id: ID! - name: String - ttl: Int + name: String! + ttl: Int! dnsClass: String + text: String! domain: DNSDomain! } @@ -1670,9 +1683,10 @@ type PTRRecord implements Node & DNSRecordInterface { """The ID of the object""" id: ID! - name: String - ttl: Int + name: String! + ttl: Int! dnsClass: String + text: String! domain: DNSDomain! } @@ -1712,9 +1726,10 @@ type SOARecord implements Node & DNSRecordInterface { """The ID of the object""" id: ID! - name: String - ttl: Int + name: String! + ttl: Int! dnsClass: String + text: String! domain: DNSDomain! } @@ -1747,9 +1762,10 @@ type SRVRecord implements Node & DNSRecordInterface { """The ID of the object""" id: ID! - name: String - ttl: Int + name: String! + ttl: Int! dnsClass: String + text: String! domain: DNSDomain! } @@ -1763,9 +1779,10 @@ type SSHFPRecord implements Node & DNSRecordInterface { """The ID of the object""" id: ID! - name: String - ttl: Int + name: String! + ttl: Int! dnsClass: String + text: String! domain: DNSDomain! } @@ -1904,7 +1921,7 @@ type UserNotificationKindValidateEmail { Enum of ways a user can login. One user can have many login methods associated with their account. - + """ enum LoginMethod { GOOGLE @@ -2177,10 +2194,15 @@ input WorkloadRunnerWasmSourceV1 { type Query { latestTOS: TermsOfService! getDeployAppVersion(name: String!, owner: String, version: String): DeployAppVersion - getAllDomains(offset: Int, before: String, after: String, first: Int, last: Int): DNSDomainConnection! + getAllDomains(namespace: String, offset: Int, before: String, after: String, first: Int, last: Int): DNSDomainConnection! getAllDNSRecords(sortBy: DNSRecordsSortBy, updatedAfter: DateTime, before: String, after: String, first: Int, last: Int): DNSRecordConnection! getDomain(name: String!): DNSDomain - getDeployApp(name: String!, owner: String): DeployApp + getDeployApp( + name: String! + + """Owner of the app. Defaults to logged in user.""" + owner: String + ): DeployApp getAppByGlobalAlias(alias: String!): DeployApp getDeployApps(sortBy: DeployAppsSortBy, updatedAfter: DateTime, offset: Int, before: String, after: String, first: Int, last: Int): DeployAppConnection! getAppVersions(sortBy: DeployAppVersionsSortBy, updatedAfter: DateTime, offset: Int, before: String, after: String, first: Int, last: Int): DeployAppVersionConnection! @@ -2728,6 +2750,7 @@ type Mutation { registerDomain(input: RegisterDomainInput!): RegisterDomainPayload upsertDNSRecord(input: UpsertDNSRecordInput!): UpsertDNSRecordPayload deleteDnsRecord(input: DeleteDNSRecordInput!): DeleteDNSRecordPayload + upsertDomainFromZoneFile(input: UpsertDomainFromZoneFileInput!): UpsertDomainFromZoneFilePayload deleteDomain(input: DeleteDomainInput!): DeleteDomainPayload tokenAuth(input: ObtainJSONWebTokenInput!): ObtainJSONWebTokenPayload generateDeployToken(input: GenerateDeployTokenInput!): GenerateDeployTokenPayload @@ -3028,11 +3051,13 @@ type RegisterDomainPayload { input RegisterDomainInput { name: String! namespace: String! + importRecords: Boolean = false clientMutationId: String } type UpsertDNSRecordPayload { success: Boolean! + record: DNSRecord! clientMutationId: String } @@ -3055,6 +3080,11 @@ enum RecordKind { NS TXT DNAME + PTR + SOA + SRV + CAA + SSHFP } input DNSMXExtraInput { @@ -3071,6 +3101,18 @@ input DeleteDNSRecordInput { clientMutationId: String } +type UpsertDomainFromZoneFilePayload { + success: Boolean! + domain: DNSDomain! + clientMutationId: String +} + +input UpsertDomainFromZoneFileInput { + zoneFile: String! + deleteMissingRecords: Boolean + clientMutationId: String +} + type DeleteDomainPayload { success: Boolean! clientMutationId: String diff --git a/lib/backend-api/src/query.rs b/lib/backend-api/src/query.rs index 59f3d3aff1e..e5be10ccf32 100644 --- a/lib/backend-api/src/query.rs +++ b/lib/backend-api/src/query.rs @@ -13,7 +13,7 @@ use crate::{ self, CreateNamespaceVars, DeployApp, DeployAppConnection, DeployAppVersion, DeployAppVersionConnection, GetCurrentUserWithAppsVars, GetDeployAppAndVersion, GetDeployAppVersionsVars, GetNamespaceAppsVars, Log, LogStream, PackageVersionConnection, - PublishDeployAppVars, + PublishDeployAppVars, UpsertDomainFromZoneFileVars, DnsDomain, }, GraphQLApiFailure, WasmerClient, }; @@ -822,6 +822,39 @@ pub async fn get_app_logs_paginated( pub async fn get_domain( client: &WasmerClient, domain: String, +) -> Result, anyhow::Error> { + let vars = types::GetDomainVars { domain }; + + let opt = client + .run_graphql(types::GetDomain::build(vars)) + .await + .map_err(anyhow::Error::from)? + .get_domain; + Ok(opt) +} + +/// Retrieve a domain by its name. +/// +/// Specify with_records to also retrieve all records for the domain. +pub async fn get_domain_zone_file( + client: &WasmerClient, + domain: String, +) -> Result, anyhow::Error> { + let vars = types::GetDomainVars { domain }; + + let opt = client + .run_graphql(types::GetDomainWithZoneFile::build(vars)) + .await + .map_err(anyhow::Error::from)? + .get_domain; + Ok(opt) +} + + +/// Retrieve a domain by its name, along with all it's records. +pub async fn get_domain_with_records( + client: &WasmerClient, + domain: String, ) -> Result, anyhow::Error> { let vars = types::GetDomainVars { domain }; @@ -847,6 +880,19 @@ pub async fn get_all_dns_records( .map(|x| x.get_all_dnsrecords) } +/// Retrieve all DNS domains. +pub async fn get_all_domains( + client: &WasmerClient, + vars: types::GetAllDomainsVariables, +) -> Result { + client + .run_graphql_strict(types::GetAllDomains::build(vars)) + .await + .map_err(anyhow::Error::from) + .map(|x| x.get_all_domains) +} + + /// Retrieve a domain by its name. /// /// Specify with_records to also retrieve all records for the domain. @@ -892,3 +938,31 @@ fn unix_timestamp(ts: OffsetDateTime) -> f64 { (secs as f64) + (nanos as f64 / nanos_per_second as f64) } + +/// Publish a new app (version). +pub async fn upsert_domain_from_zone_file( + client: &WasmerClient, + zone_file_contents: String, + delete_missing_records: bool, +) -> Result { + let vars = UpsertDomainFromZoneFileVars { + zone_file: zone_file_contents, + delete_missing_records: Some(delete_missing_records), + }; + let res = client + .run_graphql_raw(types::UpsertDomainFromZoneFile::build(vars)) + .await?; + + if let Some(domain) = res + .data + .and_then(|d| d.upsert_domain_from_zone_file) + .map(|d| d.domain) + { + Ok(domain) + } else { + Err(GraphQLApiFailure::from_errors( + "could not sync zone file", + res.errors, + )) + } +} diff --git a/lib/backend-api/src/types.rs b/lib/backend-api/src/types.rs index 216265dff5d..2b59ac88345 100644 --- a/lib/backend-api/src/types.rs +++ b/lib/backend-api/src/types.rs @@ -58,7 +58,7 @@ mod queries { pub username: String, } - #[derive(cynic::QueryFragment, Debug, Clone)] + #[derive(cynic::QueryFragment, Debug, Clone, Serialize)] pub struct Package { pub id: cynic::Id, pub package_name: String, @@ -67,7 +67,7 @@ mod queries { pub private: bool, } - #[derive(cynic::QueryFragment, Debug, Clone)] + #[derive(cynic::QueryFragment, Debug, Clone, Serialize)] pub struct PackageDistribution { pub pirita_sha256_hash: Option, pub pirita_download_url: Option, @@ -76,7 +76,7 @@ mod queries { pub pirita_size: Option, } - #[derive(cynic::QueryFragment, Debug, Clone)] + #[derive(cynic::QueryFragment, Debug, Clone, Serialize)] pub struct PackageVersion { pub id: cynic::Id, pub version: String, @@ -84,7 +84,7 @@ mod queries { pub distribution: PackageDistribution, } - #[derive(cynic::QueryFragment, Debug, Clone)] + #[derive(cynic::QueryFragment, Debug, Clone, Serialize)] #[cynic(graphql_type = "PackageVersion")] pub struct PackageVersionWithPackage { pub id: cynic::Id, @@ -210,7 +210,7 @@ mod queries { pub global_name: String, } - #[derive(cynic::QueryFragment, Debug, Clone)] + #[derive(cynic::QueryFragment, Debug, Clone, Serialize)] #[cynic(graphql_type = "User", variables = "GetCurrentUserVars")] pub struct UserWithNamespaces { pub id: cynic::Id, @@ -329,6 +329,25 @@ mod queries { pub get_deploy_app_version: Option, } + #[derive(cynic::QueryVariables, Debug)] + pub struct UpsertDomainFromZoneFileVars { + pub zone_file: String, + pub delete_missing_records: Option, + } + + #[derive(cynic::QueryFragment, Debug)] + #[cynic(graphql_type = "Mutation", variables = "UpsertDomainFromZoneFileVars")] + pub struct UpsertDomainFromZoneFile { + #[arguments(input: {zoneFile: $zone_file, deleteMissingRecords: $delete_missing_records})] + pub upsert_domain_from_zone_file: Option, + } + + #[derive(cynic::QueryFragment, Debug)] + pub struct UpsertDomainFromZoneFilePayload { + pub success: bool, + pub domain: DnsDomain, + } + #[derive(cynic::QueryVariables, Debug)] pub struct CreateNamespaceVars { pub name: String, @@ -356,12 +375,12 @@ mod queries { pub client_mutation_id: Option, } - #[derive(cynic::QueryFragment, Debug, Clone)] + #[derive(cynic::QueryFragment, Debug, Clone, Serialize)] pub struct NamespaceEdge { pub node: Option, } - #[derive(cynic::QueryFragment, Debug, Clone)] + #[derive(cynic::QueryFragment, Debug, Clone, Serialize)] pub struct NamespaceConnection { pub edges: Vec>, } @@ -439,14 +458,14 @@ mod queries { pub sort_by: Option, } - #[derive(cynic::QueryFragment, Debug, Clone)] + #[derive(cynic::QueryFragment, Debug, Clone, Serialize)] #[cynic(graphql_type = "Query", variables = "GetDeployAppVersionsVars")] pub struct GetDeployAppVersions { #[arguments(owner: $owner, name: $name)] pub get_deploy_app: Option, } - #[derive(cynic::QueryFragment, Debug, Clone)] + #[derive(cynic::QueryFragment, Debug, Clone, Serialize)] #[cynic(graphql_type = "DeployApp", variables = "GetDeployAppVersionsVars")] pub struct DeployAppVersions { #[arguments( @@ -481,13 +500,13 @@ mod queries { pub app: Option, } - #[derive(cynic::QueryFragment, Debug, Clone)] + #[derive(cynic::QueryFragment, Debug, Clone, Serialize)] pub struct DeployAppVersionConnection { pub page_info: PageInfo, pub edges: Vec>, } - #[derive(cynic::QueryFragment, Debug, Clone)] + #[derive(cynic::QueryFragment, Debug, Clone, Serialize)] pub struct DeployAppVersionEdge { pub node: Option, pub cursor: String, @@ -618,13 +637,13 @@ mod queries { pub owner: String, /// The tag associated with a particular app version. Uses the active /// version if not provided. - pub version: Option, + pub version: Option, /// The lower bound for log messages, in nanoseconds since the Unix /// epoch. - pub starting_from: f64, + pub starting_from: f64, /// The upper bound for log messages, in nanoseconds since the Unix /// epoch. - pub until: Option, + pub until: Option, pub first: Option, pub streams: Option>, @@ -658,7 +677,7 @@ mod queries { pub struct Log { pub message: String, /// When the message was recorded, in nanoseconds since the Unix epoch. - pub timestamp: f64, + pub timestamp: f64, } #[derive(cynic::QueryVariables, Debug)] @@ -732,7 +751,7 @@ mod queries { pub version: Option, } - #[derive(cynic::QueryFragment, Debug, Clone)] + #[derive(cynic::QueryFragment, Debug, Clone, Serialize)] #[cynic(graphql_type = "TXTRecord")] pub struct TxtRecord { pub id: cynic::Id, @@ -740,13 +759,14 @@ mod queries { pub updated_at: DateTime, pub deleted_at: Option, pub name: Option, - pub ttl: Option, + pub text: String, + pub ttl: Option, pub data: String, pub domain: DnsDomain, } - #[derive(cynic::QueryFragment, Debug, Clone)] + #[derive(cynic::QueryFragment, Debug, Clone, Serialize)] #[cynic(graphql_type = "SSHFPRecord")] pub struct SshfpRecord { pub id: cynic::Id, @@ -754,7 +774,8 @@ mod queries { pub updated_at: DateTime, pub deleted_at: Option, pub name: Option, - pub ttl: Option, + pub text: String, + pub ttl: Option, #[cynic(rename = "type")] pub type_: DnsmanagerSshFingerprintRecordTypeChoices, pub algorithm: DnsmanagerSshFingerprintRecordAlgorithmChoices, @@ -763,7 +784,7 @@ mod queries { pub domain: DnsDomain, } - #[derive(cynic::QueryFragment, Debug, Clone)] + #[derive(cynic::QueryFragment, Debug, Clone, Serialize)] #[cynic(graphql_type = "SRVRecord")] pub struct SrvRecord { pub id: cynic::Id, @@ -771,7 +792,8 @@ mod queries { pub updated_at: DateTime, pub deleted_at: Option, pub name: Option, - pub ttl: Option, + pub text: String, + pub ttl: Option, pub service: String, pub protocol: String, pub priority: i32, @@ -782,7 +804,7 @@ mod queries { pub domain: DnsDomain, } - #[derive(cynic::QueryFragment, Debug, Clone)] + #[derive(cynic::QueryFragment, Debug, Clone, Serialize)] #[cynic(graphql_type = "SOARecord")] pub struct SoaRecord { pub id: cynic::Id, @@ -790,7 +812,8 @@ mod queries { pub updated_at: DateTime, pub deleted_at: Option, pub name: Option, - pub ttl: Option, + pub text: String, + pub ttl: Option, pub mname: String, pub rname: String, pub serial: BigInt, @@ -829,7 +852,26 @@ mod queries { pub get_all_dnsrecords: DnsRecordConnection, } - #[derive(cynic::QueryFragment, Debug, Clone)] + #[derive(cynic::QueryVariables, Debug, Clone)] + pub struct GetAllDomainsVariables { + pub after: Option, + pub first: Option, + pub namespace: Option, + } + + #[derive(cynic::QueryFragment, Debug)] + #[cynic(graphql_type = "Query", variables = "GetAllDomainsVariables")] + pub struct GetAllDomains { + #[arguments( + first: $first, + after: $after, + namespace: $namespace, + )] + #[cynic(rename = "getAllDomains")] + pub get_all_domains: DnsDomainConnection, + } + + #[derive(cynic::QueryFragment, Debug, Clone, Serialize)] #[cynic(graphql_type = "PTRRecord")] pub struct PtrRecord { pub id: cynic::Id, @@ -837,13 +879,14 @@ mod queries { pub updated_at: DateTime, pub deleted_at: Option, pub name: Option, + pub text: String, pub ttl: Option, pub ptrdname: String, pub domain: DnsDomain, } - #[derive(cynic::QueryFragment, Debug, Clone)] + #[derive(cynic::QueryFragment, Debug, Clone, Serialize)] #[cynic(graphql_type = "NSRecord")] pub struct NsRecord { pub id: cynic::Id, @@ -851,13 +894,14 @@ mod queries { pub updated_at: DateTime, pub deleted_at: Option, pub name: Option, - pub ttl: Option, + pub text: String, + pub ttl: Option, pub nsdname: String, pub domain: DnsDomain, } - #[derive(cynic::QueryFragment, Debug, Clone)] + #[derive(cynic::QueryFragment, Debug, Clone, Serialize)] #[cynic(graphql_type = "MXRecord")] pub struct MxRecord { pub id: cynic::Id, @@ -865,6 +909,7 @@ mod queries { pub updated_at: DateTime, pub deleted_at: Option, pub name: Option, + pub text: String, pub ttl: Option, pub preference: i32, pub exchange: String, @@ -885,7 +930,20 @@ mod queries { pub node: Option, } - #[derive(cynic::QueryFragment, Debug, Clone)] + #[derive(cynic::QueryFragment, Debug)] + #[cynic(graphql_type = "DNSDomainConnection")] + pub struct DnsDomainConnection { + pub page_info: PageInfo, + pub edges: Vec>, + } + + #[derive(cynic::QueryFragment, Debug)] + #[cynic(graphql_type = "DNSDomainEdge")] + pub struct DnsDomainEdge { + pub node: Option, + } + + #[derive(cynic::QueryFragment, Debug, Clone, Serialize)] #[cynic(graphql_type = "DNAMERecord")] pub struct DNameRecord { pub id: cynic::Id, @@ -893,13 +951,14 @@ mod queries { pub updated_at: DateTime, pub deleted_at: Option, pub name: Option, - pub ttl: Option, + pub text: String, + pub ttl: Option, pub d_name: String, pub domain: DnsDomain, } - #[derive(cynic::QueryFragment, Debug, Clone)] + #[derive(cynic::QueryFragment, Debug, Clone, Serialize)] #[cynic(graphql_type = "CNAMERecord")] pub struct CNameRecord { pub id: cynic::Id, @@ -907,13 +966,14 @@ mod queries { pub updated_at: DateTime, pub deleted_at: Option, pub name: Option, - pub ttl: Option, + pub text: String, + pub ttl: Option, pub c_name: String, pub domain: DnsDomain, } - #[derive(cynic::QueryFragment, Debug, Clone)] + #[derive(cynic::QueryFragment, Debug, Clone, Serialize)] #[cynic(graphql_type = "CAARecord")] pub struct CaaRecord { pub id: cynic::Id, @@ -921,7 +981,8 @@ mod queries { pub updated_at: DateTime, pub deleted_at: Option, pub name: Option, - pub ttl: Option, + pub text: String, + pub ttl: Option, pub value: String, pub flags: i32, pub tag: DnsmanagerCertificationAuthorityAuthorizationRecordTagChoices, @@ -929,7 +990,7 @@ mod queries { pub domain: DnsDomain, } - #[derive(cynic::QueryFragment, Debug, Clone)] + #[derive(cynic::QueryFragment, Debug, Clone, Serialize)] #[cynic(graphql_type = "ARecord")] pub struct ARecord { pub id: cynic::Id, @@ -937,13 +998,13 @@ mod queries { pub updated_at: DateTime, pub deleted_at: Option, pub name: Option, - pub ttl: Option, + pub text: String, + pub ttl: Option, pub address: String, - pub domain: DnsDomain, } - #[derive(cynic::QueryFragment, Debug, Clone)] + #[derive(cynic::QueryFragment, Debug, Clone, Serialize)] #[cynic(graphql_type = "AAAARecord")] pub struct AaaaRecord { pub id: cynic::Id, @@ -951,13 +1012,13 @@ mod queries { pub updated_at: DateTime, pub deleted_at: Option, pub name: Option, - pub ttl: Option, + pub text: String, + pub ttl: Option, pub address: String, - pub domain: DnsDomain, } - #[derive(cynic::InlineFragments, Debug, Clone)] + #[derive(cynic::InlineFragments, Debug, Clone, Serialize)] #[cynic(graphql_type = "DNSRecord")] pub enum DnsRecord { A(ARecord), @@ -994,6 +1055,75 @@ mod queries { DnsRecord::Unknown => "", } } + pub fn name(&self) -> Option<&str> { + match self { + DnsRecord::A(record) => record.name.as_deref(), + DnsRecord::AAAA(record) => record.name.as_deref(), + DnsRecord::CName(record) => record.name.as_deref(), + DnsRecord::Txt(record) => record.name.as_deref(), + DnsRecord::Mx(record) => record.name.as_deref(), + DnsRecord::Ns(record) => record.name.as_deref(), + DnsRecord::CAA(record) => record.name.as_deref(), + DnsRecord::DName(record) => record.name.as_deref(), + DnsRecord::Ptr(record) => record.name.as_deref(), + DnsRecord::Soa(record) => record.name.as_deref(), + DnsRecord::Srv(record) => record.name.as_deref(), + DnsRecord::Sshfp(record) => record.name.as_deref(), + DnsRecord::Unknown => None, + } + } + pub fn ttl(&self) -> Option { + match self { + DnsRecord::A(record) => record.ttl, + DnsRecord::AAAA(record) => record.ttl, + DnsRecord::CName(record) => record.ttl, + DnsRecord::Txt(record) => record.ttl, + DnsRecord::Mx(record) => record.ttl, + DnsRecord::Ns(record) => record.ttl, + DnsRecord::CAA(record) => record.ttl, + DnsRecord::DName(record) => record.ttl, + DnsRecord::Ptr(record) => record.ttl, + DnsRecord::Soa(record) => record.ttl, + DnsRecord::Srv(record) => record.ttl, + DnsRecord::Sshfp(record) => record.ttl, + DnsRecord::Unknown => None, + } + } + + pub fn text(&self) -> &str { + match self { + DnsRecord::A(record) => record.text.as_str(), + DnsRecord::AAAA(record) => record.text.as_str(), + DnsRecord::CName(record) => record.text.as_str(), + DnsRecord::Txt(record) => record.text.as_str(), + DnsRecord::Mx(record) => record.text.as_str(), + DnsRecord::Ns(record) => record.text.as_str(), + DnsRecord::CAA(record) => record.text.as_str(), + DnsRecord::DName(record) => record.text.as_str(), + DnsRecord::Ptr(record) => record.text.as_str(), + DnsRecord::Soa(record) => record.text.as_str(), + DnsRecord::Srv(record) => record.text.as_str(), + DnsRecord::Sshfp(record) => record.text.as_str(), + DnsRecord::Unknown => "", + } + } + pub fn record_type(&self) -> &str { + match self { + DnsRecord::A(_) => "A", + DnsRecord::AAAA(_) => "AAAA", + DnsRecord::CName(_) => "CNAME", + DnsRecord::Txt(_) => "TXT", + DnsRecord::Mx(_) => "MX", + DnsRecord::Ns(_) => "NS", + DnsRecord::CAA(_) => "CAA", + DnsRecord::DName(_) => "DNAME", + DnsRecord::Ptr(_) => "PTR", + DnsRecord::Soa(_) => "SOA", + DnsRecord::Srv(_) => "SRV", + DnsRecord::Sshfp(_) => "SSHFP", + DnsRecord::Unknown => "", + } + } pub fn domain(&self) -> Option<&DnsDomain> { match self { @@ -1063,6 +1193,13 @@ mod queries { pub get_domain: Option, } + #[derive(cynic::QueryFragment, Debug)] + #[cynic(graphql_type = "Query", variables = "GetDomainVars")] + pub struct GetDomainWithZoneFile { + #[arguments(name: $domain)] + pub get_domain: Option, + } + #[derive(cynic::QueryFragment, Debug)] #[cynic(graphql_type = "Query", variables = "GetDomainVars")] pub struct GetDomainWithRecords { @@ -1070,15 +1207,26 @@ mod queries { pub get_domain: Option, } - #[derive(cynic::QueryFragment, Debug, Clone)] + #[derive(cynic::QueryFragment, Debug, Clone, Serialize)] #[cynic(graphql_type = "DNSDomain")] pub struct DnsDomain { pub id: cynic::Id, pub name: String, pub slug: String, + pub owner: Owner, } - #[derive(cynic::QueryFragment, Debug, Clone)] + #[derive(cynic::QueryFragment, Debug, Clone, Serialize)] + #[cynic(graphql_type = "DNSDomain")] + pub struct DnsDomainWithZoneFile { + pub id: cynic::Id, + pub name: String, + pub slug: String, + pub zone_file: String, + } + + + #[derive(cynic::QueryFragment, Debug, Clone, Serialize)] #[cynic(graphql_type = "DNSDomain")] pub struct DnsDomainWithRecords { pub id: cynic::Id, @@ -1087,8 +1235,9 @@ mod queries { pub records: Option>>, } + #[derive(cynic::Scalar, Debug, Clone)] - pub struct BigInt(pub String); + pub struct BigInt(pub u64); #[derive(cynic::InlineFragments, Debug)] pub enum Node { diff --git a/lib/cli/src/types.rs b/lib/cli/src/types.rs index 800a648b423..1bcf770ff6a 100644 --- a/lib/cli/src/types.rs +++ b/lib/cli/src/types.rs @@ -1,8 +1,79 @@ use comfy_table::Table; -use wasmer_api::types::{DeployApp, DeployAppVersion, Namespace}; +use wasmer_api::types::{DeployApp, DeployAppVersion, Namespace, DnsDomain, DnsDomainWithRecords}; use crate::utils::render::CliRender; +impl CliRender for DnsDomain { + fn render_item_table(&self) -> String { + let mut table = Table::new(); + table.add_rows([ + vec!["Domain".to_string(), self.name.clone()], + ]); + table.to_string() + } + + fn render_list_table(items: &[Self]) -> String { + if items.is_empty() { + return String::new(); + } + let mut table = Table::new(); + table.set_header(vec!["Domain".to_string(), "Owner".to_string()]); + table.add_rows( + items + .iter() + .map(|ns| vec![ns.name.clone(), ns.owner.global_name.clone()]), + ); + table.to_string() + } + +} + +impl CliRender for DnsDomainWithRecords { + fn render_item_table(&self) -> String { + let mut output = String::new(); + let mut table = Table::new(); + table.load_preset(comfy_table::presets::UTF8_FULL_CONDENSED) + .set_header(vec![ + "Type".to_string(), + "Name".to_string(), + "TTL".to_string(), + "Value".to_string(), + ]); + let mut rows: Vec> = vec![]; + if let Some(ref records) = self.records { + for record in records { + if let Some(ref record) = record { + rows.push(vec![ + record.record_type().to_string(), + record.name().expect("Expected record name").to_string(), + record.ttl().expect("expected a TTL value for record").to_string(), + record.text().to_string(), + ]); + } + } + } + + table.add_rows(rows); + output += &table.to_string(); + output + } + + fn render_list_table(items: &[Self]) -> String { + if items.is_empty() { + return String::new(); + } + let mut table = Table::new(); + table.set_header(vec!["Domain".to_string()]); + table.add_rows( + items + .iter() + .map(|ns| vec![ns.name.clone()]), + ); + table.to_string() + } + +} + impl CliRender for Namespace { fn render_item_table(&self) -> String { let mut table = Table::new(); From 011e494e29d7a2cfec98fa18c4f11484b097e709 Mon Sep 17 00:00:00 2001 From: Ayush Jha Date: Thu, 14 Mar 2024 03:34:29 +0545 Subject: [PATCH 4/7] add `get`, `set`, `get-zone-file`, `sync-zone-file` subcommands --- lib/cli/src/commands/domain/get.rs | 13 ++++----- lib/cli/src/commands/domain/list.rs | 15 +++++++++++ lib/cli/src/commands/domain/zonefile.rs | 36 +++++++++++++++++++++++-- lib/cli/src/opts.rs | 13 +++++++-- 4 files changed, 67 insertions(+), 10 deletions(-) diff --git a/lib/cli/src/commands/domain/get.rs b/lib/cli/src/commands/domain/get.rs index e278f5822db..4dfce3fdc29 100644 --- a/lib/cli/src/commands/domain/get.rs +++ b/lib/cli/src/commands/domain/get.rs @@ -1,19 +1,18 @@ use crate::{ commands::AsyncCliCommand, - opts::{ApiOpts, ItemFormatOpts}, + opts::{ApiOpts, ItemTableFormatOpts}, }; /// Show a domain #[derive(clap::Parser, Debug)] pub struct CmdDomainGet { #[clap(flatten)] - fmt: ItemFormatOpts, + fmt: ItemTableFormatOpts, #[clap(flatten)] api: ApiOpts, /// Name of the domain. name: String, - } #[async_trait::async_trait] @@ -22,9 +21,11 @@ impl AsyncCliCommand for CmdDomainGet { async fn run_async(self) -> Result<(), anyhow::Error> { let client = self.api.client()?; - let domain = wasmer_api::query::get_domain(&client, self.name) - .await?; - println!("{}", self.fmt.format.render(&domain)); + if let Some(domain) = wasmer_api::query::get_domain_with_records(&client, self.name).await? { + println!("{}", self.fmt.format.render(&domain)); + } else { + println!("Domain not found"); + } Ok(()) } } diff --git a/lib/cli/src/commands/domain/list.rs b/lib/cli/src/commands/domain/list.rs index 66d0b673bb8..8e03d29ab59 100644 --- a/lib/cli/src/commands/domain/list.rs +++ b/lib/cli/src/commands/domain/list.rs @@ -1,3 +1,5 @@ +use wasmer_api::types::GetAllDomainsVariables; + use crate::{ commands::AsyncCliCommand, opts::{ApiOpts, ListFormatOpts}, @@ -10,6 +12,9 @@ pub struct CmdDomainList { fmt: ListFormatOpts, #[clap(flatten)] api: ApiOpts, + + /// Name of the namespace. + namespace: Option, } #[async_trait::async_trait] @@ -17,6 +22,16 @@ impl AsyncCliCommand for CmdDomainList { type Output = (); async fn run_async(self) -> Result<(), anyhow::Error> { + let client = self.api.client()?; + let domains_connection = wasmer_api::query::get_all_domains(&client, GetAllDomainsVariables { + first: None, after: None, namespace: self.namespace, + }).await?; + let domains = domains_connection + .edges + .into_iter() + .map(|edge| edge.expect("domain not found").node.expect("domain not found")) + .collect::>(); + println!("{}", self.fmt.format.render(&domains)); Ok(()) } } diff --git a/lib/cli/src/commands/domain/zonefile.rs b/lib/cli/src/commands/domain/zonefile.rs index 2319fbeac17..0c3355bc893 100644 --- a/lib/cli/src/commands/domain/zonefile.rs +++ b/lib/cli/src/commands/domain/zonefile.rs @@ -1,8 +1,17 @@ -use crate::commands::AsyncCliCommand; +use crate::{ + commands::AsyncCliCommand, + opts::{ApiOpts, ItemFormatOpts}, +}; #[derive(clap::Parser, Debug)] /// Show a zone file pub struct CmdZoneFileGet { + #[clap(flatten)] + fmt: ItemFormatOpts, + + #[clap(flatten)] + api: ApiOpts, + /// Name of the domain. domain_name: String, @@ -14,8 +23,15 @@ pub struct CmdZoneFileGet { #[derive(clap::Parser, Debug)] /// Show a zone file pub struct CmdZoneFileSync { + #[clap(flatten)] + api: ApiOpts, + /// filename of zone-file to sync zone_file_path: String, + + /// Do not delete records that are not present in the zone file + #[clap(short='n', long="no-delete-missing-records", required = false, default_value = "false")] + no_delete_missing_records: bool, } @@ -24,17 +40,33 @@ impl AsyncCliCommand for CmdZoneFileGet { type Output = (); async fn run_async(self) -> Result<(), anyhow::Error> { + let client = self.api.client()?; + if let Some(domain) = wasmer_api::query::get_domain_zone_file(&client, self.domain_name).await? { + let zone_file_contents = domain.zone_file; + if let Some(zone_file_path) = self.zone_file_path { + std::fs::write(zone_file_path, zone_file_contents).expect("Unable to write file"); + } + else { + println!("{}", zone_file_contents); + } + } + else { + println!("Domain not found"); + } Ok(()) } } - #[async_trait::async_trait] impl AsyncCliCommand for CmdZoneFileSync { type Output = (); async fn run_async(self) -> Result<(), anyhow::Error> { + let data = std::fs::read(&self.zone_file_path).expect("Unable to read file"); + let zone_file_contents = String::from_utf8(data).expect("Not a valid UTF-8 sequence"); + let domain = wasmer_api::query::upsert_domain_from_zone_file(&self.api.client()?, zone_file_contents, !self.no_delete_missing_records,).await?; + println!("Successfully synced domain: {}", domain.name); Ok(()) } } diff --git a/lib/cli/src/opts.rs b/lib/cli/src/opts.rs index d6c31e075eb..168ffe80e4c 100644 --- a/lib/cli/src/opts.rs +++ b/lib/cli/src/opts.rs @@ -95,15 +95,24 @@ impl ApiOpts { /// Formatting options for a single item. #[derive(clap::Parser, Debug, Default)] pub struct ItemFormatOpts { - /// Output format. (json, text) + /// Output format. (yaml, json, table) #[clap(short = 'f', long, default_value = "yaml")] pub format: crate::utils::render::ItemFormat, } +/// Formatting options for a single item. +#[derive(clap::Parser, Debug, Default)] +pub struct ItemTableFormatOpts { + /// Output format. (yaml, json, table) + #[clap(short = 'f', long, default_value = "table")] + pub format: crate::utils::render::ItemFormat, +} + + /// Formatting options for a list of items. #[derive(clap::Parser, Debug)] pub struct ListFormatOpts { - /// Output format. (json, text) + /// Output format. (yaml, json, table, item-table) #[clap(short = 'f', long, default_value = "table")] pub format: crate::utils::render::ListFormat, } From 52b04d580667d8ab66b12a153305ce9ddbf49bfc Mon Sep 17 00:00:00 2001 From: Ayush Jha Date: Thu, 14 Mar 2024 03:48:59 +0545 Subject: [PATCH 5/7] Fix formatting issues --- lib/backend-api/src/query.rs | 6 ++-- lib/backend-api/src/types.rs | 30 +++++++++---------- lib/cli/src/commands/domain/get.rs | 3 +- lib/cli/src/commands/domain/list.rs | 18 +++++++++--- lib/cli/src/commands/domain/mod.rs | 12 ++++---- lib/cli/src/commands/domain/zonefile.rs | 36 ++++++++++++++--------- lib/cli/src/commands/mod.rs | 2 +- lib/cli/src/opts.rs | 1 - lib/cli/src/types.rs | 38 +++++++++++-------------- 9 files changed, 76 insertions(+), 70 deletions(-) diff --git a/lib/backend-api/src/query.rs b/lib/backend-api/src/query.rs index e5be10ccf32..0db32814583 100644 --- a/lib/backend-api/src/query.rs +++ b/lib/backend-api/src/query.rs @@ -11,9 +11,9 @@ use url::Url; use crate::{ types::{ self, CreateNamespaceVars, DeployApp, DeployAppConnection, DeployAppVersion, - DeployAppVersionConnection, GetCurrentUserWithAppsVars, GetDeployAppAndVersion, + DeployAppVersionConnection, DnsDomain, GetCurrentUserWithAppsVars, GetDeployAppAndVersion, GetDeployAppVersionsVars, GetNamespaceAppsVars, Log, LogStream, PackageVersionConnection, - PublishDeployAppVars, UpsertDomainFromZoneFileVars, DnsDomain, + PublishDeployAppVars, UpsertDomainFromZoneFileVars, }, GraphQLApiFailure, WasmerClient, }; @@ -850,7 +850,6 @@ pub async fn get_domain_zone_file( Ok(opt) } - /// Retrieve a domain by its name, along with all it's records. pub async fn get_domain_with_records( client: &WasmerClient, @@ -892,7 +891,6 @@ pub async fn get_all_domains( .map(|x| x.get_all_domains) } - /// Retrieve a domain by its name. /// /// Specify with_records to also retrieve all records for the domain. diff --git a/lib/backend-api/src/types.rs b/lib/backend-api/src/types.rs index 2b59ac88345..12ec6c7aeaf 100644 --- a/lib/backend-api/src/types.rs +++ b/lib/backend-api/src/types.rs @@ -637,13 +637,13 @@ mod queries { pub owner: String, /// The tag associated with a particular app version. Uses the active /// version if not provided. - pub version: Option, + pub version: Option, /// The lower bound for log messages, in nanoseconds since the Unix /// epoch. - pub starting_from: f64, + pub starting_from: f64, /// The upper bound for log messages, in nanoseconds since the Unix /// epoch. - pub until: Option, + pub until: Option, pub first: Option, pub streams: Option>, @@ -677,7 +677,7 @@ mod queries { pub struct Log { pub message: String, /// When the message was recorded, in nanoseconds since the Unix epoch. - pub timestamp: f64, + pub timestamp: f64, } #[derive(cynic::QueryVariables, Debug)] @@ -760,7 +760,7 @@ mod queries { pub deleted_at: Option, pub name: Option, pub text: String, - pub ttl: Option, + pub ttl: Option, pub data: String, pub domain: DnsDomain, @@ -775,7 +775,7 @@ mod queries { pub deleted_at: Option, pub name: Option, pub text: String, - pub ttl: Option, + pub ttl: Option, #[cynic(rename = "type")] pub type_: DnsmanagerSshFingerprintRecordTypeChoices, pub algorithm: DnsmanagerSshFingerprintRecordAlgorithmChoices, @@ -793,7 +793,7 @@ mod queries { pub deleted_at: Option, pub name: Option, pub text: String, - pub ttl: Option, + pub ttl: Option, pub service: String, pub protocol: String, pub priority: i32, @@ -813,7 +813,7 @@ mod queries { pub deleted_at: Option, pub name: Option, pub text: String, - pub ttl: Option, + pub ttl: Option, pub mname: String, pub rname: String, pub serial: BigInt, @@ -895,7 +895,7 @@ mod queries { pub deleted_at: Option, pub name: Option, pub text: String, - pub ttl: Option, + pub ttl: Option, pub nsdname: String, pub domain: DnsDomain, @@ -952,7 +952,7 @@ mod queries { pub deleted_at: Option, pub name: Option, pub text: String, - pub ttl: Option, + pub ttl: Option, pub d_name: String, pub domain: DnsDomain, @@ -967,7 +967,7 @@ mod queries { pub deleted_at: Option, pub name: Option, pub text: String, - pub ttl: Option, + pub ttl: Option, pub c_name: String, pub domain: DnsDomain, @@ -982,7 +982,7 @@ mod queries { pub deleted_at: Option, pub name: Option, pub text: String, - pub ttl: Option, + pub ttl: Option, pub value: String, pub flags: i32, pub tag: DnsmanagerCertificationAuthorityAuthorizationRecordTagChoices, @@ -999,7 +999,7 @@ mod queries { pub deleted_at: Option, pub name: Option, pub text: String, - pub ttl: Option, + pub ttl: Option, pub address: String, pub domain: DnsDomain, } @@ -1013,7 +1013,7 @@ mod queries { pub deleted_at: Option, pub name: Option, pub text: String, - pub ttl: Option, + pub ttl: Option, pub address: String, pub domain: DnsDomain, } @@ -1225,7 +1225,6 @@ mod queries { pub zone_file: String, } - #[derive(cynic::QueryFragment, Debug, Clone, Serialize)] #[cynic(graphql_type = "DNSDomain")] pub struct DnsDomainWithRecords { @@ -1235,7 +1234,6 @@ mod queries { pub records: Option>>, } - #[derive(cynic::Scalar, Debug, Clone)] pub struct BigInt(pub u64); diff --git a/lib/cli/src/commands/domain/get.rs b/lib/cli/src/commands/domain/get.rs index 4dfce3fdc29..294113314b7 100644 --- a/lib/cli/src/commands/domain/get.rs +++ b/lib/cli/src/commands/domain/get.rs @@ -21,7 +21,8 @@ impl AsyncCliCommand for CmdDomainGet { async fn run_async(self) -> Result<(), anyhow::Error> { let client = self.api.client()?; - if let Some(domain) = wasmer_api::query::get_domain_with_records(&client, self.name).await? { + if let Some(domain) = wasmer_api::query::get_domain_with_records(&client, self.name).await? + { println!("{}", self.fmt.format.render(&domain)); } else { println!("Domain not found"); diff --git a/lib/cli/src/commands/domain/list.rs b/lib/cli/src/commands/domain/list.rs index 8e03d29ab59..4296679ccce 100644 --- a/lib/cli/src/commands/domain/list.rs +++ b/lib/cli/src/commands/domain/list.rs @@ -23,13 +23,23 @@ impl AsyncCliCommand for CmdDomainList { async fn run_async(self) -> Result<(), anyhow::Error> { let client = self.api.client()?; - let domains_connection = wasmer_api::query::get_all_domains(&client, GetAllDomainsVariables { - first: None, after: None, namespace: self.namespace, - }).await?; + let domains_connection = wasmer_api::query::get_all_domains( + &client, + GetAllDomainsVariables { + first: None, + after: None, + namespace: self.namespace, + }, + ) + .await?; let domains = domains_connection .edges .into_iter() - .map(|edge| edge.expect("domain not found").node.expect("domain not found")) + .map(|edge| { + edge.expect("domain not found") + .node + .expect("domain not found") + }) .collect::>(); println!("{}", self.fmt.format.render(&domains)); Ok(()) diff --git a/lib/cli/src/commands/domain/mod.rs b/lib/cli/src/commands/domain/mod.rs index d86e6095077..5537b2e570d 100644 --- a/lib/cli/src/commands/domain/mod.rs +++ b/lib/cli/src/commands/domain/mod.rs @@ -1,25 +1,23 @@ -pub mod list; pub mod get; +pub mod list; pub mod zonefile; use crate::commands::AsyncCliCommand; - /// Manage DNS records #[derive(clap::Subcommand, Debug)] pub enum CmdDomain { /// List domains - List(self::list::CmdDomainList), + List(self::list::CmdDomainList), /// Get a domain - Get(self::get::CmdDomainGet), + Get(self::get::CmdDomainGet), /// Get zone file for a domain - GetZoneFile(self::zonefile::CmdZoneFileGet), + GetZoneFile(self::zonefile::CmdZoneFileGet), /// Sync local zone file with remotex - SyncZoneFile(self::zonefile::CmdZoneFileSync), - + SyncZoneFile(self::zonefile::CmdZoneFileSync), } #[async_trait::async_trait] diff --git a/lib/cli/src/commands/domain/zonefile.rs b/lib/cli/src/commands/domain/zonefile.rs index 0c3355bc893..5a248be3680 100644 --- a/lib/cli/src/commands/domain/zonefile.rs +++ b/lib/cli/src/commands/domain/zonefile.rs @@ -13,11 +13,11 @@ pub struct CmdZoneFileGet { api: ApiOpts, /// Name of the domain. - domain_name: String, + domain_name: String, /// output file name to store zone file - #[clap(short='o', long="output", required = false)] - zone_file_path: Option, + #[clap(short = 'o', long = "output", required = false)] + zone_file_path: Option, } #[derive(clap::Parser, Debug)] @@ -27,37 +27,40 @@ pub struct CmdZoneFileSync { api: ApiOpts, /// filename of zone-file to sync - zone_file_path: String, + zone_file_path: String, /// Do not delete records that are not present in the zone file - #[clap(short='n', long="no-delete-missing-records", required = false, default_value = "false")] - no_delete_missing_records: bool, + #[clap( + short = 'n', + long = "no-delete-missing-records", + required = false, + default_value = "false" + )] + no_delete_missing_records: bool, } - #[async_trait::async_trait] impl AsyncCliCommand for CmdZoneFileGet { type Output = (); async fn run_async(self) -> Result<(), anyhow::Error> { let client = self.api.client()?; - if let Some(domain) = wasmer_api::query::get_domain_zone_file(&client, self.domain_name).await? { + if let Some(domain) = + wasmer_api::query::get_domain_zone_file(&client, self.domain_name).await? + { let zone_file_contents = domain.zone_file; if let Some(zone_file_path) = self.zone_file_path { std::fs::write(zone_file_path, zone_file_contents).expect("Unable to write file"); - } - else { + } else { println!("{}", zone_file_contents); } - } - else { + } else { println!("Domain not found"); } Ok(()) } } - #[async_trait::async_trait] impl AsyncCliCommand for CmdZoneFileSync { type Output = (); @@ -65,7 +68,12 @@ impl AsyncCliCommand for CmdZoneFileSync { async fn run_async(self) -> Result<(), anyhow::Error> { let data = std::fs::read(&self.zone_file_path).expect("Unable to read file"); let zone_file_contents = String::from_utf8(data).expect("Not a valid UTF-8 sequence"); - let domain = wasmer_api::query::upsert_domain_from_zone_file(&self.api.client()?, zone_file_contents, !self.no_delete_missing_records,).await?; + let domain = wasmer_api::query::upsert_domain_from_zone_file( + &self.api.client()?, + zone_file_contents, + !self.no_delete_missing_records, + ) + .await?; println!("Successfully synced domain: {}", domain.name); Ok(()) } diff --git a/lib/cli/src/commands/mod.rs b/lib/cli/src/commands/mod.rs index 14f96423bcc..18706fa9279 100644 --- a/lib/cli/src/commands/mod.rs +++ b/lib/cli/src/commands/mod.rs @@ -14,6 +14,7 @@ mod create_exe; #[cfg(feature = "static-artifact-create")] mod create_obj; pub(crate) mod deploy; +pub(crate) mod domain; #[cfg(feature = "static-artifact-create")] mod gen_c_header; mod init; @@ -22,7 +23,6 @@ mod inspect; mod journal; mod login; pub(crate) mod namespace; -pub(crate) mod domain; mod package; mod publish; mod run; diff --git a/lib/cli/src/opts.rs b/lib/cli/src/opts.rs index 168ffe80e4c..29b0689d136 100644 --- a/lib/cli/src/opts.rs +++ b/lib/cli/src/opts.rs @@ -108,7 +108,6 @@ pub struct ItemTableFormatOpts { pub format: crate::utils::render::ItemFormat, } - /// Formatting options for a list of items. #[derive(clap::Parser, Debug)] pub struct ListFormatOpts { diff --git a/lib/cli/src/types.rs b/lib/cli/src/types.rs index 1bcf770ff6a..18d1bc55981 100644 --- a/lib/cli/src/types.rs +++ b/lib/cli/src/types.rs @@ -1,14 +1,12 @@ use comfy_table::Table; -use wasmer_api::types::{DeployApp, DeployAppVersion, Namespace, DnsDomain, DnsDomainWithRecords}; +use wasmer_api::types::{DeployApp, DeployAppVersion, DnsDomain, DnsDomainWithRecords, Namespace}; use crate::utils::render::CliRender; impl CliRender for DnsDomain { fn render_item_table(&self) -> String { let mut table = Table::new(); - table.add_rows([ - vec!["Domain".to_string(), self.name.clone()], - ]); + table.add_rows([vec!["Domain".to_string(), self.name.clone()]]); table.to_string() } @@ -25,14 +23,14 @@ impl CliRender for DnsDomain { ); table.to_string() } - } impl CliRender for DnsDomainWithRecords { fn render_item_table(&self) -> String { let mut output = String::new(); let mut table = Table::new(); - table.load_preset(comfy_table::presets::UTF8_FULL_CONDENSED) + table + .load_preset(comfy_table::presets::UTF8_FULL_CONDENSED) .set_header(vec![ "Type".to_string(), "Name".to_string(), @@ -41,16 +39,17 @@ impl CliRender for DnsDomainWithRecords { ]); let mut rows: Vec> = vec![]; if let Some(ref records) = self.records { - for record in records { - if let Some(ref record) = record { - rows.push(vec![ - record.record_type().to_string(), - record.name().expect("Expected record name").to_string(), - record.ttl().expect("expected a TTL value for record").to_string(), - record.text().to_string(), - ]); - } - } + records.iter().flatten().for_each(|record| { + rows.push(vec![ + record.record_type().to_string(), + record.name().expect("Expected record name").to_string(), + record + .ttl() + .expect("expected a TTL value for record") + .to_string(), + record.text().to_string(), + ]); + }); } table.add_rows(rows); @@ -64,14 +63,9 @@ impl CliRender for DnsDomainWithRecords { } let mut table = Table::new(); table.set_header(vec!["Domain".to_string()]); - table.add_rows( - items - .iter() - .map(|ns| vec![ns.name.clone()]), - ); + table.add_rows(items.iter().map(|ns| vec![ns.name.clone()])); table.to_string() } - } impl CliRender for Namespace { From f3bfaf4cd5faa58d6eeb0766ae1458695fb06248 Mon Sep 17 00:00:00 2001 From: Ayush Jha Date: Fri, 15 Mar 2024 13:52:10 +0545 Subject: [PATCH 6/7] make lint fixes & improvements to code quality --- lib/backend-api/src/query.rs | 31 +++++++++++++------------ lib/cli/src/commands/domain/get.rs | 2 +- lib/cli/src/commands/domain/list.rs | 11 +-------- lib/cli/src/commands/domain/mod.rs | 1 - lib/cli/src/commands/domain/zonefile.rs | 17 ++++++-------- lib/cli/src/types.rs | 2 +- 6 files changed, 26 insertions(+), 38 deletions(-) diff --git a/lib/backend-api/src/query.rs b/lib/backend-api/src/query.rs index 0db32814583..7c0d60f5b73 100644 --- a/lib/backend-api/src/query.rs +++ b/lib/backend-api/src/query.rs @@ -883,12 +883,19 @@ pub async fn get_all_dns_records( pub async fn get_all_domains( client: &WasmerClient, vars: types::GetAllDomainsVariables, -) -> Result { - client +) -> Result, anyhow::Error> { + let connection = client .run_graphql_strict(types::GetAllDomains::build(vars)) .await .map_err(anyhow::Error::from) .map(|x| x.get_all_domains) + .context("no domains returned")?; + Ok(connection + .edges + .into_iter() + .flatten() + .filter_map(|x| x.node) + .collect()) } /// Retrieve a domain by its name. @@ -948,19 +955,13 @@ pub async fn upsert_domain_from_zone_file( delete_missing_records: Some(delete_missing_records), }; let res = client - .run_graphql_raw(types::UpsertDomainFromZoneFile::build(vars)) + .run_graphql_strict(types::UpsertDomainFromZoneFile::build(vars)) .await?; - if let Some(domain) = res - .data - .and_then(|d| d.upsert_domain_from_zone_file) - .map(|d| d.domain) - { - Ok(domain) - } else { - Err(GraphQLApiFailure::from_errors( - "could not sync zone file", - res.errors, - )) - } + let domain = res + .upsert_domain_from_zone_file + .context("Upserting domain from zonefile failed")? + .domain; + + Ok(domain) } diff --git a/lib/cli/src/commands/domain/get.rs b/lib/cli/src/commands/domain/get.rs index 294113314b7..81e15a29283 100644 --- a/lib/cli/src/commands/domain/get.rs +++ b/lib/cli/src/commands/domain/get.rs @@ -25,7 +25,7 @@ impl AsyncCliCommand for CmdDomainGet { { println!("{}", self.fmt.format.render(&domain)); } else { - println!("Domain not found"); + anyhow::bail!("Domain not found"); } Ok(()) } diff --git a/lib/cli/src/commands/domain/list.rs b/lib/cli/src/commands/domain/list.rs index 4296679ccce..14ba9f634db 100644 --- a/lib/cli/src/commands/domain/list.rs +++ b/lib/cli/src/commands/domain/list.rs @@ -23,7 +23,7 @@ impl AsyncCliCommand for CmdDomainList { async fn run_async(self) -> Result<(), anyhow::Error> { let client = self.api.client()?; - let domains_connection = wasmer_api::query::get_all_domains( + let domains = wasmer_api::query::get_all_domains( &client, GetAllDomainsVariables { first: None, @@ -32,15 +32,6 @@ impl AsyncCliCommand for CmdDomainList { }, ) .await?; - let domains = domains_connection - .edges - .into_iter() - .map(|edge| { - edge.expect("domain not found") - .node - .expect("domain not found") - }) - .collect::>(); println!("{}", self.fmt.format.render(&domains)); Ok(()) } diff --git a/lib/cli/src/commands/domain/mod.rs b/lib/cli/src/commands/domain/mod.rs index 5537b2e570d..b606931d7cf 100644 --- a/lib/cli/src/commands/domain/mod.rs +++ b/lib/cli/src/commands/domain/mod.rs @@ -1,7 +1,6 @@ pub mod get; pub mod list; pub mod zonefile; - use crate::commands::AsyncCliCommand; /// Manage DNS records diff --git a/lib/cli/src/commands/domain/zonefile.rs b/lib/cli/src/commands/domain/zonefile.rs index 5a248be3680..692e4cb5b3a 100644 --- a/lib/cli/src/commands/domain/zonefile.rs +++ b/lib/cli/src/commands/domain/zonefile.rs @@ -2,6 +2,7 @@ use crate::{ commands::AsyncCliCommand, opts::{ApiOpts, ItemFormatOpts}, }; +use anyhow::Context; #[derive(clap::Parser, Debug)] /// Show a zone file @@ -30,12 +31,7 @@ pub struct CmdZoneFileSync { zone_file_path: String, /// Do not delete records that are not present in the zone file - #[clap( - short = 'n', - long = "no-delete-missing-records", - required = false, - default_value = "false" - )] + #[clap(short = 'n', long = "no-delete-missing-records")] no_delete_missing_records: bool, } @@ -50,12 +46,13 @@ impl AsyncCliCommand for CmdZoneFileGet { { let zone_file_contents = domain.zone_file; if let Some(zone_file_path) = self.zone_file_path { - std::fs::write(zone_file_path, zone_file_contents).expect("Unable to write file"); + std::fs::write(zone_file_path, zone_file_contents) + .context("Unable to write file")?; } else { println!("{}", zone_file_contents); } } else { - println!("Domain not found"); + anyhow::bail!("Domain not found"); } Ok(()) } @@ -66,8 +63,8 @@ impl AsyncCliCommand for CmdZoneFileSync { type Output = (); async fn run_async(self) -> Result<(), anyhow::Error> { - let data = std::fs::read(&self.zone_file_path).expect("Unable to read file"); - let zone_file_contents = String::from_utf8(data).expect("Not a valid UTF-8 sequence"); + let data = std::fs::read(&self.zone_file_path).context("Unable to read file")?; + let zone_file_contents = String::from_utf8(data).context("Not a valid UTF-8 sequence")?; let domain = wasmer_api::query::upsert_domain_from_zone_file( &self.api.client()?, zone_file_contents, diff --git a/lib/cli/src/types.rs b/lib/cli/src/types.rs index 18d1bc55981..68897ca7468 100644 --- a/lib/cli/src/types.rs +++ b/lib/cli/src/types.rs @@ -42,7 +42,7 @@ impl CliRender for DnsDomainWithRecords { records.iter().flatten().for_each(|record| { rows.push(vec![ record.record_type().to_string(), - record.name().expect("Expected record name").to_string(), + record.name().unwrap_or("").to_string(), record .ttl() .expect("expected a TTL value for record") From c0c4233349cca4bcc83495ab7577732ffb06d048 Mon Sep 17 00:00:00 2001 From: Ayush Jha Date: Mon, 18 Mar 2024 17:27:55 +0545 Subject: [PATCH 7/7] Add subcommand `wasmer domain register` to register a new domain --- lib/backend-api/schema.graphql | 4 +-- lib/backend-api/src/query.rs | 23 +++++++++++++ lib/backend-api/src/types.rs | 20 +++++++++++ lib/cli/src/commands/domain/mod.rs | 5 +++ lib/cli/src/commands/domain/register.rs | 45 +++++++++++++++++++++++++ 5 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 lib/cli/src/commands/domain/register.rs diff --git a/lib/backend-api/schema.graphql b/lib/backend-api/schema.graphql index 1940b71e6a7..b290541a1e2 100644 --- a/lib/backend-api/schema.graphql +++ b/lib/backend-api/schema.graphql @@ -3050,8 +3050,8 @@ type RegisterDomainPayload { input RegisterDomainInput { name: String! - namespace: String! - importRecords: Boolean = false + namespace: String + importRecords: Boolean = true clientMutationId: String } diff --git a/lib/backend-api/src/query.rs b/lib/backend-api/src/query.rs index 7c0d60f5b73..5e804e72df7 100644 --- a/lib/backend-api/src/query.rs +++ b/lib/backend-api/src/query.rs @@ -865,6 +865,29 @@ pub async fn get_domain_with_records( Ok(opt) } +/// Register a new domain +pub async fn register_domain( + client: &WasmerClient, + name: String, + namespace: Option, + import_records: Option, +) -> Result { + let vars = types::RegisterDomainVars { + name, + namespace, + import_records, + }; + let opt = client + .run_graphql_strict(types::RegisterDomain::build(vars)) + .await + .map_err(anyhow::Error::from)? + .register_domain + .context("Domain registration failed")? + .domain + .context("Domain registration failed, no associatede domain found.")?; + Ok(opt) +} + /// Retrieve all DNS records. /// /// NOTE: this is a privileged operation that requires extra permissions. diff --git a/lib/backend-api/src/types.rs b/lib/backend-api/src/types.rs index 12ec6c7aeaf..da41dd07240 100644 --- a/lib/backend-api/src/types.rs +++ b/lib/backend-api/src/types.rs @@ -329,6 +329,26 @@ mod queries { pub get_deploy_app_version: Option, } + #[derive(cynic::QueryFragment, Debug)] + pub struct RegisterDomainPayload { + pub success: bool, + pub domain: Option, + } + + #[derive(cynic::QueryVariables, Debug)] + pub struct RegisterDomainVars { + pub name: String, + pub namespace: Option, + pub import_records: Option, + } + + #[derive(cynic::QueryFragment, Debug)] + #[cynic(graphql_type = "Mutation", variables = "RegisterDomainVars")] + pub struct RegisterDomain { + #[arguments(input: {name: $name, importRecords: $import_records, namespace: $namespace})] + pub register_domain: Option, + } + #[derive(cynic::QueryVariables, Debug)] pub struct UpsertDomainFromZoneFileVars { pub zone_file: String, diff --git a/lib/cli/src/commands/domain/mod.rs b/lib/cli/src/commands/domain/mod.rs index b606931d7cf..ec9de5d556d 100644 --- a/lib/cli/src/commands/domain/mod.rs +++ b/lib/cli/src/commands/domain/mod.rs @@ -1,5 +1,6 @@ pub mod get; pub mod list; +pub mod register; pub mod zonefile; use crate::commands::AsyncCliCommand; @@ -17,6 +18,9 @@ pub enum CmdDomain { /// Sync local zone file with remotex SyncZoneFile(self::zonefile::CmdZoneFileSync), + + /// Register new domain + Register(self::register::CmdDomainRegister), } #[async_trait::async_trait] @@ -29,6 +33,7 @@ impl AsyncCliCommand for CmdDomain { CmdDomain::Get(cmd) => cmd.run_async().await, CmdDomain::GetZoneFile(cmd) => cmd.run_async().await, CmdDomain::SyncZoneFile(cmd) => cmd.run_async().await, + CmdDomain::Register(cmd) => cmd.run_async().await, } } } diff --git a/lib/cli/src/commands/domain/register.rs b/lib/cli/src/commands/domain/register.rs new file mode 100644 index 00000000000..ef2d5f3782e --- /dev/null +++ b/lib/cli/src/commands/domain/register.rs @@ -0,0 +1,45 @@ +use crate::{ + commands::AsyncCliCommand, + opts::{ApiOpts, ItemTableFormatOpts}, +}; + +/// Show a domain +#[derive(clap::Parser, Debug)] +pub struct CmdDomainRegister { + #[clap(flatten)] + fmt: ItemTableFormatOpts, + #[clap(flatten)] + api: ApiOpts, + + /// Name of the domain. + name: String, + + /// owner under which the domain will live. + #[clap(long, short)] + namespace: Option, + + /// auto update DNS records for this domain. + #[clap(long, short)] + import_records: bool, +} + +#[async_trait::async_trait] +impl AsyncCliCommand for CmdDomainRegister { + type Output = (); + + async fn run_async(self) -> Result<(), anyhow::Error> { + let client = self.api.client()?; + let domain = wasmer_api::query::register_domain( + &client, + self.name, + self.namespace, + Some(self.import_records), + ) + .await?; + println!( + "{}: domain registered under owner `{}`", + domain.name, domain.owner.global_name + ); + Ok(()) + } +}