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..b290541a1e2 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,328 @@ 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! + zoneFile: String! + createdAt: DateTime! + updatedAt: DateTime! + deletedAt: DateTime + + """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 + text: String! + domain: DNSDomain! +} + +interface DNSRecordInterface { + name: String! + ttl: Int! + dnsClass: String + text: 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 + text: 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 + text: 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 + text: 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 + text: 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 + text: 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 + text: 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 + text: 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 + text: 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 + text: 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 + text: 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 + text: 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! @@ -1520,43 +1909,158 @@ type UserNotificationKindPublishedPackageVersion { packageVersion: PackageVersion! } -type UserNotificationKindIncomingNamespaceInvite { - namespaceInvite: NamespaceCollaboratorInvite! -} +type UserNotificationKindIncomingNamespaceInvite { + namespaceInvite: NamespaceCollaboratorInvite! +} + +type UserNotificationKindValidateEmail { + user: User! +} + +""" + + Enum of ways a user can login. One user can have many login methods + associated with their account. + +""" +enum LoginMethod { + GOOGLE + GITHUB + PASSWORD +} + +type SocialAuth implements Node { + """The ID of the object""" + id: ID! + user: User! + provider: String! + uid: String! + extraData: JSONString! + created: DateTime! + modified: DateTime! + username: String! +} + +type Signature { + id: ID! + publicKey: PublicKey! + data: String! + 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. -type UserNotificationKindValidateEmail { - user: User! +Can be amex, diners, discover, jcb, mastercard, unionpay, visa, or unknown. +""" +enum CardBrand { + AMEX + DINERS + DISCOVER + JCB + MASTERCARD + UNIONPAY + VISA + UNKNOWN } """ +Card funding type. - Enum of ways a user can login. One user can have many login methods - associated with their account. - +Can be credit, debit, prepaid, or unknown. """ -enum LoginMethod { - GOOGLE - GITHUB - PASSWORD +enum CardFunding { + CREDIT + DEBIT + PREPAID + UNKNOWN } -type SocialAuth implements Node { - """The ID of the object""" - id: ID! - user: User! - provider: String! - uid: String! - extraData: JSONString! - created: DateTime! - modified: DateTime! - username: String! +type Payment { + id: ID + amount: String + paidOn: DateTime } -type Signature { - id: ID! - publicKey: PublicKey! - data: String! - createdAt: 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 { @@ -1687,131 +2191,24 @@ 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 - getDeployApp(name: String!, owner: String): DeployApp + 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 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! - 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 +2278,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 +2747,11 @@ 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 + upsertDomainFromZoneFile(input: UpsertDomainFromZoneFileInput!): UpsertDomainFromZoneFilePayload + deleteDomain(input: DeleteDomainInput!): DeleteDomainPayload tokenAuth(input: ObtainJSONWebTokenInput!): ObtainJSONWebTokenPayload generateDeployToken(input: GenerateDeployTokenInput!): GenerateDeployTokenPayload verifyAccessToken(token: String): Verify @@ -2362,6 +2789,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 +3042,87 @@ input CreateRepoForAppTemplateInput { clientMutationId: String } +type RegisterDomainPayload { + success: Boolean! + domain: DNSDomain + clientMutationId: String +} + +input RegisterDomainInput { + name: String! + namespace: String + importRecords: Boolean = true + clientMutationId: String +} + +type UpsertDNSRecordPayload { + success: Boolean! + record: DNSRecord! + 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 + PTR + SOA + SRV + CAA + SSHFP +} + +input DNSMXExtraInput { + preference: Int! +} + +type DeleteDNSRecordPayload { + success: Boolean! + clientMutationId: String +} + +input DeleteDNSRecordInput { + recordId: ID! + clientMutationId: String +} + +type UpsertDomainFromZoneFilePayload { + success: Boolean! + domain: DNSDomain! + clientMutationId: String +} + +input UpsertDomainFromZoneFileInput { + zoneFile: String! + deleteMissingRecords: Boolean + clientMutationId: String +} + +type DeleteDomainPayload { + success: Boolean! + clientMutationId: String +} + +input DeleteDomainInput { + domainId: ID! + clientMutationId: String +} + type ObtainJSONWebTokenPayload { payload: GenericScalar! refreshExpiresIn: Int! @@ -3086,6 +3595,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 +3887,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..5e804e72df7 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, + PublishDeployAppVars, UpsertDomainFromZoneFileVars, }, GraphQLApiFailure, WasmerClient, }; @@ -816,6 +816,146 @@ 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::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 }; + + let opt = client + .run_graphql(types::GetDomainWithRecords::build(vars)) + .await + .map_err(anyhow::Error::from)? + .get_domain; + 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. +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 all DNS domains. +pub async fn get_all_domains( + client: &WasmerClient, + vars: types::GetAllDomainsVariables, +) -> 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. +/// +/// 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 { @@ -826,3 +966,25 @@ 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_strict(types::UpsertDomainFromZoneFile::build(vars)) + .await?; + + let domain = res + .upsert_domain_from_zone_file + .context("Upserting domain from zonefile failed")? + .domain; + + Ok(domain) +} diff --git a/lib/backend-api/src/types.rs b/lib/backend-api/src/types.rs index 6d6c8602b6b..da41dd07240 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, @@ -57,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, @@ -66,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, @@ -75,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, @@ -83,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, @@ -209,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, @@ -328,6 +329,45 @@ 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, + 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, @@ -355,12 +395,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>, } @@ -438,14 +478,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( @@ -480,13 +520,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, @@ -608,6 +648,7 @@ mod queries { pub enum LogStream { Stdout, Stderr, + Runtime, } #[derive(cynic::QueryVariables, Debug, Clone)] @@ -730,6 +771,492 @@ mod queries { pub version: Option, } + #[derive(cynic::QueryFragment, Debug, Clone, Serialize)] + #[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 text: String, + pub ttl: Option, + pub data: String, + + pub domain: DnsDomain, + } + + #[derive(cynic::QueryFragment, Debug, Clone, Serialize)] + #[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 text: String, + pub ttl: Option, + #[cynic(rename = "type")] + pub type_: DnsmanagerSshFingerprintRecordTypeChoices, + pub algorithm: DnsmanagerSshFingerprintRecordAlgorithmChoices, + pub fingerprint: String, + + pub domain: DnsDomain, + } + + #[derive(cynic::QueryFragment, Debug, Clone, Serialize)] + #[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 text: String, + 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, Serialize)] + #[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 text: String, + 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::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, + pub created_at: DateTime, + 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, Serialize)] + #[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 text: String, + pub ttl: Option, + pub nsdname: String, + + pub domain: DnsDomain, + } + + #[derive(cynic::QueryFragment, Debug, Clone, Serialize)] + #[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 text: String, + 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)] + #[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, + pub created_at: DateTime, + pub updated_at: DateTime, + pub deleted_at: Option, + pub name: Option, + pub text: String, + pub ttl: Option, + pub d_name: String, + + pub domain: DnsDomain, + } + + #[derive(cynic::QueryFragment, Debug, Clone, Serialize)] + #[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 text: String, + pub ttl: Option, + pub c_name: String, + + pub domain: DnsDomain, + } + + #[derive(cynic::QueryFragment, Debug, Clone, Serialize)] + #[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 text: String, + pub ttl: Option, + pub value: String, + pub flags: i32, + pub tag: DnsmanagerCertificationAuthorityAuthorizationRecordTagChoices, + + pub domain: DnsDomain, + } + + #[derive(cynic::QueryFragment, Debug, Clone, Serialize)] + #[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 text: String, + pub ttl: Option, + pub address: String, + pub domain: DnsDomain, + } + + #[derive(cynic::QueryFragment, Debug, Clone, Serialize)] + #[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 text: String, + pub ttl: Option, + pub address: String, + pub domain: DnsDomain, + } + + #[derive(cynic::InlineFragments, Debug, Clone, Serialize)] + #[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 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 { + 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 GetDomainWithZoneFile { + #[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, 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, 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, + pub name: String, + pub slug: String, + pub records: Option>>, + } + + #[derive(cynic::Scalar, Debug, Clone)] + pub struct BigInt(pub u64); + #[derive(cynic::InlineFragments, Debug)] pub enum Node { DeployApp(Box), diff --git a/lib/cli/src/commands/domain/get.rs b/lib/cli/src/commands/domain/get.rs new file mode 100644 index 00000000000..81e15a29283 --- /dev/null +++ b/lib/cli/src/commands/domain/get.rs @@ -0,0 +1,32 @@ +use crate::{ + commands::AsyncCliCommand, + opts::{ApiOpts, ItemTableFormatOpts}, +}; + +/// Show a domain +#[derive(clap::Parser, Debug)] +pub struct CmdDomainGet { + #[clap(flatten)] + fmt: ItemTableFormatOpts, + #[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()?; + if let Some(domain) = wasmer_api::query::get_domain_with_records(&client, self.name).await? + { + println!("{}", self.fmt.format.render(&domain)); + } else { + anyhow::bail!("Domain not found"); + } + 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..14ba9f634db --- /dev/null +++ b/lib/cli/src/commands/domain/list.rs @@ -0,0 +1,38 @@ +use wasmer_api::types::GetAllDomainsVariables; + +use crate::{ + commands::AsyncCliCommand, + opts::{ApiOpts, ListFormatOpts}, +}; + +/// List domains. +#[derive(clap::Parser, Debug)] +pub struct CmdDomainList { + #[clap(flatten)] + fmt: ListFormatOpts, + #[clap(flatten)] + api: ApiOpts, + + /// Name of the namespace. + namespace: Option, +} + +#[async_trait::async_trait] +impl AsyncCliCommand for CmdDomainList { + type Output = (); + + async fn run_async(self) -> Result<(), anyhow::Error> { + let client = self.api.client()?; + let domains = wasmer_api::query::get_all_domains( + &client, + GetAllDomainsVariables { + first: None, + after: None, + namespace: self.namespace, + }, + ) + .await?; + 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 new file mode 100644 index 00000000000..ec9de5d556d --- /dev/null +++ b/lib/cli/src/commands/domain/mod.rs @@ -0,0 +1,39 @@ +pub mod get; +pub mod list; +pub mod register; +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), + + /// Register new domain + Register(self::register::CmdDomainRegister), +} + +#[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, + 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(()) + } +} diff --git a/lib/cli/src/commands/domain/zonefile.rs b/lib/cli/src/commands/domain/zonefile.rs new file mode 100644 index 00000000000..692e4cb5b3a --- /dev/null +++ b/lib/cli/src/commands/domain/zonefile.rs @@ -0,0 +1,77 @@ +use crate::{ + commands::AsyncCliCommand, + opts::{ApiOpts, ItemFormatOpts}, +}; +use anyhow::Context; + +#[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, + + /// 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 { + #[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")] + 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? + { + 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) + .context("Unable to write file")?; + } else { + println!("{}", zone_file_contents); + } + } else { + anyhow::bail!("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).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, + !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 9ffc442e523..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; @@ -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 { diff --git a/lib/cli/src/opts.rs b/lib/cli/src/opts.rs index d6c31e075eb..29b0689d136 100644 --- a/lib/cli/src/opts.rs +++ b/lib/cli/src/opts.rs @@ -95,15 +95,23 @@ 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, } diff --git a/lib/cli/src/types.rs b/lib/cli/src/types.rs index 800a648b423..68897ca7468 100644 --- a/lib/cli/src/types.rs +++ b/lib/cli/src/types.rs @@ -1,8 +1,73 @@ use comfy_table::Table; -use wasmer_api::types::{DeployApp, DeployAppVersion, Namespace}; +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.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 { + records.iter().flatten().for_each(|record| { + rows.push(vec![ + record.record_type().to_string(), + record.name().unwrap_or("").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();