1
0
mirror of https://github.com/google/nomulus synced 2026-07-06 16:16:38 +00:00

Compare commits

...

14 Commits

Author SHA1 Message Date
Pavlo Tkach 6c138420b0 Fix console nested routes a11y (#2669) 2025-02-05 20:45:21 +00:00
Lai Jiang 08570511f5 Update GCB scripts (#2661) 2025-02-04 19:27:44 +00:00
Pavlo Tkach e62d970d34 Update console endpoints documentation (#2665) 2025-02-04 17:43:30 +00:00
Lai Jiang 067927b735 Fix GCB failures (#2664)
We start seeing failures such as this one:

https://pantheon.corp.google.com/cloud-build/builds;region=global/843b9bd7-9c09-4221-ae4c-6e2dd2918f04?inv=1&invt=Aborfg&project=domain-registry-alpha

It looks like the inclusion of gcompute-module which itself is a git
repo caused the problem. I don't understand why it wasn't an issue before.
My guess is that GCB started using a newer version of git which is more
strict about this.

TESTED=Tested the GCB build pipeline on alpha.
2025-02-04 17:12:43 +00:00
Pavlo Tkach 4ec2919ce3 Update console dependencies (#2659) 2025-01-31 21:40:37 +00:00
gbrodman 19422075fa Remove nested transactions from domain (un)locking (#2658) 2025-01-31 16:47:44 +00:00
Pavlo Tkach 40b6984ffb Improve console screen reader interaction (#2656) 2025-01-31 16:46:25 +00:00
Lai Jiang 6952e0f653 Fix a typo (#2657) 2025-01-31 02:44:28 +00:00
Lai Jiang dcb55d27bb Upload gateway related manifests to GCS (#2655) 2025-01-30 16:12:31 +00:00
Pavlo Tkach 765bd9834a Add more accessible names to the console (#2652) 2025-01-29 20:19:00 +00:00
Lai Jiang 221088e738 Upload k8s manifests to GCS (#2654) 2025-01-29 17:07:10 +00:00
gbrodman 6649e00df7 Allow for particular flows to log all SQL statements executed (#2653)
We use this now for the DomainDeleteFlow in an attempt to figure out
what statements it's running (cross-referencing that with PSQL's own
statement logging to find slow statements).
2025-01-29 16:00:19 +00:00
gbrodman 2ceb52a7c4 Handle SPECIFIED renewal price w/token in check flow (#2651)
This is kinda nonsensical because this use case is trying to apply a
single use token multiple times in the same domain:check request --
like, trying to use a single-use token for both create, renew, and
transfer while having a $0 create price and a premium renewal price.

This change doesn't affect any actual business / costs, since SPECIFIED
token renewal prices were already set on the BillingRecurrence
2025-01-28 18:31:29 +00:00
Lai Jiang 120bcc33be Update cloud build configs to build nomulus images (#2650)
Also do appropriate text replacements for each environment.
2025-01-28 16:03:26 +00:00
105 changed files with 6992 additions and 6978 deletions
+5859 -6404
View File
File diff suppressed because it is too large Load Diff
+21 -21
View File
@@ -16,31 +16,31 @@
},
"private": true,
"dependencies": {
"@angular/animations": "^18.0.2",
"@angular/cdk": "^18.0.2",
"@angular/common": "^18.0.2",
"@angular/compiler": "^18.0.2",
"@angular/core": "^18.0.2",
"@angular/forms": "^18.0.2",
"@angular/material": "^18.0.2",
"@angular/platform-browser": "^18.0.2",
"@angular/platform-browser-dynamic": "^18.0.2",
"@angular/router": "^18.0.2",
"@angular/animations": "^19.1.4",
"@angular/cdk": "^19.1.2",
"@angular/common": "^19.1.4",
"@angular/compiler": "^19.1.4",
"@angular/core": "^19.1.4",
"@angular/forms": "^19.1.4",
"@angular/material": "^19.1.2",
"@angular/platform-browser": "^19.1.4",
"@angular/platform-browser-dynamic": "^19.1.4",
"@angular/router": "^19.1.4",
"rxjs": "~7.5.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.2"
"zone.js": "~0.15.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^18.0.3",
"@angular-eslint/builder": "18.0.1",
"@angular-eslint/eslint-plugin": "18.0.1",
"@angular-eslint/eslint-plugin-template": "18.0.1",
"@angular-eslint/schematics": "18.0.1",
"@angular-eslint/template-parser": "18.0.1",
"@angular/cli": "~18.0.3",
"@angular/compiler-cli": "^18.0.2",
"@angular-devkit/build-angular": "^19.1.5",
"@angular-eslint/builder": "19.0.2",
"@angular-eslint/eslint-plugin": "19.0.2",
"@angular-eslint/eslint-plugin-template": "19.0.2",
"@angular-eslint/schematics": "19.0.2",
"@angular-eslint/template-parser": "19.0.2",
"@angular/cli": "~19.1.5",
"@angular/compiler-cli": "^19.1.4",
"@types/jasmine": "~4.0.0",
"@types/node": "^18.11.18",
"@types/node": "^18.19.74",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
"concurrently": "^7.6.0",
@@ -52,6 +52,6 @@
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.0.0",
"prettier": "2.8.7",
"typescript": "~5.4.5"
"typescript": "^5.7.3"
}
}
@@ -15,6 +15,7 @@
<mat-sidenav
[mode]="breakpointObserver.isMobileView() ? 'over' : 'side'"
[opened]="!breakpointObserver.isMobileView()"
[disableClose]="!breakpointObserver.isMobileView()"
#sidenav
class="console-app__sidebar"
>
+1
View File
@@ -24,6 +24,7 @@ import { UserDataService } from './shared/services/userData.service';
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
standalone: false,
})
export class AppComponent implements AfterViewInit {
@ViewChild(MatSidenav)
+2
View File
@@ -59,6 +59,7 @@ import { UserDataService } from './shared/services/userData.service';
import { SnackBarModule } from './snackbar.module';
import { SupportComponent } from './support/support.component';
import { TldsComponent } from './tlds/tlds.component';
import { ForceFocusDirective } from './shared/directives/forceFocus.directive';
@NgModule({
declarations: [SelectedRegistrarWrapper],
@@ -78,6 +79,7 @@ export class SelectedRegistrarModule {}
HeaderComponent,
HomeComponent,
LocationBackDirective,
ForceFocusDirective,
UserLevelVisibility,
NavigationComponent,
NewRegistrarComponent,
@@ -1,16 +1,20 @@
<app-selected-registrar-wrapper>
<h1 class="mat-headline-4">Billing Info</h1>
<h1 class="mat-headline-4" forceFocus>Billing Info</h1>
<div class="console-app__billing">
<div>
<div class="console-app__billing-subhead">
Billing records and information
</div>
<a class="text-l" href="{{ driveFolderUrl() }}" target="_blank"
<a
class="text-l"
href="{{ driveFolderUrl() }}"
target="_blank"
aria-label="View billing records on Google Drive"
>View on Google Drive</a
>
</div>
<div>
<img src="./assets/billing.png" />
<img src="./assets/billing.png" alt="Generic billing image" />
</div>
</div>
</app-selected-registrar-wrapper>
@@ -16,7 +16,7 @@
width: 100%;
}
&-subhead {
font-size: 20px;
font-size: 1.25rem;
margin-bottom: 20px;
}
}
@@ -20,6 +20,7 @@ import { MatSnackBar } from '@angular/material/snack-bar';
selector: 'app-billingInfo',
templateUrl: './billingInfo.component.html',
styleUrls: ['./billingInfo.component.scss'],
standalone: false,
})
export class BillingInfoComponent {
public static PATH = 'billingInfo';
@@ -1,6 +1,6 @@
<app-selected-registrar-wrapper>
<div class="console-app-domains">
<h1 class="mat-headline-4">Domains</h1>
<h1 class="mat-headline-4" forceFocus>Domains</h1>
<div
class="console-app-domains__actions-wrapper"
@@ -25,7 +25,11 @@
} @else {
<mat-menu #actions="matMenu">
<ng-template matMenuContent let-domainName="domainName">
<button mat-menu-item (click)="openRegistryLock(domainName)">
<button
mat-menu-item
(click)="openRegistryLock(domainName)"
aria-label="Access registry lock for domain"
>
<mat-icon>key</mat-icon>
<span>Registry Lock</span>
</button>
@@ -77,6 +81,7 @@
<button
mat-flat-button
aria-label="Delete Selected Domains"
[attr.aria-hidden]="!selection.hasValue()"
(click)="deleteSelectedDomains()"
>
Delete Selected Domains
@@ -48,6 +48,7 @@ interface DomainData {
<button mat-button (click)="onClose()">Close</button>
</mat-dialog-actions>
`,
standalone: false,
})
export class ResponseDialogComponent {
constructor(
@@ -79,6 +80,7 @@ export class ResponseDialogComponent {
</button>
</mat-dialog-actions>
`,
standalone: false,
})
export class ReasonDialogComponent {
reason: string = '';
@@ -102,6 +104,7 @@ export class ReasonDialogComponent {
selector: 'app-domain-list',
templateUrl: './domainList.component.html',
styleUrls: ['./domainList.component.scss'],
standalone: false,
})
export class DomainListComponent {
public static PATH = 'domain-list';
@@ -49,6 +49,7 @@
color="primary"
type="submit"
[disabled]="!unlockDomain.valid"
aria-label="Submit domain unlock request"
>
Save
</button>
@@ -73,6 +74,7 @@
color="primary"
type="submit"
[disabled]="!lockDomain.valid"
aria-label="Submit domain lock request"
>
Save
</button>
@@ -25,6 +25,7 @@ import { RegistryLockService } from './registryLock.service';
selector: 'app-registry-lock',
templateUrl: './registryLock.component.html',
styleUrls: ['./registryLock.component.scss'],
standalone: false,
})
export class RegistryLockComponent {
readonly isLocked = computed(() =>
@@ -2,7 +2,7 @@
<mat-toolbar>
<button
mat-icon-button
aria-label="Open menu"
aria-label="Open navigation menu"
(click)="toggleNavPane()"
*ngIf="breakpointObserver.isMobileView()"
class="console-app__menu-btn"
@@ -19,6 +19,7 @@ import { BreakPointObserverService } from '../shared/services/breakPoint.service
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.scss'],
standalone: false,
})
export class HeaderComponent {
private isNavOpen = false;
@@ -25,6 +25,7 @@ import { BreakPointObserverService } from '../shared/services/breakPoint.service
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss'],
standalone: false,
})
export class HomeComponent {
constructor(
@@ -3,7 +3,7 @@
margin-top: 30px;
}
&-subhead {
font-size: 20px;
font-size: 1.25rem;
margin-bottom: 20px;
}
}
@@ -25,6 +25,7 @@ import { DomainListComponent } from '../domains/domainList.component';
templateUrl: './registryLockVerify.component.html',
styleUrls: ['./registryLockVerify.component.scss'],
providers: [RegistryLockVerifyService],
standalone: false,
})
export class RegistryLockVerifyComponent {
public static PATH = 'registry-lock-verify';
@@ -20,6 +20,8 @@
<mat-nested-tree-node
*matTreeNodeDef="let node; when: hasChild"
(click)="onClick(node)"
tabindex="0"
(keyup.enter)="onClick(node)"
>
<div class="mat-tree-node" [class.active]="router.url.includes(node.path)">
<button
@@ -33,6 +33,7 @@ interface NavMenuNode extends RouteWithIcon {
selector: 'app-navigation',
templateUrl: './navigation.component.html',
styleUrls: ['./navigation.component.scss'],
standalone: false,
})
export class NavigationComponent {
renderRouter: boolean = true;
@@ -30,7 +30,14 @@
>
</mat-form-field>
</p>
<button mat-flat-button color="primary" type="submit">Save</button>
<button
mat-flat-button
color="primary"
type="submit"
aria-label="Submit new OT&E account"
>
Save
</button>
</form>
}
</div>
@@ -26,7 +26,6 @@ export interface OteCreateResponse extends Map<string, string> {
@Component({
selector: 'app-ote',
standalone: true,
imports: [MaterialModule, SnackBarModule],
templateUrl: './newOte.component.html',
styleUrls: ['./newOte.component.scss'],
@@ -31,7 +31,6 @@ export interface OteStatusResponse {
@Component({
selector: 'app-ote-status',
standalone: true,
imports: [MaterialModule, SnackBarModule, CommonModule],
templateUrl: './oteStatus.component.html',
styleUrls: ['./oteStatus.component.scss'],
@@ -175,6 +175,7 @@ JPY=billing-id-for-yen"
mat-flat-button
color="primary"
type="submit"
aria-label="Submit new registrar request"
>
Save
</button>
@@ -33,6 +33,7 @@ interface LocalizedAddressStreet {
templateUrl: './newRegistrar.component.html',
styleUrls: ['./newRegistrar.component.scss'],
encapsulation: ViewEncapsulation.None,
standalone: false,
})
export default class NewRegistrarComponent {
protected newRegistrar: Registrar;
@@ -16,7 +16,7 @@
*ngIf="oteButtonVisible"
mat-stroked-button
(click)="checkOteStatus()"
aria-label="Check OT&E account"
aria-label="Check OT&E account status"
[elementId]="getElementIdForOteBlock()"
>
Check OT&E Status
@@ -27,6 +27,7 @@ import { environment } from '../../environments/environment';
selector: 'app-registrar-details',
templateUrl: './registrarDetails.component.html',
styleUrls: ['./registrarDetails.component.scss'],
standalone: false,
})
export class RegistrarDetailsComponent implements OnInit {
public static PATH = 'registrars/:id';
@@ -19,6 +19,7 @@ import { RegistrarService } from './registrar.service';
selector: 'app-registrar-selector',
templateUrl: './registrarSelector.component.html',
styleUrls: ['./registrarSelector.component.scss'],
standalone: false,
})
export class RegistrarSelectorComponent {
registrarInput = signal<string>(this.registrarService.registrarId());
@@ -3,7 +3,7 @@
} @else {
<div class="console-app__registrars">
<div class="console-app__registrars-header">
<h1 class="mat-headline-4">Registrars</h1>
<h1 class="mat-headline-4" forceFocus>Registrars</h1>
<div class="spacer"></div>
<button
mat-stroked-button
@@ -78,6 +78,7 @@ export const columns = [
templateUrl: './registrarsTable.component.html',
styleUrls: ['./registrarsTable.component.scss'],
encapsulation: ViewEncapsulation.None,
standalone: false,
})
export class RegistrarComponent {
public static PATH = 'registrars';
@@ -1,4 +1,4 @@
<h1 class="mat-headline-4">Resources</h1>
<h1 class="mat-headline-4" forceFocus>Resources</h1>
<div class="console-app__resources">
<div>
<div class="console-app__resources-subhead">Technical resources</div>
@@ -11,6 +11,6 @@
>
</div>
<div>
<img src="./assets/resources.png" />
<img src="./assets/resources.png" alt="Generic resources image" />
</div>
</div>
@@ -16,7 +16,7 @@
width: 100%;
}
&-subhead {
font-size: 20px;
font-size: 1.25rem;
margin-bottom: 20px;
}
}
@@ -19,6 +19,7 @@ import { UserDataService } from '../shared/services/userData.service';
selector: 'app-resources',
templateUrl: './resources.component.html',
styleUrls: ['./resources.component.scss'],
standalone: false,
})
export class ResourcesComponent {
public static PATH = 'resources';
@@ -27,6 +27,7 @@ import {
templateUrl: './contact.component.html',
styleUrls: ['./contact.component.scss'],
encapsulation: ViewEncapsulation.None,
standalone: false,
})
export default class ContactComponent {
public static PATH = 'contact';
@@ -128,6 +128,7 @@
mat-flat-button
color="primary"
type="submit"
aria-label="Save contact updates"
>
Save
</button>
@@ -27,6 +27,7 @@ import {
selector: 'app-contact-details',
templateUrl: './contactDetails.component.html',
styleUrls: ['./contactDetails.component.scss'],
standalone: false,
})
export class ContactDetailsComponent {
protected contactTypeToTextMap = contactTypeToTextMap;
@@ -33,6 +33,7 @@ type errorFriendlyText = { [type in errorCode]: String };
selector: 'app-epp-password-edit',
templateUrl: './eppPasswordEdit.component.html',
styleUrls: ['./eppPasswordEdit.component.scss'],
standalone: false,
})
export default class EppPasswordEditComponent {
MIN_MAX_LENGHT = new String(
@@ -23,6 +23,7 @@ import { SecurityService, apiToUiConverter } from './security.service';
selector: 'app-security',
templateUrl: './security.component.html',
styleUrls: ['./security.component.scss'],
standalone: false,
})
export default class SecurityComponent {
public static PATH = 'security';
@@ -23,7 +23,7 @@
<button
matSuffix
mat-icon-button
aria-label="Remove"
[attr.aria-label]="'Remove IP entry ' + ip.value"
(click)="removeIpEntry(ip)"
[disabled]="isUpdating"
>
@@ -36,6 +36,7 @@
[disabled]="isUpdating"
color="primary"
(click)="createIpEntry()"
aria-label="Add new IP address"
type="button"
>
+ Add IP
@@ -26,6 +26,7 @@ import { SecurityService, apiToUiConverter } from './security.service';
selector: 'app-security-edit',
templateUrl: './securityEdit.component.html',
styleUrls: ['./securityEdit.component.scss'],
standalone: false,
})
export default class SecurityEditComponent {
dataSource: SecuritySettings = {};
@@ -1,6 +1,6 @@
<app-selected-registrar-wrapper>
<div class="console-settings">
<h1 class="mat-headline-4">Settings</h1>
<h1 class="mat-headline-4" forceFocus>Settings</h1>
<nav
mat-tab-nav-bar
mat-stretch-tabs="false"
@@ -14,6 +14,7 @@
queryParamsHandling="merge"
#rla="routerLinkActive"
[active]="rla.isActive"
aria-label="Access contacts settings"
>Contacts</a
>
<a
@@ -23,6 +24,7 @@
queryParamsHandling="merge"
#rla2="routerLinkActive"
[active]="rla2.isActive"
aria-label="Access whois settings"
>WHOIS Info</a
>
<a
@@ -32,6 +34,7 @@
queryParamsHandling="merge"
#rla3="routerLinkActive"
[active]="rla3.isActive"
aria-label="Access security settings"
>Security</a
>
</nav>
@@ -19,6 +19,7 @@ import { Component, ViewEncapsulation } from '@angular/core';
templateUrl: './settings.component.html',
styleUrls: ['./settings.component.scss'],
encapsulation: ViewEncapsulation.None,
standalone: false,
})
export class SettingsComponent {
public static PATH = 'settings';
@@ -21,6 +21,7 @@ import { WhoisService } from './whois.service';
selector: 'app-whois',
templateUrl: './whois.component.html',
styleUrls: ['./whois.component.scss'],
standalone: false,
})
export default class WhoisComponent {
public static PATH = 'whois';
@@ -149,7 +149,14 @@
</mat-form-field>
}
<button mat-flat-button color="primary" type="submit">Save</button>
<button
mat-flat-button
color="primary"
type="submit"
aria-label="Save WHOIS settings"
>
Save
</button>
</form>
</div>
</div>
@@ -26,6 +26,7 @@ import { WhoisService } from './whois.service';
selector: 'app-whois-edit',
templateUrl: './whoisEdit.component.html',
styleUrls: ['./whoisEdit.component.scss'],
standalone: false,
})
export default class WhoisEditComponent {
registrarInEdit: Registrar | undefined;
@@ -24,6 +24,7 @@ interface Notification {
selector: 'app-notifications',
templateUrl: './notifications.component.html',
styleUrls: ['./notifications.component.scss'],
standalone: false,
})
export class NotificationsComponent {
protected mockNotifications: Notification[] = [
@@ -31,6 +31,7 @@ import { RegistrarService } from 'src/app/registrar/registrar.service';
</div>
}
`,
standalone: false,
})
export class SelectedRegistrarWrapper {
constructor(protected registrarService: RegistrarService) {}
@@ -0,0 +1,31 @@
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Directive, ElementRef, effect } from '@angular/core';
@Directive({
selector: '[forceFocus]',
standalone: false,
})
export class ForceFocusDirective {
constructor(private el: ElementRef) {
effect(this.processElement.bind(this));
}
processElement() {
this.el.nativeElement.tabIndex = '1';
this.el.nativeElement.focus();
this.el.nativeElement.tabIndex = '-1';
}
}
@@ -17,6 +17,7 @@ import { Directive, HostListener } from '@angular/core';
@Directive({
selector: '[backButton]',
standalone: false,
})
export class LocationBackDirective {
constructor(private location: Location) {}
@@ -35,6 +35,7 @@ export const DISABLED_ELEMENTS_PER_ROLE = {
@Directive({
selector: '[elementId]',
standalone: false,
})
export class UserLevelVisibility {
@Input() elementId!: RESTRICTED_ELEMENTS | null;
@@ -10,7 +10,10 @@
For help with OT&E sandbox and certification, or new technical requirements
for any of our new TLD launches.
</p>
<a class="text-l" href="mailto:registry-integration@google.com"
<a
class="text-l"
href="mailto:registry-integration@google.com"
aria-label="Email us with OT&E sandbox/certification or new TLD technical requirements questions."
>registry-integration&#64;google.com</a
>
<p class="text-l">
@@ -19,6 +22,7 @@
</p>
<a
class="text-l"
aria-label="Email support with general purpose questions."
href="mailto:{{ userDataService.userData()?.supportEmail }}"
>{{ userDataService.userData()?.supportEmail }}</a
>
@@ -19,6 +19,7 @@ import { UserDataService } from '../shared/services/userData.service';
selector: 'app-support',
templateUrl: './support.component.html',
styleUrls: ['./support.component.scss'],
standalone: false,
})
export class SupportComponent {
public static PATH = 'support';
@@ -18,5 +18,6 @@ import { Component } from '@angular/core';
selector: 'app-tlds',
templateUrl: './tlds.component.html',
styleUrls: ['./tlds.component.scss'],
standalone: false,
})
export class TldsComponent {}
@@ -90,11 +90,12 @@
<input
[type]="isPasswordVisible ? 'text' : 'password'"
[value]="userDetails().password"
disabled
aria-label="Password field"
readonly
/>
<button
mat-button
aria-label="Show password"
aria-hidden="true"
(click)="isPasswordVisible = !isPasswordVisible"
>
{{ isPasswordVisible ? "Hide" : "View" }} password
@@ -14,6 +14,7 @@
.console-app {
&__user-details {
max-width: 616px;
&-controls {
display: flex;
align-items: center;
@@ -34,6 +35,5 @@
border: 1px solid #ddd;
border-radius: 10px;
}
max-width: 616px;
}
}
@@ -27,7 +27,6 @@ import { UserEditFormComponent } from './userEditForm.component';
selector: 'app-user-edit',
templateUrl: './userDetails.component.html',
styleUrls: ['./userDetails.component.scss'],
standalone: true,
imports: [
FormsModule,
MaterialModule,
@@ -33,7 +33,13 @@
</mat-select>
</mat-form-field>
</p>
<button mat-flat-button color="primary" aria-label="Save user" type="submit">
<button
mat-flat-button
color="primary"
aria-label="Save user"
type="submit"
aria-label="Save changes to the user"
>
Save
</button>
</form>
@@ -29,19 +29,17 @@ import { User } from './users.service';
selector: 'app-user-edit-form',
templateUrl: './userEditForm.component.html',
styleUrls: ['./userEditForm.component.scss'],
standalone: true,
imports: [FormsModule, MaterialModule, CommonModule],
providers: [],
})
export class UserEditFormComponent {
@ViewChild('form') form!: ElementRef;
isNew = input<boolean>(false);
user = input<User>(
user = input<User, User>(
{
emailAddress: '',
role: 'ACCOUNT_MANAGER',
},
// @ts-ignore - legit option, typescript fails to match it to a proper type
{ transform: (user: User) => structuredClone(user) }
);
@@ -1,10 +1,10 @@
<app-selected-registrar-wrapper cdkTrapFocus [cdkTrapFocusAutoCapture]="true">
<app-selected-registrar-wrapper>
@if(isLoading) {
<div class="console-app__users-spinner">
<mat-spinner />
</div>
} @else if(selectingExistingUser) {
<div class="console-app__users">
<div class="console-app__users" cdkTrapFocus [cdkTrapFocusAutoCapture]="true">
<h1 class="mat-headline-4">Add existing user</h1>
<p>
<button
@@ -58,24 +58,32 @@
}
</div>
} @else if(usersService.currentlyOpenUserEmail()) {
<app-user-edit></app-user-edit>
<div cdkTrapFocus [cdkTrapFocusAutoCapture]="true">
<app-user-edit></app-user-edit>
</div>
} @else if(isNew) {
<h1 class="mat-headline-4">New User Form</h1>
<div class="spacer"></div>
<p>
<button
mat-icon-button
aria-label="Back to users list"
(click)="isNew = false"
>
<mat-icon>arrow_back</mat-icon>
</button>
</p>
<app-user-edit-form [isNew]="true" (onEditComplete)="createNewUser($event)" />
<div cdkTrapFocus [cdkTrapFocusAutoCapture]="true">
<h1 class="mat-headline-4">New User Form</h1>
<div class="spacer"></div>
<p>
<button
mat-icon-button
aria-label="Back to users list"
(click)="isNew = false"
>
<mat-icon>arrow_back</mat-icon>
</button>
</p>
<app-user-edit-form
[isNew]="true"
(onEditComplete)="createNewUser($event)"
/>
</div>
} @else {
<div class="console-app__users">
<div class="console-app__users-header">
<h1 class="mat-headline-4">Users</h1>
<h1 class="mat-headline-4" forceFocus>Users</h1>
<div class="spacer"></div>
<div class="console-app__users-header-buttons">
<button
@@ -88,12 +96,7 @@
<mat-icon>add</mat-icon>
Add existing user
</button>
<button
mat-flat-button
(click)="isNew = true"
aria-label="Create new user"
color="primary"
>
<button mat-flat-button (click)="isNew = true" color="primary">
Create New User
</button>
</div>
@@ -32,7 +32,6 @@ import { UserEditFormComponent } from './userEditForm.component';
selector: 'app-users',
templateUrl: './users.component.html',
styleUrls: ['./users.component.scss'],
standalone: true,
imports: [
FormsModule,
MaterialModule,
@@ -43,7 +43,6 @@ export const columns = [
selector: 'app-users-list',
templateUrl: './usersList.component.html',
styleUrls: ['./usersList.component.scss'],
standalone: true,
imports: [MaterialModule, CommonModule],
providers: [],
})
+3 -3
View File
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
@use "@angular/material" as mat;
@import "app/registrar/registrarSelector.component.scss";
@use "app/registrar/registrarSelector.component.scss";
html,
body {
@@ -55,13 +55,13 @@ body {
font-weight: bold;
width: var(--list-item-title-width);
display: inline-block;
font-size: 14px;
font-size: 0.85rem;
white-space: pre-line;
color: #202124;
flex-shrink: 0;
}
.console-app__list-value {
font-size: 14px;
font-size: 0.85rem;
white-space: pre-line;
word-break: break-word;
color: var(--text);
+4 -4
View File
@@ -1,7 +1,6 @@
@use "sass:map";
@use "sass:math";
@use "@angular/material" as mat;
@use "@material/textfield";
$secondary-color: #80868b;
$border-color: #dadce0;
@@ -13,7 +12,8 @@ $border-color: #dadce0;
// Include the common styles for Angular Material. We include this here so that you only
// have to load a single css file for Angular Material in your app.
// Be sure that you only ever include this mixin once!
@include mat.core();
@include mat.elevation-classes();
@include mat.app-background();
$typographyConfig: mat.m2-define-typography-config(
$headline-1:
@@ -83,10 +83,10 @@ $typographyConfig: mat.m2-define-typography-config(
);
.text-xl {
font-size: 18px;
font-size: 1.1rem;
}
.text-l {
font-size: 16px;
font-size: 1rem;
}
mat-row:nth-child(odd) {
@@ -147,6 +147,13 @@ public class FlowModule {
.map(IsolationLevel::value);
}
@Provides
@FlowScope
@LogSqlStatements
boolean provideShouldLogSqlStatements(Class<? extends Flow> flowClass) {
return SqlStatementLoggingFlow.class.isAssignableFrom(flowClass);
}
@Provides
@FlowScope
@Superuser
@@ -370,4 +377,9 @@ public class FlowModule {
@Qualifier
@Documented
public @interface Transactional {}
/** Dagger qualifier for if we should log all SQL statements in a flow. */
@Qualifier
@Documented
public @interface LogSqlStatements {}
}
@@ -19,6 +19,7 @@ import static google.registry.xml.XmlTransformer.prettyPrint;
import com.google.common.flogger.FluentLogger;
import google.registry.flows.FlowModule.DryRun;
import google.registry.flows.FlowModule.InputXml;
import google.registry.flows.FlowModule.LogSqlStatements;
import google.registry.flows.FlowModule.RegistrarId;
import google.registry.flows.FlowModule.Superuser;
import google.registry.flows.FlowModule.Transactional;
@@ -49,6 +50,7 @@ public class FlowRunner {
@Inject @DryRun boolean isDryRun;
@Inject @Superuser boolean isSuperuser;
@Inject @Transactional boolean isTransactional;
@Inject @LogSqlStatements boolean logSqlStatements;
@Inject SessionMetadata sessionMetadata;
@Inject Trid trid;
@Inject FlowReporter flowReporter;
@@ -97,7 +99,8 @@ public class FlowRunner {
} catch (EppException e) {
throw new EppRuntimeException(e);
}
});
},
logSqlStatements);
} catch (DryRunException e) {
return e.output;
} catch (EppRuntimeException e) {
@@ -0,0 +1,24 @@
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.flows;
/**
* Interface for a {@link Flow} that logs its SQL statements when running transactionally.
*
* <p>We don't wish to log all SQL statements ever executed (that'll create too much log bloat) but
* for some flows and some occasions we may wish to know precisely what SQL statements are being
* run.
*/
public interface SqlStatementLoggingFlow extends TransactionalFlow {}
@@ -55,6 +55,7 @@ import google.registry.flows.FlowModule.Superuser;
import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.MutatingFlow;
import google.registry.flows.SessionMetadata;
import google.registry.flows.SqlStatementLoggingFlow;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.flows.custom.DomainDeleteFlowCustomLogic;
import google.registry.flows.custom.DomainDeleteFlowCustomLogic.AfterValidationParameters;
@@ -117,7 +118,7 @@ import org.joda.time.Duration;
* @error {@link DomainFlowUtils.NotAuthorizedForTldException}
*/
@ReportingSpec(ActivityReportField.DOMAIN_DELETE)
public final class DomainDeleteFlow implements MutatingFlow {
public final class DomainDeleteFlow implements MutatingFlow, SqlStatementLoggingFlow {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -287,6 +287,9 @@ public final class DomainPricingLogic {
|| token.getRenewalPriceBehavior().equals(RenewalPriceBehavior.NONPREMIUM)) {
return tld.getStandardRenewCost(dateTime).multipliedBy(years);
}
if (token.getRenewalPriceBehavior().equals(RenewalPriceBehavior.SPECIFIED)) {
return token.getRenewalPrice().get();
}
}
return getDomainCostWithDiscount(
domainPrices.isPremium(),
@@ -21,6 +21,7 @@ import jakarta.persistence.Query;
import jakarta.persistence.TypedQuery;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.metamodel.Metamodel;
import java.util.concurrent.Callable;
/** Sub-interface of {@link TransactionManager} which defines JPA related methods. */
public interface JpaTransactionManager extends TransactionManager {
@@ -92,4 +93,15 @@ public interface JpaTransactionManager extends TransactionManager {
/** Return the {@link TransactionIsolationLevel} used in the current transaction. */
TransactionIsolationLevel getCurrentTransactionIsolationLevel();
/** Executes the work with the given isolation level, possibly logging all SQL statements used. */
<T> T transact(
TransactionIsolationLevel isolationLevel, Callable<T> work, boolean logSqlStatements);
/**
* Executes the work with the given isolation level without retry, possibly logging all SQL
* statements used.
*/
<T> T transactNoRetry(
TransactionIsolationLevel isolationLevel, Callable<T> work, boolean logSqlStatements);
}
@@ -78,6 +78,7 @@ import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import javax.annotation.Nullable;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Environment;
import org.joda.time.DateTime;
@@ -89,6 +90,7 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
private static final String NESTED_TRANSACTION_MESSAGE =
"Nested transaction detected. Try refactoring to avoid nested transactions. If unachievable,"
+ " use reTransact() in nested transactions";
private static final String SQL_STATEMENT_LOG_SENTINEL_FORMAT = "SQL_STATEMENT_LOG: %s";
// EntityManagerFactory is thread safe.
private final EntityManagerFactory emf;
@@ -174,6 +176,12 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
@Override
public <T> T transact(TransactionIsolationLevel isolationLevel, Callable<T> work) {
return transact(isolationLevel, work, false);
}
@Override
public <T> T transact(
TransactionIsolationLevel isolationLevel, Callable<T> work, boolean logSqlStatements) {
if (inTransaction()) {
if (!getHibernateAllowNestedTransactions()) {
throw new IllegalStateException(NESTED_TRANSACTION_MESSAGE);
@@ -186,7 +194,8 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
return transactNoRetry(isolationLevel, work);
}
return retrier.callWithRetry(
() -> transactNoRetry(isolationLevel, work), JpaRetries::isFailedTxnRetriable);
() -> transactNoRetry(isolationLevel, work, logSqlStatements),
JpaRetries::isFailedTxnRetriable);
}
@Override
@@ -202,6 +211,14 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
@Override
public <T> T transactNoRetry(
@Nullable TransactionIsolationLevel isolationLevel, Callable<T> work) {
return transactNoRetry(isolationLevel, work, false);
}
@Override
public <T> T transactNoRetry(
@Nullable TransactionIsolationLevel isolationLevel,
Callable<T> work,
boolean logSqlStatements) {
if (inTransaction()) {
// This check will no longer be necessary when the transact() method always throws
// inside a nested transaction, as the only way to pass a non-null isolation level
@@ -224,7 +241,18 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
}
}
TransactionInfo txnInfo = transactionInfo.get();
txnInfo.entityManager = emf.createEntityManager();
txnInfo.entityManager =
logSqlStatements
? emf.unwrap(SessionFactory.class)
.withOptions()
.statementInspector(
s -> {
logger.atInfo().log(SQL_STATEMENT_LOG_SENTINEL_FORMAT, s);
return s;
})
.openSession()
: emf.createEntityManager();
if (readOnly) {
// Disable Hibernate's dirty object check on flushing, it has become more aggressive in v6.
txnInfo.entityManager.unwrap(Session.class).setDefaultReadOnly(true);
@@ -138,7 +138,7 @@ public final class DomainLockUtils {
createLockBuilder(domainName, registrarId, registrarPocId, isAdmin)
.setLockCompletionTime(now)
.build());
tm().transact(() -> applyLockStatuses(newLock, now, isAdmin));
applyLockStatuses(newLock, now, isAdmin);
setAsRelock(newLock);
return newLock;
});
@@ -160,7 +160,7 @@ public final class DomainLockUtils {
createUnlockBuilder(domainName, registrarId, isAdmin, relockDuration)
.setUnlockCompletionTime(now)
.build());
tm().transact(() -> removeLockStatuses(result, isAdmin, now));
removeLockStatuses(result, isAdmin, now);
return result;
});
// Submit relock outside the transaction to make sure that it fully succeeded
@@ -16,11 +16,9 @@ package google.registry.tools;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.collect.Iterables.partition;
import static google.registry.model.eppcommon.StatusValue.SERVER_DELETE_PROHIBITED;
import static google.registry.model.eppcommon.StatusValue.SERVER_TRANSFER_PROHIBITED;
import static google.registry.model.eppcommon.StatusValue.SERVER_UPDATE_PROHIBITED;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.CollectionUtils.findDuplicates;
import com.beust.jcommander.Parameter;
@@ -38,8 +36,6 @@ public abstract class LockOrUnlockDomainCommand extends ConfirmingCommand {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final int BATCH_SIZE = 10;
public static final ImmutableSet<StatusValue> REGISTRY_LOCK_STATUSES =
ImmutableSet.of(
SERVER_DELETE_PROHIBITED, SERVER_TRANSFER_PROHIBITED, SERVER_UPDATE_PROHIBITED);
@@ -79,26 +75,16 @@ public abstract class LockOrUnlockDomainCommand extends ConfirmingCommand {
protected String execute() {
ImmutableSet.Builder<String> successfulDomainsBuilder = new ImmutableSet.Builder<>();
ImmutableMap.Builder<String, String> failedDomainsToReasons = new ImmutableMap.Builder<>();
partition(getDomains(), BATCH_SIZE)
.forEach(
batch ->
// we require that the jpaTm is the outer transaction in DomainLockUtils
tm().transact(
() ->
tm().transact(
() -> {
for (String domain : batch) {
try {
createAndApplyRequest(domain);
} catch (Throwable t) {
logger.atSevere().withCause(t).log(
"Error when (un)locking domain %s.", domain);
failedDomainsToReasons.put(domain, t.getMessage());
continue;
}
successfulDomainsBuilder.add(domain);
}
})));
for (String domain : getDomains()) {
try {
createAndApplyRequest(domain);
} catch (Throwable t) {
logger.atSevere().withCause(t).log("Error when (un)locking domain %s.", domain);
failedDomainsToReasons.put(domain, t.getMessage());
continue;
}
successfulDomainsBuilder.add(domain);
}
ImmutableSet<String> successfulDomains = successfulDomainsBuilder.build();
ImmutableSet<String> failedDomains =
failedDomainsToReasons.build().entrySet().stream()
@@ -56,7 +56,7 @@ import org.junit.jupiter.api.extension.RegisterExtension;
public abstract class ResourceFlowTestCase<F extends Flow, R extends EppResource>
extends FlowTestCase<F> {
private final TestLogHandler logHandler = new TestLogHandler();
protected final TestLogHandler logHandler = new TestLogHandler();
@RegisterExtension
public final TestCacheExtension testCacheExtension =
@@ -102,8 +102,10 @@ import google.registry.model.transfer.TransferResponse;
import google.registry.model.transfer.TransferStatus;
import google.registry.testing.CloudTasksHelper.TaskMatcher;
import google.registry.testing.DatabaseHelper;
import google.registry.testing.LogsSubject;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Level;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.joda.time.Duration;
@@ -1258,4 +1260,17 @@ class DomainDeleteFlowTest extends ResourceFlowTestCase<DomainDeleteFlow, Domain
runFlowAssertResponse(loadFile("domain_delete_response_pending.xml"));
assertPollMessages();
}
@Test
void testSuccess_logsSqlStatements() throws Exception {
setUpSuccessfulTest();
runFlowAssertResponse(loadFile("domain_delete_response_pending.xml"));
LogsSubject.assertAboutLogs()
.that(logHandler)
.hasLogAtLevelWithMessage(
Level.INFO,
"SQL_STATEMENT_LOG: insert into \"DomainHistory\" (history_by_superuser,"
+ "history_registrar_id,history_modification_time,history_other_registrar_id,"
+ "history_period_unit,history_period_value,history_reason,history");
}
}
@@ -1242,4 +1242,23 @@ public class DomainPricingLogicTest {
.addFeeOrCredit(Fee.create(new BigDecimal("120.00"), CREATE, true))
.build());
}
@Test
void testDomainRenewPrice_specifiedToken() throws Exception {
AllocationToken allocationToken =
persistResource(
new AllocationToken.Builder()
.setToken("abc123")
.setTokenType(SINGLE_USE)
.setDomainName("premium.example")
.setRenewalPriceBehavior(SPECIFIED)
.setRenewalPrice(Money.of(USD, 5))
.build());
assertThat(
domainPricingLogic
.getRenewPrice(
tld, "premium.example", clock.nowUtc(), 1, null, Optional.of(allocationToken))
.getRenewCost())
.isEqualTo(Money.of(USD, 5));
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 74 KiB

+544 -286
View File
@@ -1,349 +1,607 @@
{
"swagger": "2.0",
"info": {
"title": "Registry Console API",
"version": "1.0.0"
"version": "1.0.0",
"title": "Console API",
"description": "API for managing registrars, user data, registry locks, domain lists, users, and contact settings."
},
"basePath": "/console-api",
"schemes": [
"https"
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"paths": {
"/console-api/user": {
"/registrars": {
"get": {
"summary": "Provides user type to UI",
"responses": {
"200": {
"schema": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum":[
"ADMIN",
"USER"
]
}
}
}
}
}
}
},
"/console-api/poll": {
"post": {
"summary": "Every T minutes (1?) request/response to keep front-end in sync with the back-end",
"parameters": [{
"name": "version",
"in": "body",
"required": true,
"type": "string",
"format": "string"
}, {
"name": "registrarId",
"in": "body",
"required": false,
"type": "string",
"format": "string"
}, {
"name": "notificationId",
"in": "body",
"required": false,
"type": "string",
"format": "string"
}],
"summary": "Retrieve a list of registrars",
"produces": [
"application/json"
],
"responses": {
"200": {
"description": "Successful retrieval of registrars.",
"schema": {
"type": "array",
"items": {
"type": "object",
"properties": {
"notificationText": {
"type": "string"
},
"notificationId": {
"type": "string"
},
"dismissable": {
"type": "boolean"
}
}
"$ref": "#/definitions/Registrar"
}
}
}
}
}
},
"/console-api/registrars": {
"/userdata": {
"get": {
"summary": "Provides list of available registrars to UI",
"summary": "Retrieve user data",
"produces": [
"application/json"
],
"responses": {
"200": {
"description": "Successful retrieval of user data.",
"schema": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"registrarId": {
"type": "string"
}
}
}
"$ref": "#/definitions/UserData"
}
}
}
}
},
"/console-api/domains": {
"/registry-lock": {
"get": {
"summary": "GET list of domains per registrar with pagination and filters",
"parameters": [{
"name": "registrarId",
"in": "query",
"required": false,
"type": "string",
"format": "string"
}, {
"name": "limit",
"in": "query",
"required": false,
"type": "number",
"default": 10
}, {
"name": "marker",
"description": "Name of the last domain retrieved by the user",
"in": "query",
"required": false,
"type": "string"
}, {
"name": "filterBy",
"in": "query",
"required": false,
"content": {
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"tld": {
"type":"array",
"items": {
"type":"string"
}
},
"status":{
"type":"array",
"items": {
"type":"string"
}
},
"isLocked": {
"type": "boolean"
}
}
}
}
}],
"responses": {
"200": {
"description": "Provides list of domains per registrar",
"schema": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"tld": {
"type": "string"
},
"status": {
"type": "array",
"items": {
"type": "string",
"enum":[
"CLIENT_DELETE_PROHIBITED",
"CLIENT_HOLD",
"CLIENT_RENEW_PROHIBITED",
"CLIENT_TRANSFER_PROHIBITED",
"CLIENT_UPDATE_PROHIBITED",
"INACTIVE",
"LINKED",
"OK",
"PENDING_CREATE",
"PENDING_DELETE",
"PENDING_TRANSFER",
"PENDING_UPDATE",
"SERVER_DELETE_PROHIBITED",
"SERVER_HOLD",
"SERVER_RENEW_PROHIBITED",
"SERVER_TRANSFER_PROHIBITED",
"SERVER_UPDATE_PROHIBITED"
]
}
},
"isLocked": {
"type": "boolean"
},
"creationTime": {
"type": "string"
},
"expirationTime": {
"type": "string"
}
}
}
}
}
}
}
},
"/console-api/domain": {
"post": {
"summary": "Apply action on a domain",
"responses": {
"200": {
"description": "Action has been succesfully applied"
}
},
"parameters": [
{
"name": "domain",
"in": "path",
"required": true,
"type": "string",
"format": "string"
},
{
"name": "registrarId",
"in": "body",
"required": false,
"type": "string",
"format": "string"
},
{
"name": "action",
"in": "body",
"required": true,
"type": "string",
"format": "string"
}
]
}
},
"/console-api/settings/contacts/get": {
"get": {
"summary": "GET settings contact page",
"responses": {
"200": {
"description": "Provides list of contacts per registrar",
"schema": {
"$ref": "#/definitions/settingsContacts"
}
}
},
"summary": "Retrieve registry lock information",
"parameters": [
{
"name": "registrarId",
"in": "query",
"required": false,
"type": "string",
"format": "string"
"description": "Unique identifier for the registrar.",
"required": true,
"type": "string"
}
]
}
},
"/console-api/settings/contacts/upd": {
"post": {
"summary": "Creates, updates or delete registrar contacts",
],
"produces": [
"application/json"
],
"responses": {
"200": {
"description": "Successful retrieval of registry lock information.",
"schema": {
"$ref": "#/definitions/settingsContacts"
"type": "array",
"items": {
"$ref": "#/definitions/RegistryLock"
}
}
}
},
}
}
},
"/domain-list": {
"get": {
"summary": "Retrieve a list of domains",
"parameters": [
{
"name": "registrarId",
"in": "body",
"required": false,
"type": "string",
"format": "string"
"in": "query",
"description": "Unique identifier for the registrar.",
"required": true,
"type": "string"
},
{
"name": "contacts",
"in": "body",
"name": "resultsPerPage",
"in": "query",
"description": "Number of results per page.",
"required": true,
"type": "array",
"type": "integer"
}
],
"produces": [
"application/json"
],
"responses": {
"200": {
"description": "Successful retrieval of domain list.",
"schema": {
"$ref": "#/definitions/settingsContacts"
"$ref": "#/definitions/DomainList"
}
}
]
}
}
},
"/users": {
"get": {
"summary": "Retrieve a list of users",
"parameters": [
{
"name": "registrarId",
"in": "query",
"description": "Unique identifier for the registrar.",
"required": true,
"type": "string"
}
],
"produces": [
"application/json"
],
"responses": {
"200": {
"description": "Successful retrieval of users.",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/User"
}
}
}
}
}
},
"/settings/contacts": {
"get": {
"summary": "Retrieve contact settings",
"parameters": [
{
"name": "registrarId",
"in": "query",
"description": "Unique identifier for the registrar.",
"required": true,
"type": "string"
}
],
"produces": [
"application/json"
],
"responses": {
"200": {
"description": "Successful retrieval of contact settings.",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/ContactSetting"
}
}
}
}
},
"post": {
"summary": "Update contact settings",
"consumes": [
"application/json"
],
"parameters": [
{
"name": "body",
"in": "body",
"description": "Contact settings to update.",
"required": true,
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/ContactSetting"
}
}
}
],
"responses": {
"200": {
"description": "Contact settings updated successfully."
}
}
}
},
"/settings/whois-fields": {
"post": {
"summary": "Update WHOIS fields",
"consumes": [
"application/json"
],
"parameters": [
{
"name": "body",
"in": "body",
"description": "WHOIS fields to update.",
"required": true,
"schema": {
"$ref": "#/definitions/Registrar"
}
}
],
"responses": {
"200": {
"description": "WHOIS fields updated successfully."
}
}
}
}
},
"definitions": {
"settingsContacts": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"Registrar": {
"type": "object",
"properties": {
"registrarId": {
"type": "string",
"description": "Unique identifier for the registrar."
},
"registrarName": {
"type": "string",
"description": "Name of the registrar."
},
"type": {
"type": "string",
"description": "Type of registrar (e.g., OTE, REAL)."
},
"allowedTlds": {
"type": "array",
"items": {
"type": "string"
},
"emailAddress": {
"description": "List of allowed Top-Level Domains."
},
"ipAddressAllowList": {
"type": "array",
"items": {
"type": "string"
},
"registrarId": {
"description": "List of allowed IP addresses."
},
"localizedAddress": {
"type": "object",
"properties": {
"street": {
"type": "array",
"items": {
"type": "string"
},
"description": "Street address."
},
"city": {
"type": "string",
"description": "City."
},
"state": {
"type": "string",
"description": "State."
},
"zip": {
"type": "string",
"description": "Zip code."
},
"countryCode": {
"type": "string",
"description": "Country code."
}
},
"required": [
"street",
"city",
"state",
"zip",
"countryCode"
]
},
"phoneNumber": {
"type": "string",
"description": "Phone number."
},
"faxNumber": {
"type": "string",
"description": "Fax number."
},
"emailAddress": {
"type": "string",
"description": "Email address."
},
"icannReferralEmail": {
"type": "string",
"description": "ICANN referral email address."
},
"registryLockAllowed": {
"type": "boolean",
"description": "Whether registry lock is allowed."
},
"clientCertificate": {
"type": "string",
"description": "Client certificate (if applicable)."
},
"failoverClientCertificate": {
"type": "string",
"description": "Failover client certificate (if applicable)."
},
"whoisServer": {
"type": "string",
"description": "WHOIS server (if applicable)."
},
"url": {
"type": "string",
"description": "URL (if applicable)."
},
"ianaIdentifier": {
"type": "integer",
"description": "IANA identifier (if applicable)."
},
"billingAccountMap": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"registryLockEmailAddress": {
"type": "string"
},
"phoneNumber": {
"type": "string"
},
"faxNumber": {
"type": "string"
},
"types": {
"type": "string",
"enum": [
"ABUSE",
"ADMIN",
"BILLING",
"LEGAL",
"MARKETING",
"TECH",
"WHOIS"
]
},
"visibleInWhoisAsAdmin": {
"type": "boolean"
},
"visibleInWhoisAsTech": {
"type": "boolean"
},
"visibleInDomainWhoisAsAbuse": {
"type": "boolean"
},
"allowedToSetRegistryLockPassword": {
"type": "boolean"
"description": "Mapping of billing accounts."
},
"driveFolderId": {
"type": "string",
"description": "Google Drive folder ID (if applicable)."
}
},
"required": [
"registrarId",
"registrarName",
"type",
"allowedTlds",
"localizedAddress",
"emailAddress",
"icannReferralEmail",
"registryLockAllowed"
]
},
"UserData": {
"type": "object",
"properties": {
"userRoles": {
"type": "object",
"description": "User roles (currently empty)."
},
"supportEmail": {
"type": "string",
"description": "Support email address."
},
"supportPhoneNumber": {
"type": "string",
"description": "Support phone number."
},
"globalRole": {
"type": "string",
"description": "Global role of the user."
},
"isAdmin": {
"type": "boolean",
"description": "Whether the user is an admin."
},
"productName": {
"type": "string",
"description": "Name of the product."
},
"technicalDocsUrl": {
"type": "string",
"description": "URL for technical documentation."
}
}
},
"RegistryLock": {
"type": "object",
"properties": {
"domainName": {
"type": "string",
"description": "Name of the domain."
},
"registrarPocId": {
"type": "string",
"description": "Registrar point of contact ID."
},
"lockRequestTime": {
"type": "object",
"properties": {
"creationTime": {
"type": "string",
"format": "date-time",
"description": "Time of lock request."
}
}
},
"unlockRequestTime": {
"type": "null",
"description": "Time of unlock request (null if not requested)."
},
"lockCompletionTime": {
"type": "string",
"format": "date-time",
"description": "Time of lock completion."
},
"unlockCompletionTime": {
"type": "null",
"description": "Time of unlock completion (null if not completed)."
},
"isSuperuser": {
"type": "boolean",
"description": "Whether the user is a superuser."
}
}
},
"DomainList": {
"type": "object",
"properties": {
"domains": {
"type": "array",
"items": {
"$ref": "#/definitions/Domain"
},
"description": "List of domains."
},
"checkpointTime": {
"type": "string",
"format": "date-time",
"description": "Checkpoint time for the domain list."
},
"totalResults": {
"type": "integer",
"description": "Total number of results."
}
}
},
"Domain": {
"type": "object",
"properties": {
"domainName": {
"type": "string",
"description": "Name of the domain."
},
"adminContact": {
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "Key for the admin contact."
},
"kind": {
"type": "string",
"description": "Kind of admin contact."
}
}
},
"techContact": {
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "Key for the tech contact."
},
"kind": {
"type": "string",
"description": "Kind of tech contact."
}
}
},
"registrantContact": {
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "Key for the registrant contact."
},
"kind": {
"type": "string",
"description": "Kind of registrant contact."
}
}
},
"registrationExpirationTime": {
"type": "string",
"format": "date-time",
"description": "Expiration time for domain registration."
},
"lastTransferTime": {
"type": "null",
"description": "Time of last transfer (null if not transferred)."
},
"repoId": {
"type": "string",
"description": "Repository ID."
},
"currentSponsorRegistrarId": {
"type": "string",
"description": "Current sponsor registrar ID."
},
"creationRegistrarId": {
"type": "string",
"description": "Creation registrar ID."
},
"lastEppUpdateRegistrarId": {
"type": "string",
"description": "Last EPP update registrar ID."
},
"creationTime": {
"type": "object",
"properties": {
"creationTime": {
"type": "string",
"format": "date-time",
"description": "Creation time."
}
}
},
"lastEppUpdateTime": {
"type": "string",
"format": "date-time",
"description": "Last EPP update time."
},
"statuses": {
"type": "array",
"items": {
"type": "string"
},
"description": "Domain statuses."
},
"nsHosts": {
"type": "array",
"items": {
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "Key for the name server host."
},
"kind": {
"type": "string",
"description": "Kind of name server host."
}
}
},
"description": "Name server hosts."
}
}
},
"User": {
"type": "object",
"properties": {
"emailAddress": {
"type": "string",
"description": "Email address of the user."
},
"role": {
"type": "string",
"description": "Role of the user."
}
}
},
"ContactSetting": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of the contact."
},
"emailAddress": {
"type": "string",
"description": "Email address of the contact."
},
"registrarId": {
"type": "string",
"description": "Registrar ID."
},
"phoneNumber": {
"type": "string",
"description": "Phone number of the contact."
},
"types": {
"type": "array",
"items": {
"type": "string"
},
"description": "Contact types."
},
"visibleInWhoisAsAdmin": {
"type": "boolean",
"description": "Whether the contact is visible in WHOIS as admin."
},
"visibleInWhoisAsTech": {
"type": "boolean",
"description": "Whether the contact is visible in WHOIS as tech."
},
"visibleInDomainWhoisAsAbuse": {
"type": "boolean",
"description": "Whether the contact is visible in domain WHOIS as abuse."
},
"userFriendlyTypes": {
"type": "array",
"items": {
"type": "string"
},
"description": "User-friendly contact types."
}
}
}
+1 -1
View File
@@ -43,7 +43,7 @@ steps:
gcloud auth activate-service-account --key-file=tool-credential.json
for service in default pubapi backend bsa tools
for service in default pubapi backend bsa tools console
do
for version in $(gcloud app versions list \
--filter="SERVICE:$service AND SERVING_STATUS:STOPPED" \
+3 -3
View File
@@ -10,21 +10,21 @@
# https://cloud.google.com/cloud-build/docs/running-builds/automate-builds
steps:
# Compile javadoc
- name: 'gcr.io/${PROJECT_ID}/builder'
- name: 'gcr.io/${PROJECT_ID}/builder:live'
entrypoint: /bin/bash
args: ['./gradlew', ':javadoc']
# Upload the files to GCS
# We don't use GCB's built-in artifacts uploader because we want to delete
# the existing files in the bucket first, and we want to parallelize the
# uploading process.
- name: 'gcr.io/${PROJECT_ID}/builder'
- name: 'gcr.io/${PROJECT_ID}/builder:live'
entrypoint: /bin/bash
args: ['gsutil', '-m', 'rsync', '-d', '-r', 'build/docs/javadoc', 'gs://${PROJECT_ID}-javadoc']
# Upload the files to GCS
# We don't use GCB's built-in artifacts uploader because we want to delete
# the existing files in the bucket first, and we want to parallelize the
# uploading process.
- name: 'gcr.io/${PROJECT_ID}/builder'
- name: 'gcr.io/${PROJECT_ID}/builder:live'
entrypoint: /bin/bash
args: ['gsutil', '-m', 'rsync', '-d', '-r', 'db/src/main/resources/sql/er_diagram',
'gs://${PROJECT_ID}-er-diagram']
+3 -5
View File
@@ -9,7 +9,7 @@
# https://cloud.google.com/cloud-build/docs/running-builds/automate-builds
steps:
# Download Kythe
- name: 'gcr.io/${PROJECT_ID}/builder:latest'
- name: 'gcr.io/${PROJECT_ID}/builder:live'
entrypoint: /bin/bash
args:
- -c
@@ -20,7 +20,7 @@ steps:
rm kythe-${_KYTHE_VERSION}.tar.gz
mv kythe-${_KYTHE_VERSION} kythe
# Build Nomulus with the Kythe wrapper
- name: 'gcr.io/${PROJECT_ID}/builder:latest'
- name: 'gcr.io/${PROJECT_ID}/builder:live'
entrypoint: /bin/bash
args:
- -c
@@ -44,7 +44,7 @@ steps:
./gradlew clean testClasses \
-Dno_werror=true -PenableCrossReferencing=true
# Merge kzip files
- name: 'gcr.io/${PROJECT_ID}/builder:latest'
- name: 'gcr.io/${PROJECT_ID}/builder:live'
entrypoint: /bin/bash
args:
- -c
@@ -53,13 +53,11 @@ steps:
./kythe/tools/kzip merge \
--output $${KYTHE_OUTPUT_DIRECTORY}/merged/${COMMIT_SHA}.kzip \
$${KYTHE_OUTPUT_DIRECTORY}/*.kzip
artifacts:
objects:
location: 'gs://${PROJECT_ID}-codesearch'
paths:
- 'kythe_output/merged/${COMMIT_SHA}.kzip'
timeout: 3600s
options:
machineType: 'E2_HIGHCPU_32'
+62 -52
View File
@@ -36,18 +36,53 @@ steps:
# informational. It makes it easier to tell the tag of the current 'live' release.
- name: 'gcr.io/${PROJECT_ID}/builder:latest'
entrypoint: /bin/bash
args: [ '-c', 'echo ${TAG_NAME} > output/tag_name']
# Tag the tool image and push it to the registry.
args: [ '-c', 'echo ${TAG_NAME} > output/tag_name' ]
# Build Nomulus, tool and proxy image, them upload them to GCR.
- name: 'gcr.io/${PROJECT_ID}/builder:latest'
# Set home for Gradle caches. Must be consistent with last step below
# and ./build_nomulus_for_env.sh
env: [ 'GRADLE_USER_HOME=/workspace/cloudbuild-caches' ]
entrypoint: /bin/bash
args:
- -c
- |
./gradlew :jetty:buildNomulusImage :proxy:buildProxyImage :core:buildToolImage\
-PmavenUrl=gcs://domain-registry-maven-repository/maven \
-PpluginsUrl=gcs://domain-registry-maven-repository/plugins
docker tag nomulus gcr.io/${PROJECT_ID}/nomulus:${TAG_NAME}
docker tag nomulus gcr.io/${PROJECT_ID}/nomulus:latest
docker push gcr.io/${PROJECT_ID}/nomulus:${TAG_NAME}
docker push gcr.io/${PROJECT_ID}/nomulus:latest
docker tag proxy gcr.io/${PROJECT_ID}/proxy:${TAG_NAME}
docker tag proxy gcr.io/${PROJECT_ID}/proxy:latest
docker push gcr.io/${PROJECT_ID}/proxy:${TAG_NAME}
docker push gcr.io/${PROJECT_ID}/proxy:latest
docker tag nomulus-tool gcr.io/${PROJECT_ID}/nomulus-tool:${TAG_NAME}
docker tag nomulus-tool gcr.io/${PROJECT_ID}/nomulus-tool:latest
docker push gcr.io/${PROJECT_ID}/nomulus-tool:${TAG_NAME}
docker push gcr.io/${PROJECT_ID}/nomulus-tool:latest
# Sign nomulus and proxy images.
- name: 'gcr.io/${PROJECT_ID}/builder:latest'
entrypoint: /bin/bash
args:
- -c
- |
set -e
docker tag nomulus-tool gcr.io/${PROJECT_ID}/nomulus-tool:${TAG_NAME}
docker tag nomulus-tool gcr.io/${PROJECT_ID}/nomulus-tool:latest
docker push gcr.io/${PROJECT_ID}/nomulus-tool:${TAG_NAME}
docker push gcr.io/${PROJECT_ID}/nomulus-tool:latest
nomulus_digest=$(gcloud container images list-tags gcr.io/${PROJECT_ID}/nomulus \
--format="get(digest)" --filter="tags = ${TAG_NAME}")
proxy_digest=$(gcloud container images list-tags gcr.io/${PROJECT_ID}/proxy \
--format="get(digest)" --filter="tags = ${TAG_NAME}")
gcloud --project=${PROJECT_ID} beta container binauthz attestations \
sign-and-create --artifact-url=gcr.io/${PROJECT_ID}/nomulus@$nomulus_digest \
--attestor=build-attestor --attestor-project=${PROJECT_ID} \
--keyversion-project=${PROJECT_ID} --keyversion-location=global \
--keyversion-keyring=attestor-keys --keyversion-key=signing \
--keyversion=1
gcloud --project=${PROJECT_ID} beta container binauthz attestations \
sign-and-create --artifact-url=gcr.io/${PROJECT_ID}/proxy@$proxy_digest \
--attestor=build-attestor --attestor-project=${PROJECT_ID} \
--keyversion-project=${PROJECT_ID} --keyversion-location=global \
--keyversion-keyring=attestor-keys --keyversion-key=signing \
--keyversion=1
# Get the tool image digest and substitute in the digest in other GCB files.
- name: 'gcr.io/${PROJECT_ID}/builder:latest'
entrypoint: /bin/bash
@@ -57,14 +92,11 @@ steps:
set -e
digest=$(gcloud container images list-tags gcr.io/${PROJECT_ID}/nomulus-tool \
--format="get(digest)" --filter="tags = ${TAG_NAME}")
sed -i s/'$${_IMAGE}'/nomulus-tool/g release/cloudbuild-tag.yaml
sed -i s/':$${TAG_NAME}'/@$digest/g release/cloudbuild-tag.yaml
sed -i s/'nomulus-tool:latest'/nomulus-tool@$digest/g release/cloudbuild-deploy-*.yaml
# schema-deploy and schema-verify scripts
sed -i s/'nomulus-tool:latest'/nomulus-tool@$digest/g release/cloudbuild-schema-*.yaml
# Build the prober_cert_updater image and upload it to GCR. This image extends
# from the `builder` and the nomulus.jar built earlier.
- name: 'gcr.io/cloud-builders/docker'
sed -i s/nomulus-tool:latest/nomulus-tool@$digest/g release/cloudbuild-schema-*.yaml
# Build and upload the prober_cert_updater image. This image extends from the `builder` and the
# nomulus.jar built earlier.
- name: 'gcr.io/${PROJECT_ID}/builder:latest'
entrypoint: /bin/bash
args:
- -c
@@ -76,24 +108,12 @@ steps:
--build-arg TAG_NAME=${TAG_NAME} --build-arg PROJECT_ID=${PROJECT_ID} .
docker tag gcr.io/${PROJECT_ID}/prober_cert_updater:${TAG_NAME} \
gcr.io/${PROJECT_ID}/prober_cert_updater:latest
docker push gcr.io/${PROJECT_ID}/prober_cert_updater:latest
docker push gcr.io/${PROJECT_ID}/prober_cert_updater:${TAG_NAME}
docker push gcr.io/${PROJECT_ID}/prober_cert_updater:latest
dir: 'release/prober-cert-updater/'
# Update the prober_updater image digest in relevant GCB files.
# Build and upload the db_object_updater image. This image extends from the `builder` and the
# nomulus.jar built earlier.
- name: 'gcr.io/${PROJECT_ID}/builder:latest'
entrypoint: /bin/bash
args:
- -c
- |
set -e
digest=$(gcloud container images list-tags \
gcr.io/${PROJECT_ID}/prober_cert_updater \
--format="get(digest)" --filter="tags = ${TAG_NAME}")
sed -i s/'prober_cert_updater:latest'/prober_cert_updater@$digest/g \
release/cloudbuild-renew-prober-certs-*.yaml
# Build the db_object_updater image and upload it to GCR. This image extends
# from the `builder` and the nomulus.jar built earlier.
- name: 'gcr.io/cloud-builders/docker'
entrypoint: /bin/bash
args:
- -c
@@ -105,25 +125,13 @@ steps:
--build-arg TAG_NAME=${TAG_NAME} --build-arg PROJECT_ID=${PROJECT_ID} .
docker tag gcr.io/${PROJECT_ID}/db_object_updater:${TAG_NAME} \
gcr.io/${PROJECT_ID}/db_object_updater:latest
docker push gcr.io/${PROJECT_ID}/db_object_updater:latest
docker push gcr.io/${PROJECT_ID}/db_object_updater:${TAG_NAME}
docker push gcr.io/${PROJECT_ID}/db_object_updater:latest
dir: 'release/db-object-updater/'
# Update the db_object_updater image digest in relevant GCB files.
- name: 'gcr.io/${PROJECT_ID}/builder:latest'
entrypoint: /bin/bash
args:
- -c
- |
set -e
digest=$(gcloud container images list-tags \
gcr.io/${PROJECT_ID}/db_object_updater \
--format="get(digest)" --filter="tags = ${TAG_NAME}")
sed -i s/'db_object_updater:latest'/db_object_updater@$digest/g \
release/cloudbuild-sync-db-objects-*.yaml
# Build and stage Dataflow Flex templates.
- name: 'gcr.io/${PROJECT_ID}/builder:latest'
entrypoint: /bin/bash
# Set home for Gradle caches. Must be consistent with previous steps above
# Set home for Gradle caches. Must be consistent with the previous steps above
# and ./build_nomulus_for_env.sh
env: [ 'GRADLE_USER_HOME=/workspace/cloudbuild-caches' ]
args:
@@ -146,9 +154,8 @@ steps:
google/registry/beam/resave_all_epp_resources_pipeline_metadata.json \
google.registry.beam.wipeout.WipeOutContactHistoryPiiPipeline \
google/registry/beam/wipe_out_contact_history_pii_pipeline_metadata.json
# Tentatively build and publish Cloud SQL schema jar here, before schema release
# process is finalized. Also publish nomulus:core jars that are needed for
# server/schema compatibility tests.
# Tentatively build and publish Cloud SQL schema jar here, before the schema release process is
# finalized. Also publish nomulus:core jars that are needed for server/schema compatibility tests.
- name: 'gcr.io/${PROJECT_ID}/builder:latest'
entrypoint: /bin/bash
# Set home for Gradle caches. Must be consistent with previous steps above
@@ -183,15 +190,18 @@ artifacts:
- 'output/tag_name'
- 'output/nomulus.jar'
- 'output/schema.jar'
- 'release/cloudbuild-tag.yaml'
- 'release/cloudbuild-sync.yaml'
- 'release/cloudbuild-sync-and-tag.yaml'
- 'release/cloudbuild-deploy-*.yaml'
- 'release/cloudbuild-delete-*.yaml'
- 'release/cloudbuild-renew-prober-certs-*.yaml'
- 'release/cloudbuild-schema-deploy-*.yaml'
- 'release/cloudbuild-schema-verify-*.yaml'
- 'release/cloudbuild-sync-db-objects-*.yaml'
- 'jetty/kubernetes/*.yaml'
- 'jetty/kubernetes/gateway/*.yaml'
# The images are already uploaded, but we still need to include them there so that
# the GCB pubsub message contains them (for Spinnaker to consume).
images:
- 'gcr.io/${PROJECT_ID}/nomulus:${TAG_NAME}'
- 'gcr.io/${PROJECT_ID}/proxy:${TAG_NAME}'
timeout: 7200s
options:
machineType: 'E2_HIGHCPU_32'
machineType: 'E2_HIGHCPU_32'
-3
View File
@@ -43,8 +43,6 @@ steps:
--keyversion-project=${PROJECT_ID} --keyversion-location=global \
--keyversion-keyring=attestor-keys --keyversion-key=signing \
--keyversion=1
sed -i s/'$${_IMAGE}'/proxy/g release/cloudbuild-tag.yaml
sed -i s/':$${TAG_NAME}'/@$digest/g release/cloudbuild-tag.yaml
# Images to upload to GCR. Even though the image has already been uploaded, we still include it
# here so that the GCB pubsub message contains it (for Spinnaker to consume).
images: ['gcr.io/${PROJECT_ID}/proxy:${TAG_NAME}']
@@ -55,7 +53,6 @@ artifacts:
# This cannot be regexs because of how Spinnaker constructs artifact paths.
paths:
- 'proxy/kubernetes/proxy-*.yaml'
- 'release/cloudbuild-tag.yaml'
timeout: 3600s
options:
machineType: 'E2_HIGHCPU_32'
+92 -47
View File
@@ -7,7 +7,7 @@
#
# This pipeline prepares a release. The pipeline should be run against the Nomulus public repo on
# GitHub. It builds the builder and base images, and hard codes the sha256 hashes of the resulting
# images in the merged code base (internal + public) , which is tagged and pushed into the release
# images in the merged code base (internal + public), which is tagged and pushed into the release
# repo. Actual release artifacts are built from the release repo, ensuring reproducibility.
steps:
# Check the out internal repo.
@@ -42,7 +42,7 @@ steps:
rm -rf .git && rm -rf nomulus-internal/.git
cp -rf nomulus-internal/* .
rm -rf nomulus-internal
# Build the builder image and tag the proxy base image, then upload them to GCR.
# Build the builder image and pull the base images, them upload them to GCR.
- name: 'gcr.io/cloud-builders/docker'
entrypoint: /bin/bash
args:
@@ -51,18 +51,18 @@ steps:
set -e
docker build -t gcr.io/${PROJECT_ID}/builder:${TAG_NAME} .
docker tag gcr.io/${PROJECT_ID}/builder:${TAG_NAME} gcr.io/${PROJECT_ID}/builder:latest
docker pull gcr.io/distroless/java
docker tag gcr.io/distroless/java gcr.io/${PROJECT_ID}/base:${TAG_NAME}
docker tag gcr.io/distroless/java gcr.io/${PROJECT_ID}/base:latest
docker pull gcr.io/distroless/java:debug
docker tag gcr.io/distroless/java:debug gcr.io/${PROJECT_ID}/base-debug:${TAG_NAME}
docker tag gcr.io/distroless/java:debug gcr.io/${PROJECT_ID}/base-debug:latest
docker push gcr.io/${PROJECT_ID}/builder:latest
docker push gcr.io/${PROJECT_ID}/builder:${TAG_NAME}
docker push gcr.io/${PROJECT_ID}/base:latest
docker push gcr.io/${PROJECT_ID}/base:${TAG_NAME}
docker push gcr.io/${PROJECT_ID}/base-debug:latest
docker push gcr.io/${PROJECT_ID}/base-debug:${TAG_NAME}
docker push gcr.io/${PROJECT_ID}/builder:latest
docker pull jetty:12-jdk21
docker tag jetty:12-jdk21 gcr.io/${PROJECT_ID}/jetty:${TAG_NAME}
docker tag jetty:12-jdk21 gcr.io/${PROJECT_ID}/jetty:latest
docker push gcr.io/${PROJECT_ID}/jetty:${TAG_NAME}
docker push gcr.io/${PROJECT_ID}/jetty:latest
docker pull eclipse-temurin:21
docker tag eclipse-temurin:21 gcr.io/${PROJECT_ID}/temurin:${TAG_NAME}
docker tag eclipse-temurin:21 gcr.io/${PROJECT_ID}/temurin:latest
docker push gcr.io/${PROJECT_ID}/temurin:${TAG_NAME}
docker push gcr.io/${PROJECT_ID}/temurin:latest
dir: 'release/builder/'
# Do text replacement in the merged repo, hardcoding image digests.
- name: 'gcr.io/cloud-builders/gcloud'
@@ -73,27 +73,30 @@ steps:
set -e
builder_digest=$(gcloud container images list-tags gcr.io/${PROJECT_ID}/builder \
--format='get(digest)' --filter='tags = ${TAG_NAME}')
base_digest=$(gcloud container images list-tags gcr.io/${PROJECT_ID}/base \
jetty_digest=$(gcloud container images list-tags gcr.io/${PROJECT_ID}/jetty \
--format='get(digest)' --filter='tags = ${TAG_NAME}')
debug_digest=$(gcloud container images list-tags gcr.io/${PROJECT_ID}/base-debug \
temurin_digest=$(gcloud container images list-tags gcr.io/${PROJECT_ID}/temurin \
--format='get(digest)' --filter='tags = ${TAG_NAME}')
sed -i s%distroless/java%${PROJECT_ID}/base@$base_digest% proxy/Dockerfile
sed -i s%distroless/java:debug%${PROJECT_ID}/base-debug@$debug_digest% core/Dockerfile
sed -i s%eclipse-temurin:21%gcr.io/${PROJECT_ID}/temurin@$temurin_digest%g proxy/Dockerfile
sed -i s%eclipse-temurin:21%gcr.io/${PROJECT_ID}/temurin@$temurin_digest%g core/Dockerfile
sed -i s%jetty:12-jdk21%gcr.io/${PROJECT_ID}/jetty@$jetty_digest%g jetty/Dockerfile
sed -i s/builder:latest/builder@$builder_digest/g release/cloudbuild-proxy.yaml
sed -i s/builder:latest/builder@$builder_digest/g release/cloudbuild-nomulus.yaml
sed -i s/builder:latest/builder@$builder_digest/g release/cloudbuild-deploy.yaml
sed -i s/builder:latest/builder@$builder_digest/g release/cloudbuild-sync.yaml
sed -i s/builder:latest/builder@$builder_digest/g release/cloudbuild-tag.yaml
sed -i s/builder:latest/builder@$builder_digest/g release/cloudbuild-sync-and-tag.yaml
sed -i s/builder:latest/builder@$builder_digest/g release/cloudbuild-schema-deploy.yaml
sed -i s/builder:latest/builder@$builder_digest/g release/cloudbuild-schema-verify.yaml
sed -i s/builder:latest/builder@$builder_digest/g release/cloudbuild-delete.yaml
sed -i s/GCP_PROJECT/${PROJECT_ID}/ proxy/kubernetes/proxy-*.yaml
sed -i s/'$${TAG_NAME}'/${TAG_NAME}/g release/cloudbuild-sync.yaml
sed -i s/'$${TAG_NAME}'/${TAG_NAME}/g release/cloudbuild-sync-and-tag.yaml
sed -i s/'$${TAG_NAME}'/${TAG_NAME}/g release/cloudbuild-deploy.yaml
for environment in alpha crash sandbox production; do
for environment in alpha crash qa sandbox production; do
sed s/'$${_ENV}'/${environment}/g release/cloudbuild-deploy.yaml \
> release/cloudbuild-deploy-${environment}.yaml
sed s/'$${_ENV}'/${environment}/g release/cloudbuild-delete.yaml \
> release/cloudbuild-delete-${environment}.yaml
done
# Build the schema_deployer image and upload it to GCR.
# Build and upload the schema_deployer image.
- name: 'gcr.io/cloud-builders/docker'
entrypoint: /bin/bash
args:
@@ -103,10 +106,10 @@ steps:
docker build -t gcr.io/${PROJECT_ID}/schema_deployer:${TAG_NAME} --build-arg TAG_NAME=${TAG_NAME} --build-arg PROJECT_ID=${PROJECT_ID} .
docker tag gcr.io/${PROJECT_ID}/schema_deployer:${TAG_NAME} \
gcr.io/${PROJECT_ID}/schema_deployer:latest
docker push gcr.io/${PROJECT_ID}/schema_deployer:latest
docker push gcr.io/${PROJECT_ID}/schema_deployer:${TAG_NAME}
docker push gcr.io/${PROJECT_ID}/schema_deployer:latest
dir: 'release/schema-deployer/'
# Build the schema_verifier image and upload it to GCR.
# Build and upload the schema_verifier image.
- name: 'gcr.io/cloud-builders/docker'
entrypoint: /bin/bash
args:
@@ -116,33 +119,21 @@ steps:
docker build -t gcr.io/${PROJECT_ID}/schema_verifier:${TAG_NAME} --build-arg TAG_NAME=${TAG_NAME} --build-arg PROJECT_ID=${PROJECT_ID} .
docker tag gcr.io/${PROJECT_ID}/schema_verifier:${TAG_NAME} \
gcr.io/${PROJECT_ID}/schema_verifier:latest
docker push gcr.io/${PROJECT_ID}/schema_verifier:latest
docker push gcr.io/${PROJECT_ID}/schema_verifier:${TAG_NAME}
docker push gcr.io/${PROJECT_ID}/schema_verifier:latest
dir: 'release/schema-verifier/'
# Do text replacement in the schema-deploy, schema-verify and
# prober_cert_updater configs.
# Do text replacement in the cloud build YAML files.
- name: 'gcr.io/cloud-builders/gcloud'
entrypoint: /bin/bash
args:
- -c
- |
set -e
builder_digest=$( \
gcloud container images list-tags gcr.io/${PROJECT_ID}/builder \
builder_digest=$(gcloud container images list-tags gcr.io/${PROJECT_ID}/builder \
--format='get(digest)' --filter='tags = ${TAG_NAME}')
schema_deployer_digest=$( \
gcloud container images list-tags gcr.io/${PROJECT_ID}/schema_deployer \
schema_deployer_digest=$(gcloud container images list-tags gcr.io/${PROJECT_ID}/schema_deployer \
--format='get(digest)' --filter='tags = ${TAG_NAME}')
schema_verifier_digest=$( \
gcloud container images list-tags gcr.io/${PROJECT_ID}/schema_verifier \
--format='get(digest)' --filter='tags = ${TAG_NAME}')
prober_cert_updater_digest=$( \
gcloud container images list-tags \
gcr.io/${PROJECT_ID}/prober_cert_updater \
--format='get(digest)' --filter='tags = ${TAG_NAME}')
db_object_updater_digest=$( \
gcloud container images list-tags \
gcr.io/${PROJECT_ID}/db_object_updater \
schema_verifier_digest=$(gcloud container images list-tags gcr.io/${PROJECT_ID}/schema_verifier \
--format='get(digest)' --filter='tags = ${TAG_NAME}')
sed -i s/builder:latest/builder@$builder_digest/g \
release/cloudbuild-schema-deploy.yaml
@@ -157,17 +148,70 @@ steps:
sed -i s/schema_verifier:latest/schema_verifier@$schema_verifier_digest/g \
release/cloudbuild-schema-verify.yaml
sed -i s/'$${TAG_NAME}'/${TAG_NAME}/g release/cloudbuild-schema-deploy.yaml
for environment in alpha crash sandbox production; do
for environment in alpha crash qa sandbox production; do
sed s/'$${_ENV}'/${environment}/g release/cloudbuild-schema-deploy.yaml \
> release/cloudbuild-schema-deploy-${environment}.yaml
sed s/'$${_ENV}'/${environment}/g release/cloudbuild-schema-verify.yaml \
> release/cloudbuild-schema-verify-${environment}.yaml
sed s/'$${_ENV}'/${environment}/g release/cloudbuild-renew-prober-certs.yaml \
> release/cloudbuild-renew-prober-certs-${environment}.yaml
sed s/'$${_ENV}'/${environment}/g release/cloudbuild-sync-db-objects.yaml \
> release/cloudbuild-sync-db-objects-${environment}.yaml
done
# Upload the gradle binary to GCS if it does not exist and point URL in gradle wrapper to it.
# Do text replacement in the k8s manifests.
- name: 'gcr.io/cloud-builders/gcloud'
entrypoint: /bin/bash
args:
- -c
- |
set -e
for env in alpha crash qa sandbox production
do
# This is the project where Nomulus runs, and we need it to correctly bind the k8s service
# account to the GCP service account.
if [ ${env} == production ]
then
project="domain-registry"
else
project="domain-registry-${env}"
fi
base_domain=$(grep baseDomain \
./core/src/main/java/google/registry/config/files/nomulus-config-${env}.yaml | \
awk '{print $2}')
for service in frontend backend pubapi console
do
# non-canary
sed s/GCP_PROJECT/${PROJECT_ID}/g ./jetty/kubernetes/nomulus-${service}.yaml | \
sed s/ENVIRONMENT/${env}/g | \
sed s/PROXY_ENV/${env}/g | \
sed s/EPP/epp/g | \
sed s/WHOIS/whois/g > ./jetty/kubernetes/nomulus-${env}-${service}.yaml
if [ ${service} == frontend ]
then
sed -i s/${PROJECT_ID}.iam.gserviceaccount.com/${project}.iam.gserviceaccount.com/g \
./jetty/kubernetes/nomulus-${env}-${service}.yaml
fi
# canary
sed s/GCP_PROJECT/${PROJECT_ID}/g ./jetty/kubernetes/nomulus-${service}.yaml | \
sed s/ENVIRONMENT/${env}/g | \
sed s/PROXY_ENV/${env}_canary/g | \
sed s/EPP/epp-canary/g | \
sed s/WHOIS/whois-canary/g | \
sed s/${service}/${service}-canary/g \
> ./jetty/kubernetes/nomulus-${env}-${service}-canary.yaml
if [ ${service} == frontend ]
then
sed -i s/${PROJECT_ID}.iam.gserviceaccount.com/${project}.iam.gserviceaccount.com/g \
./jetty/kubernetes/nomulus-${env}-${service}-canary.yaml
fi
# gateway
sed s/BASE_DOMAIN/${base_domain}/g \
./jetty/kubernetes/gateway/nomulus-route-${service}.yaml \
> ./jetty/kubernetes/gateway/nomulus-route-${env}-${service}.yaml
# IAP
sed s/SERVICE/${service}/g ./jetty/kubernetes/gateway/nomulus-iap-${env}.yaml \
> ./jetty/kubernetes/gateway/nomulus-iap-${env}-${service}.yaml
sed s/SERVICE/${service}-canary/g ./jetty/kubernetes/gateway/nomulus-iap-${env}.yaml \
> ./jetty/kubernetes/gateway/nomulus-iap-${env}-${service}-canary.yaml
done
done
# Upload the Gradle binary to GCS if it does not exist and point URL in Gradle wrapper to it.
- name: 'gcr.io/cloud-builders/gsutil'
entrypoint: /bin/bash
args:
@@ -204,6 +248,7 @@ steps:
- -c
- |
set -e
rm -rf gcompute-tools
cp -rf nomulus-release/.git .
rm -rf nomulus-release
git config --global user.name "Cloud Build"

Some files were not shown because too many files have changed in this diff Show More