diff --git a/it/xds-k8s-node-ip-extractor/src/test/java/com/linecorp/centraldogma/it/xds/k8s/XdsKubernetesNodeIpExtractorTest.java b/it/xds-k8s-node-ip-extractor/src/test/java/com/linecorp/centraldogma/it/xds/k8s/XdsKubernetesNodeIpExtractorTest.java index 59dbb10e5..bb0475395 100644 --- a/it/xds-k8s-node-ip-extractor/src/test/java/com/linecorp/centraldogma/it/xds/k8s/XdsKubernetesNodeIpExtractorTest.java +++ b/it/xds-k8s-node-ip-extractor/src/test/java/com/linecorp/centraldogma/it/xds/k8s/XdsKubernetesNodeIpExtractorTest.java @@ -17,6 +17,7 @@ import static com.google.common.collect.ImmutableList.toImmutableList; import static com.linecorp.centraldogma.it.xds.k8s.LabelBasedNodeIpExtractor.NODE_IP_LABEL_PROPERTY; +import static com.linecorp.centraldogma.server.internal.storage.InternalProjectConstants.INTERNAL_PROJECT_XDS; import static net.javacrumbs.jsonunit.fluent.JsonFluentAssert.assertThatJson; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; @@ -85,7 +86,6 @@ @EnableKubernetesMockClient(crud = true) class XdsKubernetesNodeIpExtractorTest { - private static final String XDS_CENTRAL_DOGMA_PROJECT = "@xds"; private static final String K8S_ENDPOINT_AGGREGATORS_DIRECTORY = "/k8s/endpointAggregators/"; private static final String K8S_ENDPOINTS_DIRECTORY = "/k8s/endpoints/"; @@ -149,7 +149,7 @@ void extractsNodeIpFromLabel() throws Exception { assertThat(response.status()).isSameAs(HttpStatus.OK); assertThat(response.headers().get("grpc-status")).isEqualTo("0"); - final Repository fooGroup = dogma.projectManager().get(XDS_CENTRAL_DOGMA_PROJECT) + final Repository fooGroup = dogma.projectManager().get(INTERNAL_PROJECT_XDS) .repos().get("foo"); final Entry aggregatorEntry = fooGroup.get(Revision.HEAD, Query.ofYaml( @@ -225,7 +225,7 @@ void fallsBackToInternalIpWhenLabelKeyIsAbsent() throws Exception { assertThat(response.status()).isSameAs(HttpStatus.OK); assertThat(response.headers().get("grpc-status")).isEqualTo("0"); - final Repository fooGroup = dogma.projectManager().get(XDS_CENTRAL_DOGMA_PROJECT) + final Repository fooGroup = dogma.projectManager().get(INTERNAL_PROJECT_XDS) .repos().get("foo"); final Entry aggregatorEntry = fooGroup.get(Revision.HEAD, Query.ofYaml( diff --git a/it/xds-member-permission/src/test/java/com/linecorp/centraldogma/server/test/XdsMemberPermissionTest.java b/it/xds-member-permission/src/test/java/com/linecorp/centraldogma/server/test/XdsMemberPermissionTest.java index 816d73898..e144eeafc 100644 --- a/it/xds-member-permission/src/test/java/com/linecorp/centraldogma/server/test/XdsMemberPermissionTest.java +++ b/it/xds-member-permission/src/test/java/com/linecorp/centraldogma/server/test/XdsMemberPermissionTest.java @@ -17,6 +17,7 @@ package com.linecorp.centraldogma.server.test; import static com.linecorp.centraldogma.internal.CredentialUtil.credentialName; +import static com.linecorp.centraldogma.server.internal.storage.InternalProjectConstants.INTERNAL_PROJECT_XDS; import static com.linecorp.centraldogma.testing.internal.auth.TestAuthMessageUtil.PASSWORD; import static com.linecorp.centraldogma.testing.internal.auth.TestAuthMessageUtil.USERNAME; import static org.assertj.core.api.Assertions.assertThat; @@ -91,12 +92,13 @@ void shouldAllowMembersToAccessInternalProjects() throws Exception { .build() .blocking(); - assertThat(adminClient.listProjects().join()).containsOnly("dogma", "foo", "@xds"); + assertThat(adminClient.listProjects().join()).containsOnly("dogma", "foo", INTERNAL_PROJECT_XDS); // The xDS project is a self-service project, so it is visible to any authenticated user even before // being granted a role. Other internal projects (e.g. dogma) remain hidden. - assertThat(nonAdminClient.listProjects().join()).containsOnly("foo", "@xds"); + assertThat(nonAdminClient.listProjects().join()).containsOnly("foo", INTERNAL_PROJECT_XDS); - final CentralDogmaRepository adminRepo = adminClient.createRepository("@xds", "test").join(); + final CentralDogmaRepository adminRepo = adminClient.createRepository(INTERNAL_PROJECT_XDS, "test") + .join(); adminRepo.commit("Add test.txt", Change.ofTextUpsert("/text.txt", "foo")) .push() .join(); @@ -105,18 +107,18 @@ void shouldAllowMembersToAccessInternalProjects() throws Exception { adminWebClient.prepare() .post("/api/v1/projects/@xds/credentials") .contentJson(new CreateCredentialRequest( - "test", new NoneCredential(credentialName("@xds", "test")))) + "test", new NoneCredential(credentialName(INTERNAL_PROJECT_XDS, "test")))) .execute(); assertThat(credentialResponse.status()).isEqualTo(HttpStatus.CREATED); // All CRUD operations should be blocked. assertThatThrownBy(() -> { - nonAdminClient.createRepository("@xds", "test2").join(); + nonAdminClient.createRepository(INTERNAL_PROJECT_XDS, "test2").join(); }).isInstanceOf(CompletionException.class) .hasCauseInstanceOf(PermissionException.class) .hasMessageContaining("You must have the MEMBER project role to access the project '@xds'."); - final CentralDogmaRepository userRepo = nonAdminClient.forRepo("@xds", "test"); + final CentralDogmaRepository userRepo = nonAdminClient.forRepo(INTERNAL_PROJECT_XDS, "test"); assertThatThrownBy(() -> { userRepo.commit("Update test.txt", Change.ofTextUpsert("/text.txt", "bar")) .push() @@ -148,7 +150,7 @@ void shouldAllowMembersToAccessInternalProjects() throws Exception { // @xds project should be visible to member app identities. await().untilAsserted( - () -> assertThat(nonAdminClient.listProjects().join()).containsOnly("foo", "@xds") + () -> assertThat(nonAdminClient.listProjects().join()).containsOnly("foo", INTERNAL_PROJECT_XDS) ); // Read and write should be granted as well. userRepo.commit("Update test.txt", Change.ofTextUpsert("/text.txt", "bar")) diff --git a/server-mirror-dogma/src/main/java/com/linecorp/centraldogma/server/internal/mirror/CentralDogmaMirror.java b/server-mirror-dogma/src/main/java/com/linecorp/centraldogma/server/internal/mirror/CentralDogmaMirror.java index 9d34f169c..f67e267ce 100644 --- a/server-mirror-dogma/src/main/java/com/linecorp/centraldogma/server/internal/mirror/CentralDogmaMirror.java +++ b/server-mirror-dogma/src/main/java/com/linecorp/centraldogma/server/internal/mirror/CentralDogmaMirror.java @@ -408,6 +408,7 @@ protected MirrorResult mirrorRemoteToLocal(File workDir, CommandExecutor executo changes.put(path, Change.ofRemoval(path)); } }); + validateChanges(changes); final String summary = "Mirror " + remoteHead + ", '" + remoteUri() + "' to the repository '" + localRepo().name() + '\''; diff --git a/server-mirror-git/src/main/java/com/linecorp/centraldogma/server/internal/mirror/AbstractGitMirror.java b/server-mirror-git/src/main/java/com/linecorp/centraldogma/server/internal/mirror/AbstractGitMirror.java index 9b1e61c43..37658fa7b 100644 --- a/server-mirror-git/src/main/java/com/linecorp/centraldogma/server/internal/mirror/AbstractGitMirror.java +++ b/server-mirror-git/src/main/java/com/linecorp/centraldogma/server/internal/mirror/AbstractGitMirror.java @@ -450,6 +450,7 @@ MirrorResult mirrorRemoteToLocal( } }); + validateChanges(changes); try { final Revision revision = executor.execute(Command.push( MIRROR_AUTHOR, localRepo().parent().name(), localRepo().name(), diff --git a/server-mirror-git/src/test/java/com/linecorp/centraldogma/server/internal/mirror/DefaultMetaRepositoryWithMirrorTest.java b/server-mirror-git/src/test/java/com/linecorp/centraldogma/server/internal/mirror/DefaultMetaRepositoryWithMirrorTest.java index 2eacddbcc..226f80f85 100644 --- a/server-mirror-git/src/test/java/com/linecorp/centraldogma/server/internal/mirror/DefaultMetaRepositoryWithMirrorTest.java +++ b/server-mirror-git/src/test/java/com/linecorp/centraldogma/server/internal/mirror/DefaultMetaRepositoryWithMirrorTest.java @@ -19,8 +19,10 @@ import static com.google.common.collect.ImmutableList.toImmutableList; import static com.linecorp.centraldogma.internal.CredentialUtil.credentialFile; import static com.linecorp.centraldogma.internal.CredentialUtil.credentialName; +import static com.linecorp.centraldogma.server.internal.storage.InternalProjectConstants.INTERNAL_PROJECT_XDS; import static com.linecorp.centraldogma.server.internal.storage.repository.MirrorConfig.DEFAULT_SCHEDULE; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.Comparator; @@ -245,6 +247,27 @@ void testMirrorWithCredentialId() { assertThat(((SshKeyCredential) m.credential()).username()).isEqualTo("alice"); } + @Test + void xdsMirrorWithNonRootLocalPath_isRejected() { + final MirrorRequest badMirror = new MirrorRequest( + "xds-mirror", true, INTERNAL_PROJECT_XDS, DEFAULT_SCHEDULE, "REMOTE_TO_LOCAL", "some-group", + "/clusters/", "git+ssh", "git.example.com/org/repo.git", "/", "main", null, "", null); + assertThatThrownBy(() -> + metaRepo.createMirrorPushCommand("some-group", badMirror, Author.SYSTEM, null, false)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("localPath"); + } + + @Test + void xdsMirrorWithRootLocalPath_isAccepted() { + final MirrorRequest validMirror = new MirrorRequest( + "xds-mirror", true, INTERNAL_PROJECT_XDS, DEFAULT_SCHEDULE, "REMOTE_TO_LOCAL", "some-group", + "/", "git+ssh", "git.example.com/org/repo.git", "/", "main", null, "", null); + assertThatCode(() -> + metaRepo.createMirrorPushCommand("some-group", validMirror, Author.SYSTEM, null, false)) + .doesNotThrowAnyException(); + } + private List findMirrors() { // Get the mirror list and sort it by localRepo name alphabetically for easier testing. return metaRepo.mirrors().join().stream() diff --git a/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/AbstractMirror.java b/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/AbstractMirror.java index b3846f3f0..8aadc04ff 100644 --- a/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/AbstractMirror.java +++ b/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/AbstractMirror.java @@ -26,14 +26,19 @@ import java.time.Instant; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.ServiceLoader; import org.eclipse.jgit.ignore.IgnoreNode; import org.eclipse.jgit.ignore.IgnoreNode.MatchResult; import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.cronutils.descriptor.CronDescriptor; import com.cronutils.model.Cron; @@ -41,9 +46,11 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects.ToStringHelper; +import com.google.common.collect.ImmutableList; import com.google.common.hash.Hashing; import com.linecorp.centraldogma.common.Author; +import com.linecorp.centraldogma.common.Change; import com.linecorp.centraldogma.common.Entry; import com.linecorp.centraldogma.common.EntryType; import com.linecorp.centraldogma.common.MirrorException; @@ -51,6 +58,7 @@ import com.linecorp.centraldogma.server.credential.Credential; import com.linecorp.centraldogma.server.mirror.Mirror; import com.linecorp.centraldogma.server.mirror.MirrorDirection; +import com.linecorp.centraldogma.server.mirror.MirrorFileValidator; import com.linecorp.centraldogma.server.mirror.MirrorResult; import com.linecorp.centraldogma.server.mirror.MirrorStatus; import com.linecorp.centraldogma.server.mirror.RepositoryUri; @@ -58,8 +66,17 @@ public abstract class AbstractMirror implements Mirror { + private static final Logger logger = LoggerFactory.getLogger(AbstractMirror.class); + private static final CronDescriptor CRON_DESCRIPTOR = CronDescriptor.instance(); + private static final List FILE_VALIDATORS; + + static { + FILE_VALIDATORS = ImmutableList.copyOf(ServiceLoader.load(MirrorFileValidator.class)); + logger.debug("Available {}s: {}", MirrorFileValidator.class.getSimpleName(), FILE_VALIDATORS); + } + protected static final Author MIRROR_AUTHOR = new Author("Mirror", "mirror@localhost.localdomain"); protected static final String MIRROR_STATE_FILE_NAME = "mirror_state.json"; @@ -252,6 +269,39 @@ protected final IgnoreNode ignoreNode() { return ignoreNode; } + /** + * Validates the given changes using all registered {@link MirrorFileValidator}s before they are + * committed. Mirror state files are excluded from validation. + * + * @throws MirrorException if any change fails validation + */ + protected final void validateChanges(Map> changes) { + if (FILE_VALIDATORS.isEmpty()) { + return; + } + final String projectName = localRepo().parent().name(); + final String repoName = localRepo().name(); + final List errors = new ArrayList<>(); + for (Change change : changes.values()) { + // Skip mirror state files — they are internal bookkeeping, not user content. + if (change.path().endsWith(MIRROR_STATE_FILE_NAME)) { + continue; + } + for (MirrorFileValidator validator : FILE_VALIDATORS) { + try { + validator.validate(projectName, repoName, change); + } catch (MirrorException e) { + errors.add(e.getMessage()); + } + } + } + if (!errors.isEmpty()) { + throw new MirrorException( + "Mirror validation failed for '" + projectName + '/' + repoName + "':\n" + + String.join("\n", errors)); + } + } + /** * Filters the entries using gitignore patterns. Returns the entries as-is if no gitignore is configured. * The entries should be sorted by path so that directory entries come before their children. diff --git a/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/InternalProjectConstants.java b/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/InternalProjectConstants.java new file mode 100644 index 000000000..06bb5dff9 --- /dev/null +++ b/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/InternalProjectConstants.java @@ -0,0 +1,30 @@ +/* + * Copyright 2026 LINE Corporation + * + * LINE Corporation licenses this file to you 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: + * + * https://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 com.linecorp.centraldogma.server.internal.storage; + +/** + * Constants for internal project names. + */ +public final class InternalProjectConstants { + + /** + * The name of the internal project used by the xDS control plane. + */ + public static final String INTERNAL_PROJECT_XDS = "@xds"; + + private InternalProjectConstants() {} +} diff --git a/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/project/ProjectApiManager.java b/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/project/ProjectApiManager.java index cfa80abfd..7e72305c4 100644 --- a/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/project/ProjectApiManager.java +++ b/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/project/ProjectApiManager.java @@ -16,6 +16,7 @@ package com.linecorp.centraldogma.server.internal.storage.project; import static com.linecorp.centraldogma.internal.Util.INTERNAL_PROJECT_PREFIX; +import static com.linecorp.centraldogma.server.internal.storage.InternalProjectConstants.INTERNAL_PROJECT_XDS; import static com.linecorp.centraldogma.server.storage.project.InternalProjectInitializer.INTERNAL_PROJECT_DOGMA; import java.time.Instant; @@ -47,10 +48,6 @@ */ public final class ProjectApiManager { - // The xDS project is a self-service internal project: any authenticated user may access it (e.g. to list - // and create groups via the xDS web UI). It is intentionally more permissive than other internal projects. - private static final String INTERNAL_PROJECT_XDS = "@xds"; - private final ProjectManager projectManager; private final CommandExecutor commandExecutor; private final MetadataService metadataService; diff --git a/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/DefaultMetaRepository.java b/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/DefaultMetaRepository.java index 4ad2126d6..4c87b8bb2 100644 --- a/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/DefaultMetaRepository.java +++ b/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/DefaultMetaRepository.java @@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.linecorp.centraldogma.internal.CredentialUtil.credentialFile; +import static com.linecorp.centraldogma.server.internal.storage.InternalProjectConstants.INTERNAL_PROJECT_XDS; import static com.linecorp.centraldogma.server.internal.storage.repository.MirrorConverter.converterToMirrorConfig; import static java.util.Objects.requireNonNull; @@ -416,5 +417,11 @@ private static void validateMirror(MirrorRequest mirror, @Nullable ZoneConfig zo checkArgument(zoneConfig.allZones().contains(zone), "The zone '%s' is not in the zone configuration: %s", zone, zoneConfig); } + + if (INTERNAL_PROJECT_XDS.equals(mirror.projectName())) { + final String localPath = mirror.localPath(); + checkArgument("/".equals(localPath) || localPath.isEmpty(), + "xDS mirrors must use localPath '/', but got: %s", localPath); + } } } diff --git a/server/src/main/java/com/linecorp/centraldogma/server/mirror/MirrorFileValidator.java b/server/src/main/java/com/linecorp/centraldogma/server/mirror/MirrorFileValidator.java new file mode 100644 index 000000000..a2e638b89 --- /dev/null +++ b/server/src/main/java/com/linecorp/centraldogma/server/mirror/MirrorFileValidator.java @@ -0,0 +1,34 @@ +/* + * Copyright 2026 LY Corporation + * + * LY Corporation licenses this file to you 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: + * + * https://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 com.linecorp.centraldogma.server.mirror; + +import com.linecorp.centraldogma.common.Change; + +/** + * Validates file changes before they are committed to a repository during mirroring. + * + *

Implementations are loaded via {@link java.util.ServiceLoader} and invoked in + * {@link com.linecorp.centraldogma.server.internal.mirror.AbstractMirror} before each push.

+ */ +@FunctionalInterface +public interface MirrorFileValidator { + + /** + * Validates a file change before it is committed to a repository during mirroring. + */ + void validate(String projectName, String repoName, Change change); +} diff --git a/webapp/src/dogma/features/project/settings/credentials/CredentialForm.tsx b/webapp/src/dogma/features/project/settings/credentials/CredentialForm.tsx index 2cb30933f..8428bf010 100644 --- a/webapp/src/dogma/features/project/settings/credentials/CredentialForm.tsx +++ b/webapp/src/dogma/features/project/settings/credentials/CredentialForm.tsx @@ -50,6 +50,7 @@ interface CredentialFormProps { defaultValue: CredentialDto; onSubmit: (credential: CredentialDto, onSuccess: () => void) => Promise; isWaitingResponse: boolean; + hideScope?: boolean; } interface CredentialTypeWithDescription { @@ -70,6 +71,7 @@ const CredentialForm = ({ defaultValue, onSubmit, isWaitingResponse, + hideScope = false, }: CredentialFormProps) => { const [credentialType, setCredentialType] = useState(defaultValue.type); @@ -112,17 +114,18 @@ const CredentialForm = ({ {isNew ? 'New Credential' : 'Edit Credential'} - {repoName ? ( - - - {repoName} - - ) : ( - - - {projectName} - - )} + {!hideScope && + (repoName ? ( + + + {repoName} + + ) : ( + + + {projectName} + + ))} diff --git a/webapp/src/dogma/features/project/settings/credentials/CredentialList.tsx b/webapp/src/dogma/features/project/settings/credentials/CredentialList.tsx index 633e3cb26..ff971e581 100644 --- a/webapp/src/dogma/features/project/settings/credentials/CredentialList.tsx +++ b/webapp/src/dogma/features/project/settings/credentials/CredentialList.tsx @@ -13,6 +13,7 @@ export type CredentialListProps = { credentials: CredentialDto[]; deleteCredential: (projectName: string, id: string, repoName?: string) => Promise; isLoading: boolean; + buildDetailUrl?: (id: string) => string; }; const CredentialList = ({ @@ -21,6 +22,7 @@ const CredentialList = ({ credentials, deleteCredential, isLoading, + buildDetailUrl, }: CredentialListProps) => { const columnHelper = createColumnHelper(); const columns = useMemo( @@ -28,9 +30,11 @@ const CredentialList = ({ columnHelper.accessor((row: CredentialDto) => row.id, { cell: (info) => { const id = info.getValue() || 'undefined'; - const credentialLink = repoName - ? `/app/projects/${projectName}/repos/${repoName}/settings/credentials/${info.row.original.id}` - : `/app/projects/${projectName}/settings/credentials/${info.row.original.id}`; + const credentialLink = buildDetailUrl + ? buildDetailUrl(info.row.original.id) + : repoName + ? `/app/projects/${projectName}/repos/${repoName}/settings/credentials/${info.row.original.id}` + : `/app/projects/${projectName}/settings/credentials/${info.row.original.id}`; return ( {id} @@ -59,7 +63,7 @@ const CredentialList = ({ enableSorting: false, }), ], - [columnHelper, deleteCredential, isLoading, projectName, repoName], + [buildDetailUrl, columnHelper, deleteCredential, isLoading, projectName, repoName], ); return []} data={credentials || []} />; }; diff --git a/webapp/src/dogma/features/project/settings/credentials/CredentialView.tsx b/webapp/src/dogma/features/project/settings/credentials/CredentialView.tsx index e58eff295..465c336e5 100644 --- a/webapp/src/dogma/features/project/settings/credentials/CredentialView.tsx +++ b/webapp/src/dogma/features/project/settings/credentials/CredentialView.tsx @@ -106,11 +106,19 @@ interface CredentialViewProps { projectName: string; repoName?: string; credential: CredentialDto; + editUrl?: string; + hideScope?: boolean; } const AlignedIcon = ({ as }: { as: IconType }) => ; -const CredentialView = ({ projectName, repoName, credential }: CredentialViewProps) => { +const CredentialView = ({ + projectName, + repoName, + credential, + editUrl, + hideScope = false, +}: CredentialViewProps) => { const dispatch = useAppDispatch(); return ( @@ -129,21 +137,22 @@ const CredentialView = ({ projectName, repoName, credential }: CredentialViewPro - {repoName ? ( - - - Repository - - - - ) : ( - - - Project - - - - )} + {!hideScope && + (repoName ? ( + + + Repository + + + + ) : ( + + + Project + + + + ))} Credential ID @@ -231,7 +240,10 @@ const CredentialView = ({ projectName, repoName, credential }: CredentialViewPro
- ), - }), - ], - [onOpen], + const { data, isLoading, error } = useGetRepoCredentialsQuery( + { projectName: '@xds', repoName: group }, + { refetchOnMountOrArgChange: true }, ); - - // Only access token credentials are surfaced for now. Memoized so the table receives a stable data - // reference across re-renders (e.g. while typing in the add form), avoiding react-table re-render churn. - const credentials = useMemo(() => (data || []).filter((c) => c.type === 'ACCESS_TOKEN'), [data]); - const table = useReactTable({ - data: credentials, - columns, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), - }); + const [deleteCredentialMutation, { isLoading: isDeleting }] = useDeleteRepoCredentialMutation(); if (isLoading) { - return ; + return null; } if (error) { const status = (error as FetchBaseQueryError).status; @@ -144,58 +54,36 @@ export const CredentialsTab = ({ group }: { group: string }) => { return ( - - - Add an access token credential - - - - Credential ID - setNewId(e.target.value)} - /> - - - Access token - setNewToken(e.target.value)} - /> - - - - - - {credentials.length === 0 ? ( - No access token credentials in this group yet. - ) : ( - - )} - - + + + + { + try { + await deleteCredentialMutation({ projectName, id, repoName }).unwrap(); + dispatch(newNotification('Credential deleted', `Credential '${id}' is deleted`, 'success')); + } catch (err) { + dispatch( + newNotification('Failed to delete the credential', ErrorMessageParser.parse(err), 'error'), + ); + } + }} isLoading={isDeleting} + buildDetailUrl={(id) => + `/app/xds/credentials/${encodeURIComponent(id)}?group=${encodeURIComponent(group)}` + } /> ); diff --git a/webapp/src/dogma/features/xds/Sidebar.tsx b/webapp/src/dogma/features/xds/Sidebar.tsx index 4c48773a1..39d9ba4ab 100644 --- a/webapp/src/dogma/features/xds/Sidebar.tsx +++ b/webapp/src/dogma/features/xds/Sidebar.tsx @@ -26,6 +26,7 @@ import { MdHistory, MdDashboard, MdAccountTree, + MdSync, } from 'react-icons/md'; import { TbServer2, TbRouteSquare } from 'react-icons/tb'; import { SiKubernetes } from 'react-icons/si'; @@ -80,6 +81,7 @@ const SECTION_ICONS: Record = { permissions: MdLockOutline, dangerZone: MdWarningAmber, history: MdHistory, + mirroring: MdSync, }; export const Sidebar = () => { @@ -170,6 +172,15 @@ export const Sidebar = () => { active={section === 'history'} /> )} + {/* Mirror configuration for the group's backing repository, visible only to admins. */} + {!endpointsOnly && isAdmin && ( + + )} {/* Credentials and Permissions manage group-level access, so they are shown only to group admins. */} {!endpointsOnly && isAdmin && ( ( + + + + + `/app/xds/mirrors/${encodeURIComponent(id)}?group=${encodeURIComponent(group)}`} + hideRepoColumn + /> + +); diff --git a/webapp/src/dogma/features/xds/useXdsRoute.ts b/webapp/src/dogma/features/xds/useXdsRoute.ts index c373339ba..6cb506a51 100644 --- a/webapp/src/dogma/features/xds/useXdsRoute.ts +++ b/webapp/src/dogma/features/xds/useXdsRoute.ts @@ -24,6 +24,7 @@ export type XdsSection = | 'permissions' | 'dangerZone' | 'history' + | 'mirroring' | 'references'; export interface XdsRoute { @@ -46,6 +47,7 @@ export function useXdsRoute(): XdsRoute { type === 'credentials' || type === 'dangerZone' || type === 'history' || + type === 'mirroring' || type === 'references' || (type && Object.prototype.hasOwnProperty.call(XDS_RESOURCE_META, type)) ? (type as XdsSection) diff --git a/webapp/src/pages/app/xds/credentials/[id]/edit/index.tsx b/webapp/src/pages/app/xds/credentials/[id]/edit/index.tsx new file mode 100644 index 000000000..390ce6abd --- /dev/null +++ b/webapp/src/pages/app/xds/credentials/[id]/edit/index.tsx @@ -0,0 +1,118 @@ +/* + * Copyright 2026 LY Corporation + * + * LY Corporation licenses this file to you 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: + * + * https://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 { Breadcrumb, BreadcrumbItem, BreadcrumbLink } from '@chakra-ui/react'; +import { default as RouteLink } from 'next/link'; +import Router, { useRouter } from 'next/router'; +import { FetchBaseQueryError } from '@reduxjs/toolkit/query'; +import { SerializedError } from '@reduxjs/toolkit'; +import { useGetRepoCredentialQuery, useUpdateRepoCredentialMutation } from 'dogma/features/api/apiSlice'; +import { useAppDispatch } from 'dogma/hooks'; +import { newNotification } from 'dogma/features/notification/notificationSlice'; +import ErrorMessageParser from 'dogma/features/services/ErrorMessageParser'; +import { Deferred } from 'dogma/common/components/Deferred'; +import { CredentialDto } from 'dogma/features/project/settings/credentials/CredentialDto'; +import CredentialForm from 'dogma/features/project/settings/credentials/CredentialForm'; + +const XdsCredentialEditPage = () => { + const router = useRouter(); + const group = router.query.group as string | undefined; + const id = router.query.id as string | undefined; + + const { + data, + isLoading: isCredentialLoading, + error, + } = useGetRepoCredentialQuery({ projectName: '@xds', repoName: group, id }, { skip: !group || !id }); + const [updateCredential, { isLoading: isWaitingMutationResponse }] = useUpdateRepoCredentialMutation(); + const dispatch = useAppDispatch(); + + if (!group || !id) { + return null; + } + + const onSubmit = async (credential: CredentialDto, onSuccess: () => void) => { + try { + credential.name = `projects/@xds/repos/${group}/credentials/${credential.id}`; + const response = await updateCredential({ + projectName: '@xds', + id, + credential, + repoName: group, + }).unwrap(); + if ((response as { error: FetchBaseQueryError | SerializedError }).error) { + throw (response as { error: FetchBaseQueryError | SerializedError }).error; + } + dispatch(newNotification(`Credential '${credential.id}' is updated`, 'Successfully updated', 'success')); + onSuccess(); + Router.push(`/app/xds/credentials/${encodeURIComponent(id)}?group=${encodeURIComponent(group)}`); + } catch (error) { + dispatch(newNotification('Failed to update the credential', ErrorMessageParser.parse(error), 'error')); + } + }; + + return ( + + {() => ( + <> + + + + Groups + + + + + {group} + + + + + Credentials + + + + + {id} + + + + Edit + + + + + )} + + ); +}; + +export default XdsCredentialEditPage; diff --git a/webapp/src/pages/app/xds/credentials/[id]/index.tsx b/webapp/src/pages/app/xds/credentials/[id]/index.tsx new file mode 100644 index 000000000..1c5554bf0 --- /dev/null +++ b/webapp/src/pages/app/xds/credentials/[id]/index.tsx @@ -0,0 +1,83 @@ +/* + * Copyright 2026 LY Corporation + * + * LY Corporation licenses this file to you 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: + * + * https://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 { Breadcrumb, BreadcrumbItem, BreadcrumbLink, Flex, Spacer } from '@chakra-ui/react'; +import { default as RouteLink } from 'next/link'; +import { useRouter } from 'next/router'; +import { useGetRepoCredentialQuery } from 'dogma/features/api/apiSlice'; +import { Deferred } from 'dogma/common/components/Deferred'; +import CredentialView from 'dogma/features/project/settings/credentials/CredentialView'; + +const XdsCredentialViewPage = () => { + const router = useRouter(); + const group = router.query.group as string | undefined; + const id = router.query.id as string | undefined; + + const { data, isLoading, error } = useGetRepoCredentialQuery( + { projectName: '@xds', repoName: group!, id: id! }, + { skip: !group || !id }, + ); + + if (!group || !id) { + return null; + } + + return ( + + {() => ( + <> + + + + + Groups + + + + + {group} + + + + + Credentials + + + + {id} + + + + + + + )} + + ); +}; + +export default XdsCredentialViewPage; diff --git a/webapp/src/pages/app/xds/credentials/new.tsx b/webapp/src/pages/app/xds/credentials/new.tsx new file mode 100644 index 000000000..4b78cf8ef --- /dev/null +++ b/webapp/src/pages/app/xds/credentials/new.tsx @@ -0,0 +1,109 @@ +/* + * Copyright 2026 LY Corporation + * + * LY Corporation licenses this file to you 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: + * + * https://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 { Breadcrumb, BreadcrumbItem, BreadcrumbLink } from '@chakra-ui/react'; +import { default as RouteLink } from 'next/link'; +import Router, { useRouter } from 'next/router'; +import { FetchBaseQueryError } from '@reduxjs/toolkit/query'; +import { SerializedError } from '@reduxjs/toolkit'; +import { useAddNewRepoCredentialMutation } from 'dogma/features/api/apiSlice'; +import { useAppDispatch } from 'dogma/hooks'; +import { newNotification } from 'dogma/features/notification/notificationSlice'; +import ErrorMessageParser from 'dogma/features/services/ErrorMessageParser'; +import { + CredentialDto, + CreateCredentialRequestDto, +} from 'dogma/features/project/settings/credentials/CredentialDto'; +import CredentialForm from 'dogma/features/project/settings/credentials/CredentialForm'; + +const EMPTY_CREDENTIAL: CredentialDto = { + id: '', + name: '', + type: 'SSH_KEY', +}; + +const XdsNewCredentialPage = () => { + const router = useRouter(); + const group = router.query.group as string | undefined; + const dispatch = useAppDispatch(); + const [addNewCredential, { isLoading }] = useAddNewRepoCredentialMutation(); + + if (!group) { + return null; + } + + const onSubmit = async (credential: CredentialDto, onSuccess: () => void) => { + try { + const credentialRequest: CreateCredentialRequestDto = { + credentialId: credential.id, + credential: credential, + }; + const response = await addNewCredential({ + projectName: '@xds', + credentialRequest, + repoName: group, + }).unwrap(); + if ((response as { error: FetchBaseQueryError | SerializedError }).error) { + throw (response as { error: FetchBaseQueryError | SerializedError }).error; + } + dispatch(newNotification('New credential is created', 'Successfully created', 'success')); + onSuccess(); + Router.push(`/app/xds/group?name=${encodeURIComponent(group)}&type=credentials`); + } catch (error) { + dispatch(newNotification('Failed to create a new credential', ErrorMessageParser.parse(error), 'error')); + } + }; + + return ( + <> + + + + Groups + + + + + {group} + + + + + Credentials + + + + New Credential + + + + + ); +}; + +export default XdsNewCredentialPage; diff --git a/webapp/src/pages/app/xds/group.tsx b/webapp/src/pages/app/xds/group.tsx index 91715070c..f00e41c48 100644 --- a/webapp/src/pages/app/xds/group.tsx +++ b/webapp/src/pages/app/xds/group.tsx @@ -35,6 +35,7 @@ import { DangerZone } from 'dogma/features/xds/DangerZone'; import { ResourceHistory } from 'dogma/features/xds/ResourceHistory'; import { GroupOverview } from 'dogma/features/xds/GroupOverview'; import { ResourceReferences } from 'dogma/features/xds/ResourceReferences'; +import { XdsMirroringTab } from 'dogma/features/xds/XdsMirroringTab'; import { Loading } from 'dogma/common/components/Loading'; import { XDS_RESOURCE_META, XdsResourceType } from 'dogma/features/xds/XdsTypes'; import { useXdsRoute } from 'dogma/features/xds/useXdsRoute'; @@ -43,7 +44,7 @@ import { useGroupAdminAccess } from 'dogma/features/xds/useGroupAdminAccess'; import { useGroupExists } from 'dogma/features/xds/useGroupExists'; // Sections that manage group-level access and are therefore restricted to group admins. -const ADMIN_ONLY_SECTIONS = ['permissions', 'credentials', 'dangerZone']; +const ADMIN_ONLY_SECTIONS = ['permissions', 'credentials', 'dangerZone', 'mirroring']; const SECTION_TITLE: Record = { overview: 'Overview', @@ -52,6 +53,7 @@ const SECTION_TITLE: Record = { credentials: 'Credentials', dangerZone: 'Danger Zone', history: 'History', + mirroring: 'Mirroring', references: 'References', }; @@ -156,6 +158,8 @@ const GroupDetailPage = () => { ) : section === 'history' ? ( + ) : section === 'mirroring' ? ( + ) : section === 'references' ? ( ) : ( diff --git a/webapp/src/pages/app/xds/mirrors/[id]/edit/index.tsx b/webapp/src/pages/app/xds/mirrors/[id]/edit/index.tsx new file mode 100644 index 000000000..4ab83c101 --- /dev/null +++ b/webapp/src/pages/app/xds/mirrors/[id]/edit/index.tsx @@ -0,0 +1,127 @@ +/* + * Copyright 2026 LY Corporation + * + * LY Corporation licenses this file to you 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: + * + * https://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 { Breadcrumb, BreadcrumbItem, BreadcrumbLink } from '@chakra-ui/react'; +import { default as RouteLink } from 'next/link'; +import Router, { useRouter } from 'next/router'; +import { FetchBaseQueryError } from '@reduxjs/toolkit/query'; +import { SerializedError } from '@reduxjs/toolkit'; +import { useGetMirrorQuery, useUpdateMirrorMutation } from 'dogma/features/api/apiSlice'; +import { useAppDispatch } from 'dogma/hooks'; +import { newNotification } from 'dogma/features/notification/notificationSlice'; +import ErrorMessageParser from 'dogma/features/services/ErrorMessageParser'; +import { Deferred } from 'dogma/common/components/Deferred'; +import { MirrorDto, MirrorRequest } from 'dogma/features/repo/settings/mirrors/MirrorRequest'; +import MirrorForm from 'dogma/features/repo/settings/mirrors/MirrorForm'; + +const XdsMirrorEditPage = () => { + const router = useRouter(); + const group = router.query.group as string | undefined; + const id = router.query.id as string | undefined; + + const { data, isLoading, error } = useGetMirrorQuery( + { projectName: '@xds', repoName: group!, id: id! }, + { skip: !group || !id }, + ); + const [updateMirror, { isLoading: isWaitingMutationResponse }] = useUpdateMirrorMutation(); + const dispatch = useAppDispatch(); + + if (!group || !id) { + return null; + } + + const onSubmit = async (mirror: MirrorRequest | MirrorDto, onSuccess: () => void) => { + try { + let mirrorRequest: MirrorRequest; + if ('allow' in mirror) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { allow, ...rest } = mirror; + mirrorRequest = rest; + } else { + mirrorRequest = mirror; + } + mirrorRequest.projectName = '@xds'; + mirrorRequest.localRepo = group; + + const response = await updateMirror({ + projectName: '@xds', + repoName: group, + id, + mirror: mirrorRequest, + }).unwrap(); + if ((response as { error: FetchBaseQueryError | SerializedError }).error) { + throw (response as { error: FetchBaseQueryError | SerializedError }).error; + } + dispatch(newNotification(`Mirror '${mirror.id}' is updated`, 'Successfully updated', 'success')); + onSuccess(); + Router.push(`/app/xds/mirrors/${encodeURIComponent(id)}?group=${encodeURIComponent(group)}`); + } catch (error) { + dispatch(newNotification('Failed to update the mirror', ErrorMessageParser.parse(error), 'error')); + } + }; + + return ( + + {() => ( + <> + + + + Groups + + + + + {group} + + + + + Mirroring + + + + + {id} + + + + Edit + + + + + )} + + ); +}; + +export default XdsMirrorEditPage; diff --git a/webapp/src/pages/app/xds/mirrors/[id]/index.tsx b/webapp/src/pages/app/xds/mirrors/[id]/index.tsx new file mode 100644 index 000000000..04d26ac73 --- /dev/null +++ b/webapp/src/pages/app/xds/mirrors/[id]/index.tsx @@ -0,0 +1,80 @@ +/* + * Copyright 2026 LY Corporation + * + * LY Corporation licenses this file to you 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: + * + * https://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 { Breadcrumb, BreadcrumbItem, BreadcrumbLink } from '@chakra-ui/react'; +import { default as RouteLink } from 'next/link'; +import { useRouter } from 'next/router'; +import { useGetMirrorQuery } from 'dogma/features/api/apiSlice'; +import { Deferred } from 'dogma/common/components/Deferred'; +import MirrorView from 'dogma/features/repo/settings/mirrors/MirrorView'; + +const XdsMirrorViewPage = () => { + const router = useRouter(); + const group = router.query.group as string | undefined; + const id = router.query.id as string | undefined; + + const { + data: mirror, + isLoading, + error, + } = useGetMirrorQuery({ projectName: '@xds', repoName: group!, id: id! }, { skip: !group || !id }); + + if (!group || !id) { + return null; + } + + return ( + + {() => ( + <> + + + + Groups + + + + + {group} + + + + + Mirroring + + + + {id} + + + + + )} + + ); +}; + +export default XdsMirrorViewPage; diff --git a/webapp/src/pages/app/xds/mirrors/new.tsx b/webapp/src/pages/app/xds/mirrors/new.tsx new file mode 100644 index 000000000..910d52ede --- /dev/null +++ b/webapp/src/pages/app/xds/mirrors/new.tsx @@ -0,0 +1,119 @@ +/* + * Copyright 2026 LY Corporation + * + * LY Corporation licenses this file to you 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: + * + * https://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 { Breadcrumb, BreadcrumbItem, BreadcrumbLink } from '@chakra-ui/react'; +import { default as RouteLink } from 'next/link'; +import Router, { useRouter } from 'next/router'; +import { UseFormSetError } from 'react-hook-form'; +import { FetchBaseQueryError } from '@reduxjs/toolkit/query'; +import { SerializedError } from '@reduxjs/toolkit'; +import { useAddNewMirrorMutation } from 'dogma/features/api/apiSlice'; +import { useAppDispatch } from 'dogma/hooks'; +import { newNotification } from 'dogma/features/notification/notificationSlice'; +import ErrorMessageParser from 'dogma/features/services/ErrorMessageParser'; +import { MirrorRequest } from 'dogma/features/repo/settings/mirrors/MirrorRequest'; +import MirrorForm from 'dogma/features/repo/settings/mirrors/MirrorForm'; + +const XdsNewMirrorPage = () => { + const router = useRouter(); + const group = router.query.group as string | undefined; + const dispatch = useAppDispatch(); + const [addNewMirror, { isLoading }] = useAddNewMirrorMutation(); + + if (!group) { + return null; + } + + const emptyMirror: MirrorRequest = { + id: '', + direction: 'REMOTE_TO_LOCAL', + schedule: '0 * * * * ?', + projectName: '@xds', + localRepo: group, + localPath: '/', + remoteScheme: '', + remoteUrl: '', + remoteBranch: 'main', + remotePath: '/', + credentialName: null, + gitignore: null, + enabled: false, + }; + + const onSubmit = async ( + formData: MirrorRequest, + onSuccess: () => void, + setError: UseFormSetError, + ) => { + try { + formData.projectName = '@xds'; + formData.localRepo = group; + if (formData.remoteScheme.startsWith('git') && !formData.remoteUrl.endsWith('.git')) { + setError('remoteUrl', { type: 'manual', message: "The remote path must end with '.git'" }); + return; + } + const response = await addNewMirror(formData).unwrap(); + if ((response as { error: FetchBaseQueryError | SerializedError }).error) { + throw (response as { error: FetchBaseQueryError | SerializedError }).error; + } + dispatch(newNotification('New mirror is created', 'Successfully created', 'success')); + onSuccess(); + Router.push(`/app/xds/group?name=${encodeURIComponent(group)}&type=mirroring`); + } catch (error) { + dispatch(newNotification('Failed to create a new mirror', ErrorMessageParser.parse(error), 'error')); + } + }; + + return ( + <> + + + + Groups + + + + + {group} + + + + + Mirroring + + + + New Mirror + + + + + ); +}; + +export default XdsNewMirrorPage; diff --git a/xds/build.gradle b/xds/build.gradle index fc78e57a5..f613e1934 100644 --- a/xds/build.gradle +++ b/xds/build.gradle @@ -19,8 +19,8 @@ dependencies { testImplementation libs.logback15 testImplementation libs.testcontainers.junit.jupiter - // The xDS web UI is bundled into and served by the main ':webapp' (under '/app/xds'); the control plane - // plugin only exposes the xDS APIs and the '/api/v1/xds/web' enabled flag. + testImplementation project(":server-mirror-git") + testImplementation project(":server-mirror-dogma") testImplementation project(":server-auth:shiro") testImplementation libs.shiro.core } diff --git a/xds/src/main/java/com/linecorp/centraldogma/xds/endpoint/v1/XdsEndpointUpdateScheduler.java b/xds/src/main/java/com/linecorp/centraldogma/xds/endpoint/v1/XdsEndpointUpdateScheduler.java index 05942592e..77eb1fe67 100644 --- a/xds/src/main/java/com/linecorp/centraldogma/xds/endpoint/v1/XdsEndpointUpdateScheduler.java +++ b/xds/src/main/java/com/linecorp/centraldogma/xds/endpoint/v1/XdsEndpointUpdateScheduler.java @@ -15,8 +15,8 @@ */ package com.linecorp.centraldogma.xds.endpoint.v1; +import static com.linecorp.centraldogma.server.internal.storage.InternalProjectConstants.INTERNAL_PROJECT_XDS; import static com.linecorp.centraldogma.server.storage.repository.FindOptions.FIND_ONE_WITHOUT_CONTENT; -import static com.linecorp.centraldogma.xds.internal.ControlPlanePlugin.XDS_CENTRAL_DOGMA_PROJECT; import static com.linecorp.centraldogma.xds.internal.XdsResourceManager.JSON_MESSAGE_MARSHALLER; import java.io.IOException; @@ -216,7 +216,7 @@ private void flush() { return xdsResourceManager.commandExecutor() .execute(Command.push( Author.SYSTEM, - XDS_CENTRAL_DOGMA_PROJECT, group, + INTERNAL_PROJECT_XDS, group, Revision.HEAD, commitMessage, "", Markup.PLAINTEXT, changes)); }) @@ -255,7 +255,7 @@ private void executeYamlTransform(String commitMessage, List new ContentTransformer<>(fileName, EntryType.YAML, new BatchUpdateTransformer(toRegister, toDeregister)); xdsResourceManager.commandExecutor() - .execute(Command.transform(null, Author.SYSTEM, XDS_CENTRAL_DOGMA_PROJECT, + .execute(Command.transform(null, Author.SYSTEM, INTERNAL_PROJECT_XDS, group, Revision.HEAD, commitMessage, "", Markup.PLAINTEXT, transformer)) .handle((result, cause) -> { diff --git a/xds/src/main/java/com/linecorp/centraldogma/xds/group/v1/XdsGroupService.java b/xds/src/main/java/com/linecorp/centraldogma/xds/group/v1/XdsGroupService.java index 9b2e2d47d..16b0e162d 100644 --- a/xds/src/main/java/com/linecorp/centraldogma/xds/group/v1/XdsGroupService.java +++ b/xds/src/main/java/com/linecorp/centraldogma/xds/group/v1/XdsGroupService.java @@ -20,7 +20,7 @@ import static com.linecorp.centraldogma.server.internal.admin.auth.AuthUtil.getAuthor; import static com.linecorp.centraldogma.server.internal.api.RepositoryServiceUtil.createRepository; import static com.linecorp.centraldogma.server.internal.api.RepositoryServiceUtil.removeRepository; -import static com.linecorp.centraldogma.xds.internal.ControlPlanePlugin.XDS_CENTRAL_DOGMA_PROJECT; +import static com.linecorp.centraldogma.server.internal.storage.InternalProjectConstants.INTERNAL_PROJECT_XDS; import static com.linecorp.centraldogma.xds.internal.XdsResourceManager.checkGroupId; import static com.linecorp.centraldogma.xds.internal.XdsResourceManager.removePrefix; @@ -65,7 +65,7 @@ public void createGroup(CreateGroupRequest request, if (xdsProject.repos().exists(groupId)) { throw alreadyExistsException(groupId); } - createRepository(commandExecutor, mds, currentAuthor(), XDS_CENTRAL_DOGMA_PROJECT, groupId, false, null) + createRepository(commandExecutor, mds, currentAuthor(), INTERNAL_PROJECT_XDS, groupId, false, null) .handle((unused, cause) -> { if (cause != null) { final Throwable peeled = Exceptions.peel(cause); @@ -116,7 +116,7 @@ public void deleteGroup(DeleteGroupRequest request, StreamObserver respon .asRuntimeException()); return; } - removeRepository(commandExecutor, mds, getAuthor(user), XDS_CENTRAL_DOGMA_PROJECT, name) + removeRepository(commandExecutor, mds, getAuthor(user), INTERNAL_PROJECT_XDS, name) .handle((unused, cause1) -> { if (cause1 != null) { responseObserver.onError( diff --git a/xds/src/main/java/com/linecorp/centraldogma/xds/internal/ControlPlanePlugin.java b/xds/src/main/java/com/linecorp/centraldogma/xds/internal/ControlPlanePlugin.java index 751bb7453..3774c78ea 100644 --- a/xds/src/main/java/com/linecorp/centraldogma/xds/internal/ControlPlanePlugin.java +++ b/xds/src/main/java/com/linecorp/centraldogma/xds/internal/ControlPlanePlugin.java @@ -16,6 +16,8 @@ package com.linecorp.centraldogma.xds.internal; +import static com.linecorp.centraldogma.server.internal.storage.InternalProjectConstants.INTERNAL_PROJECT_XDS; + import java.util.concurrent.CompletionStage; import org.jspecify.annotations.Nullable; @@ -31,17 +33,15 @@ public final class ControlPlanePlugin extends AllReplicasPlugin { - public static final String XDS_CENTRAL_DOGMA_PROJECT = "@xds"; - @Nullable private volatile ControlPlaneService controlPlaneService; @Override public void init(PluginInitContext pluginInitContext) { final InternalProjectInitializer projectInitializer = pluginInitContext.internalProjectInitializer(); - projectInitializer.initialize(XDS_CENTRAL_DOGMA_PROJECT); + projectInitializer.initialize(INTERNAL_PROJECT_XDS); final ControlPlaneService controlPlaneService = new ControlPlaneService( - pluginInitContext.projectManager().get(XDS_CENTRAL_DOGMA_PROJECT), + pluginInitContext.projectManager().get(INTERNAL_PROJECT_XDS), pluginInitContext.meterRegistry()); this.controlPlaneService = controlPlaneService; controlPlaneService.start(pluginInitContext); diff --git a/xds/src/main/java/com/linecorp/centraldogma/xds/internal/XdsMirrorFileValidator.java b/xds/src/main/java/com/linecorp/centraldogma/xds/internal/XdsMirrorFileValidator.java new file mode 100644 index 000000000..1c906d901 --- /dev/null +++ b/xds/src/main/java/com/linecorp/centraldogma/xds/internal/XdsMirrorFileValidator.java @@ -0,0 +1,118 @@ +/* + * Copyright 2026 LY Corporation + * + * LY Corporation licenses this file to you 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: + * + * https://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 com.linecorp.centraldogma.xds.internal; + +import static com.linecorp.centraldogma.server.internal.storage.InternalProjectConstants.INTERNAL_PROJECT_XDS; +import static com.linecorp.centraldogma.xds.internal.ControlPlaneService.CLUSTERS_DIRECTORY; +import static com.linecorp.centraldogma.xds.internal.ControlPlaneService.ENDPOINTS_DIRECTORY; +import static com.linecorp.centraldogma.xds.internal.ControlPlaneService.K8S_ENDPOINTS_DIRECTORY; +import static com.linecorp.centraldogma.xds.internal.ControlPlaneService.LISTENERS_DIRECTORY; +import static com.linecorp.centraldogma.xds.internal.ControlPlaneService.ROUTES_DIRECTORY; +import static com.linecorp.centraldogma.xds.internal.XdsResourceManager.JSON_MESSAGE_MARSHALLER; +import static com.linecorp.centraldogma.xds.k8s.v1.XdsKubernetesService.K8S_ENDPOINT_AGGREGATORS_DIRECTORY; + +import java.io.IOException; + +import org.jspecify.annotations.Nullable; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.protobuf.Message; + +import com.linecorp.centraldogma.common.Change; +import com.linecorp.centraldogma.common.ChangeType; +import com.linecorp.centraldogma.common.MirrorException; +import com.linecorp.centraldogma.server.mirror.MirrorFileValidator; +import com.linecorp.centraldogma.xds.k8s.v1.KubernetesEndpointAggregator; + +import io.envoyproxy.envoy.config.cluster.v3.Cluster; +import io.envoyproxy.envoy.config.endpoint.v3.ClusterLoadAssignment; +import io.envoyproxy.envoy.config.listener.v3.Listener; +import io.envoyproxy.envoy.config.route.v3.RouteConfiguration; + +/** + * Validates xDS resource files before they are mirrored into an {@code @xds} repository. + * + *

Each file path determines the expected xDS resource type. Files at unrecognized paths or + * paths reserved for system-managed resources (e.g. {@code /k8s/endpoints/}) cause an error. + * The content must be parseable as the expected protobuf message type without unknown fields.

+ */ +public final class XdsMirrorFileValidator implements MirrorFileValidator { + + @Override + public void validate(String projectName, String repoName, Change change) { + if (!INTERNAL_PROJECT_XDS.equals(projectName)) { + return; + } + if (change.type() == ChangeType.REMOVE) { + return; + } + + final String filePath = change.path(); + + // /k8s/endpoints/ files are written by the Kubernetes controller, not by users. + if (filePath.startsWith(K8S_ENDPOINTS_DIRECTORY)) { + throw new MirrorException( + filePath + ": files under " + K8S_ENDPOINTS_DIRECTORY + + " are managed by the Kubernetes controller and cannot be created via mirroring"); + } + + if (!filePath.endsWith(".yaml")) { + throw new MirrorException("file must be ends with '.yaml'. file: " + filePath); + } + + final Message.Builder builder = builderForPath(filePath); + if (builder == null) { + throw new MirrorException( + filePath + ": unexpected file path in an xDS repository; " + + "only " + CLUSTERS_DIRECTORY + ", " + LISTENERS_DIRECTORY + ", " + + ROUTES_DIRECTORY + ", " + ENDPOINTS_DIRECTORY + ", and " + + K8S_ENDPOINT_AGGREGATORS_DIRECTORY + " are allowed"); + } + + try { + final Object content = change.content(); + if (content instanceof JsonNode) { + // Use JsonNode.traverse() so both JSON and YAML changes are handled uniformly: + // YAML is already parsed into a JsonNode, and traverse() produces JSON tokens. + JSON_MESSAGE_MARSHALLER.mergeValue(((JsonNode) content).traverse(), builder); + } else { + JSON_MESSAGE_MARSHALLER.mergeValue(change.contentAsText(), builder); + } + } catch (IOException e) { + throw new MirrorException( + filePath + ": not a valid " + builder.getDescriptorForType().getName(), e); + } + } + + private static Message.@Nullable Builder builderForPath(String filePath) { + if (filePath.startsWith(CLUSTERS_DIRECTORY)) { + return Cluster.newBuilder(); + } + if (filePath.startsWith(LISTENERS_DIRECTORY)) { + return Listener.newBuilder(); + } + if (filePath.startsWith(ROUTES_DIRECTORY)) { + return RouteConfiguration.newBuilder(); + } + if (filePath.startsWith(ENDPOINTS_DIRECTORY)) { + return ClusterLoadAssignment.newBuilder(); + } + if (filePath.startsWith(K8S_ENDPOINT_AGGREGATORS_DIRECTORY)) { + return KubernetesEndpointAggregator.newBuilder(); + } + return null; + } +} diff --git a/xds/src/main/java/com/linecorp/centraldogma/xds/internal/XdsResourceManager.java b/xds/src/main/java/com/linecorp/centraldogma/xds/internal/XdsResourceManager.java index d70b9b386..d52defe6f 100644 --- a/xds/src/main/java/com/linecorp/centraldogma/xds/internal/XdsResourceManager.java +++ b/xds/src/main/java/com/linecorp/centraldogma/xds/internal/XdsResourceManager.java @@ -16,8 +16,8 @@ package com.linecorp.centraldogma.xds.internal; import static com.linecorp.centraldogma.internal.Util.PROJECT_AND_REPO_NAME_PATTERN; +import static com.linecorp.centraldogma.server.internal.storage.InternalProjectConstants.INTERNAL_PROJECT_XDS; import static com.linecorp.centraldogma.server.storage.repository.FindOptions.FIND_ONE_WITHOUT_CONTENT; -import static com.linecorp.centraldogma.xds.internal.ControlPlanePlugin.XDS_CENTRAL_DOGMA_PROJECT; import static java.util.Objects.requireNonNull; import java.io.IOException; @@ -232,7 +232,7 @@ private void doPush( final ImmutableList> changes = legacyFileToRemove != null ? ImmutableList.of(Change.ofRemoval(legacyFileToRemove), change) : ImmutableList.of(change); - commandExecutor.execute(Command.push(author, XDS_CENTRAL_DOGMA_PROJECT, group, Revision.HEAD, + commandExecutor.execute(Command.push(author, INTERNAL_PROJECT_XDS, group, Revision.HEAD, summary, "", Markup.PLAINTEXT, changes)) .handle((unused, cause) -> { if (cause != null) { @@ -289,7 +289,7 @@ public void delete(StreamObserver responseObserver, String group, public void delete(StreamObserver responseObserver, String group, String resourceName, String fileName, String summary, Author author) { updateOrDelete(responseObserver, group, resourceName, fileName, resolvedFileName -> - commandExecutor.execute(Command.push(author, XDS_CENTRAL_DOGMA_PROJECT, group, + commandExecutor.execute(Command.push(author, INTERNAL_PROJECT_XDS, group, Revision.HEAD, summary, "", Markup.PLAINTEXT, ImmutableList.of(Change.ofRemoval(resolvedFileName)))) .handle((unused, cause) -> { diff --git a/xds/src/main/java/com/linecorp/centraldogma/xds/k8s/v1/XdsKubernetesEndpointFetchingPlugin.java b/xds/src/main/java/com/linecorp/centraldogma/xds/k8s/v1/XdsKubernetesEndpointFetchingPlugin.java index 556dc0718..2362e4a43 100644 --- a/xds/src/main/java/com/linecorp/centraldogma/xds/k8s/v1/XdsKubernetesEndpointFetchingPlugin.java +++ b/xds/src/main/java/com/linecorp/centraldogma/xds/k8s/v1/XdsKubernetesEndpointFetchingPlugin.java @@ -15,7 +15,7 @@ */ package com.linecorp.centraldogma.xds.k8s.v1; -import static com.linecorp.centraldogma.xds.internal.ControlPlanePlugin.XDS_CENTRAL_DOGMA_PROJECT; +import static com.linecorp.centraldogma.server.internal.storage.InternalProjectConstants.INTERNAL_PROJECT_XDS; import static io.fabric8.kubernetes.client.Config.KUBERNETES_DISABLE_AUTO_CONFIG_SYSTEM_PROPERTY; import static java.util.Objects.requireNonNull; @@ -54,10 +54,10 @@ public synchronized CompletionStage start(PluginContext context) { if (fetchingService != null) { return UnmodifiableFuture.completedFuture(null); } - context.internalProjectInitializer().initialize(XDS_CENTRAL_DOGMA_PROJECT); + context.internalProjectInitializer().initialize(INTERNAL_PROJECT_XDS); fetchingService = new XdsKubernetesEndpointFetchingService( - context.projectManager().get(XDS_CENTRAL_DOGMA_PROJECT), context.commandExecutor(), + context.projectManager().get(INTERNAL_PROJECT_XDS), context.commandExecutor(), context.meterRegistry()); fetchingService.start(); return UnmodifiableFuture.completedFuture(null); diff --git a/xds/src/main/java/com/linecorp/centraldogma/xds/k8s/v1/XdsKubernetesEndpointFetchingService.java b/xds/src/main/java/com/linecorp/centraldogma/xds/k8s/v1/XdsKubernetesEndpointFetchingService.java index 99d9c1506..dc5556423 100644 --- a/xds/src/main/java/com/linecorp/centraldogma/xds/k8s/v1/XdsKubernetesEndpointFetchingService.java +++ b/xds/src/main/java/com/linecorp/centraldogma/xds/k8s/v1/XdsKubernetesEndpointFetchingService.java @@ -16,8 +16,8 @@ package com.linecorp.centraldogma.xds.k8s.v1; import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.linecorp.centraldogma.server.internal.storage.InternalProjectConstants.INTERNAL_PROJECT_XDS; import static com.linecorp.centraldogma.server.storage.repository.FindOptions.FIND_ONE_WITHOUT_CONTENT; -import static com.linecorp.centraldogma.xds.internal.ControlPlanePlugin.XDS_CENTRAL_DOGMA_PROJECT; import static com.linecorp.centraldogma.xds.internal.ControlPlaneService.K8S_ENDPOINTS_DIRECTORY; import static com.linecorp.centraldogma.xds.internal.XdsResourceManager.JSON_MESSAGE_MARSHALLER; import static com.linecorp.centraldogma.xds.k8s.v1.XdsKubernetesService.AGGREGATORS_REPLCACE_PATTERN; @@ -219,7 +219,7 @@ protected void onFileRemoved(String groupName, String path) { private void removeEndpointFile(String groupName, String endpointPath) { commandExecutor.execute( - Command.push(Author.SYSTEM, XDS_CENTRAL_DOGMA_PROJECT, groupName, Revision.HEAD, + Command.push(Author.SYSTEM, INTERNAL_PROJECT_XDS, groupName, Revision.HEAD, "Remove " + endpointPath, "", Markup.PLAINTEXT, Change.ofRemoval(endpointPath))).handle((unused, cause) -> { if (cause != null) { @@ -365,7 +365,7 @@ private void pushK8sEndpoints() { final String legacyFileName = K8S_ENDPOINTS_DIRECTORY + aggregatorId + ".json"; final List> changes = ImmutableList.of(Change.ofRemoval(legacyFileName), yamlChange); commandExecutor.execute( - Command.push(Author.SYSTEM, XDS_CENTRAL_DOGMA_PROJECT, groupName, Revision.HEAD, + Command.push(Author.SYSTEM, INTERNAL_PROJECT_XDS, groupName, Revision.HEAD, "Add " + aggregator.getClusterName() + '.', "", Markup.PLAINTEXT, changes)).handle((unused, cause) -> { if (cause != null) { @@ -393,7 +393,7 @@ private void pushK8sEndpoints() { private void pushYamlChange(Change change) { commandExecutor.execute( - Command.push(Author.SYSTEM, XDS_CENTRAL_DOGMA_PROJECT, groupName, Revision.HEAD, + Command.push(Author.SYSTEM, INTERNAL_PROJECT_XDS, groupName, Revision.HEAD, "Add " + aggregator.getClusterName() + '.', "", Markup.PLAINTEXT, change)).handle((unused, cause) -> { if (cause != null) { diff --git a/xds/src/main/java/com/linecorp/centraldogma/xds/k8s/v1/XdsKubernetesService.java b/xds/src/main/java/com/linecorp/centraldogma/xds/k8s/v1/XdsKubernetesService.java index 488cbe942..204c84669 100644 --- a/xds/src/main/java/com/linecorp/centraldogma/xds/k8s/v1/XdsKubernetesService.java +++ b/xds/src/main/java/com/linecorp/centraldogma/xds/k8s/v1/XdsKubernetesService.java @@ -18,7 +18,7 @@ import static com.google.common.base.Strings.isNullOrEmpty; import static com.linecorp.centraldogma.internal.CredentialUtil.credentialName; import static com.linecorp.centraldogma.server.internal.admin.auth.AuthUtil.currentAuthor; -import static com.linecorp.centraldogma.xds.internal.ControlPlanePlugin.XDS_CENTRAL_DOGMA_PROJECT; +import static com.linecorp.centraldogma.server.internal.storage.InternalProjectConstants.INTERNAL_PROJECT_XDS; import static com.linecorp.centraldogma.xds.internal.XdsResourceManager.RESOURCE_ID_PATTERN; import static com.linecorp.centraldogma.xds.internal.XdsResourceManager.RESOURCE_ID_PATTERN_STRING; import static com.linecorp.centraldogma.xds.internal.XdsResourceManager.fileName; @@ -90,7 +90,7 @@ public final class XdsKubernetesService extends XdsKubernetesServiceImplBase { } } - static final String K8S_ENDPOINT_AGGREGATORS_DIRECTORY = "/k8s/endpointAggregators/"; + public static final String K8S_ENDPOINT_AGGREGATORS_DIRECTORY = "/k8s/endpointAggregators/"; public static final Pattern AGGREGATORS_REPLCACE_PATTERN = Pattern.compile("(?<=/k8s)/endpointAggregators/"); @@ -310,7 +310,7 @@ private static CompletableFuture toConfig(Kubeconfig kubeconfig, MetaRep } final CompletableFuture future = new CompletableFuture<>(); // xDS only support repository credential so try using it first. - metaRepository.credential(credentialName(XDS_CENTRAL_DOGMA_PROJECT, group, credentialId)) + metaRepository.credential(credentialName(INTERNAL_PROJECT_XDS, group, credentialId)) .thenAccept(credential -> { if (!(credential instanceof AccessTokenCredential)) { future.completeExceptionally(new IllegalArgumentException( @@ -324,7 +324,7 @@ private static CompletableFuture toConfig(Kubeconfig kubeconfig, MetaRep final Throwable peeled = Exceptions.peel(cause); if (peeled instanceof EntryNotFoundException) { // Try to use the legacy project credential for backward compatibility. - metaRepository.credential(credentialName(XDS_CENTRAL_DOGMA_PROJECT, credentialId)) + metaRepository.credential(credentialName(INTERNAL_PROJECT_XDS, credentialId)) .handle((credential, cause1) -> { if (cause1 != null) { future.completeExceptionally(cause1); diff --git a/xds/src/main/resources/META-INF/services/com.linecorp.centraldogma.server.mirror.MirrorFileValidator b/xds/src/main/resources/META-INF/services/com.linecorp.centraldogma.server.mirror.MirrorFileValidator new file mode 100644 index 000000000..aee41a9fb --- /dev/null +++ b/xds/src/main/resources/META-INF/services/com.linecorp.centraldogma.server.mirror.MirrorFileValidator @@ -0,0 +1 @@ +com.linecorp.centraldogma.xds.internal.XdsMirrorFileValidator diff --git a/xds/src/test/java/com/linecorp/centraldogma/xds/endpoint/v1/XdsEndpointServiceTest.java b/xds/src/test/java/com/linecorp/centraldogma/xds/endpoint/v1/XdsEndpointServiceTest.java index 7a652e5d3..b253e7316 100644 --- a/xds/src/test/java/com/linecorp/centraldogma/xds/endpoint/v1/XdsEndpointServiceTest.java +++ b/xds/src/test/java/com/linecorp/centraldogma/xds/endpoint/v1/XdsEndpointServiceTest.java @@ -15,7 +15,7 @@ */ package com.linecorp.centraldogma.xds.endpoint.v1; -import static com.linecorp.centraldogma.xds.internal.ControlPlanePlugin.XDS_CENTRAL_DOGMA_PROJECT; +import static com.linecorp.centraldogma.server.internal.storage.InternalProjectConstants.INTERNAL_PROJECT_XDS; import static com.linecorp.centraldogma.xds.internal.ControlPlaneService.ENDPOINTS_DIRECTORY; import static com.linecorp.centraldogma.xds.internal.XdsResourceManager.JSON_MESSAGE_MARSHALLER; import static com.linecorp.centraldogma.xds.internal.XdsTestUtil.createEndpoint; @@ -182,7 +182,7 @@ void createEndpointReturnAlreadyExistsWhenYamlExists() throws Exception { // Pre-populate the repo with a YAML endpoint (simulating a JSON→YAML migration). final String clusterName = "groups/foo/clusters/yaml-exists/1"; final ClusterLoadAssignment initial = loadAssignment(clusterName, "127.0.0.1", 8080); - dogma.client().forRepo(XDS_CENTRAL_DOGMA_PROJECT, "foo") + dogma.client().forRepo(INTERNAL_PROJECT_XDS, "foo") .commit("Add YAML endpoint", Change.ofYamlUpsert(ENDPOINTS_DIRECTORY + "yaml-exists/1.yaml", JSON_MESSAGE_MARSHALLER.writeValueAsString(initial))) @@ -197,7 +197,7 @@ void createEndpointReturnAlreadyExistsWhenYamlExists() throws Exception { // The original .yaml file must still be the only file present (no new .json created). final Repository repo = - dogma.projectManager().get(XDS_CENTRAL_DOGMA_PROJECT).repos().get("foo"); + dogma.projectManager().get(INTERNAL_PROJECT_XDS).repos().get("foo"); assertThat(repo.find(Revision.HEAD, ENDPOINTS_DIRECTORY + "yaml-exists/1.yaml", FindOptions.FIND_ONE_WITHOUT_CONTENT).join()).isNotEmpty(); assertThat(repo.find(Revision.HEAD, ENDPOINTS_DIRECTORY + "yaml-exists/1.json", @@ -210,7 +210,7 @@ void updateYamlEndpointViaHttp() throws Exception { final String clusterName = "groups/foo/clusters/yaml-endpoint/1"; final String endpointName = "groups/foo/endpoints/yaml-endpoint/1"; final ClusterLoadAssignment initial = loadAssignment(clusterName, "127.0.0.1", 8080); - dogma.client().forRepo(XDS_CENTRAL_DOGMA_PROJECT, "foo") + dogma.client().forRepo(INTERNAL_PROJECT_XDS, "foo") .commit("Add YAML endpoint", Change.ofYamlUpsert(ENDPOINTS_DIRECTORY + "yaml-endpoint/1.yaml", JSON_MESSAGE_MARSHALLER.writeValueAsString(initial))) @@ -228,7 +228,7 @@ void updateYamlEndpointViaHttp() throws Exception { // The .yaml file must have been updated in-place; no new .json file should exist. final Repository repo = - dogma.projectManager().get(XDS_CENTRAL_DOGMA_PROJECT).repos().get("foo"); + dogma.projectManager().get(INTERNAL_PROJECT_XDS).repos().get("foo"); assertThat(repo.find(Revision.HEAD, ENDPOINTS_DIRECTORY + "yaml-endpoint/1.yaml", FindOptions.FIND_ONE_WITHOUT_CONTENT).join()).isNotEmpty(); assertThat(repo.find(Revision.HEAD, ENDPOINTS_DIRECTORY + "yaml-endpoint/1.json", @@ -245,7 +245,7 @@ void deleteYamlEndpointViaHttp() throws Exception { final String clusterName = "groups/foo/clusters/yaml-endpoint/2"; final String endpointName = "groups/foo/endpoints/yaml-endpoint/2"; final ClusterLoadAssignment initial = loadAssignment(clusterName, "127.0.0.1", 8080); - dogma.client().forRepo(XDS_CENTRAL_DOGMA_PROJECT, "foo") + dogma.client().forRepo(INTERNAL_PROJECT_XDS, "foo") .commit("Add YAML endpoint", Change.ofYamlUpsert(ENDPOINTS_DIRECTORY + "yaml-endpoint/2.yaml", JSON_MESSAGE_MARSHALLER.writeValueAsString(initial))) @@ -259,7 +259,7 @@ void deleteYamlEndpointViaHttp() throws Exception { // The .yaml file must be gone. final Repository repo = - dogma.projectManager().get(XDS_CENTRAL_DOGMA_PROJECT).repos().get("foo"); + dogma.projectManager().get(INTERNAL_PROJECT_XDS).repos().get("foo"); assertThat(repo.find(Revision.HEAD, ENDPOINTS_DIRECTORY + "yaml-endpoint/2.yaml", FindOptions.FIND_ONE_WITHOUT_CONTENT).join()).isEmpty(); diff --git a/xds/src/test/java/com/linecorp/centraldogma/xds/endpoint/v1/XdsRegisterEndpointTest.java b/xds/src/test/java/com/linecorp/centraldogma/xds/endpoint/v1/XdsRegisterEndpointTest.java index b5ec92b10..df0fb7c45 100644 --- a/xds/src/test/java/com/linecorp/centraldogma/xds/endpoint/v1/XdsRegisterEndpointTest.java +++ b/xds/src/test/java/com/linecorp/centraldogma/xds/endpoint/v1/XdsRegisterEndpointTest.java @@ -15,9 +15,9 @@ */ package com.linecorp.centraldogma.xds.endpoint.v1; +import static com.linecorp.centraldogma.server.internal.storage.InternalProjectConstants.INTERNAL_PROJECT_XDS; import static com.linecorp.centraldogma.xds.endpoint.v1.XdsEndpointServiceTest.assertOk; import static com.linecorp.centraldogma.xds.endpoint.v1.XdsEndpointServiceTest.checkEndpointsViaDiscoveryRequest; -import static com.linecorp.centraldogma.xds.internal.ControlPlanePlugin.XDS_CENTRAL_DOGMA_PROJECT; import static com.linecorp.centraldogma.xds.internal.ControlPlaneService.ENDPOINTS_DIRECTORY; import static com.linecorp.centraldogma.xds.internal.XdsResourceManager.JSON_MESSAGE_MARSHALLER; import static com.linecorp.centraldogma.xds.internal.XdsTestUtil.createEndpoint; @@ -77,7 +77,7 @@ void registerOrDeregister() throws Exception { checkEndpointsViaDiscoveryRequest(dogma.httpClient().uri(), endpoint, clusterName); final Repository fooRepository = - dogma.projectManager().get(XDS_CENTRAL_DOGMA_PROJECT).repos().get("foo"); + dogma.projectManager().get(INTERNAL_PROJECT_XDS).repos().get("foo"); int prevMajor = fooRepository.normalizeNow(Revision.HEAD).major(); // Register endpoints to the same locality endpoint. @@ -273,13 +273,13 @@ void registerAndDeregisterMigratesLegacyJsonFile() throws Exception { .build(); final JsonNode jsonNode = Jackson.readTree( JSON_MESSAGE_MARSHALLER.writeValueAsString(initialAssignment)); - dogma.client().forRepo(XDS_CENTRAL_DOGMA_PROJECT, "foo") + dogma.client().forRepo(INTERNAL_PROJECT_XDS, "foo") .commit("Add legacy JSON endpoint", Change.ofJsonUpsert(ENDPOINTS_DIRECTORY + endpointId + ".json", jsonNode)) .push().join(); final Repository fooRepository = - dogma.projectManager().get(XDS_CENTRAL_DOGMA_PROJECT).repos().get("foo"); + dogma.projectManager().get(INTERNAL_PROJECT_XDS).repos().get("foo"); // Register a new endpoint — flush() discovers the .json file and migrates it to .yaml atomically. final LocalityLbEndpoint newEndpoint = @@ -344,7 +344,7 @@ void registerAndDeregisterOnYamlEndpoint() throws Exception { final Locality locality = Locality.newBuilder().setRegion("r1").setZone("z1").build(); final ClusterLoadAssignment initial = loadAssignment(clusterName, locality, endpoint("127.0.0.1", 9100)); - dogma.client().forRepo(XDS_CENTRAL_DOGMA_PROJECT, "foo") + dogma.client().forRepo(INTERNAL_PROJECT_XDS, "foo") .commit("Add YAML endpoint", Change.ofYamlUpsert(ENDPOINTS_DIRECTORY + "yaml-register-ep.yaml", JSON_MESSAGE_MARSHALLER.writeValueAsString(initial))) diff --git a/xds/src/test/java/com/linecorp/centraldogma/xds/internal/CreatingInternalGroupPlugin.java b/xds/src/test/java/com/linecorp/centraldogma/xds/internal/CreatingInternalGroupPlugin.java index a6774632c..0176fa334 100644 --- a/xds/src/test/java/com/linecorp/centraldogma/xds/internal/CreatingInternalGroupPlugin.java +++ b/xds/src/test/java/com/linecorp/centraldogma/xds/internal/CreatingInternalGroupPlugin.java @@ -15,7 +15,7 @@ */ package com.linecorp.centraldogma.xds.internal; -import static com.linecorp.centraldogma.xds.internal.ControlPlanePlugin.XDS_CENTRAL_DOGMA_PROJECT; +import static com.linecorp.centraldogma.server.internal.storage.InternalProjectConstants.INTERNAL_PROJECT_XDS; import java.util.concurrent.CompletionStage; @@ -36,12 +36,12 @@ public final class CreatingInternalGroupPlugin extends AllReplicasPlugin { @Override public void init(PluginInitContext pluginInitContext) { - pluginInitContext.internalProjectInitializer().initialize(XDS_CENTRAL_DOGMA_PROJECT); + pluginInitContext.internalProjectInitializer().initialize(INTERNAL_PROJECT_XDS); final MetadataService mds = new MetadataService(pluginInitContext.projectManager(), pluginInitContext.commandExecutor(), pluginInitContext.internalProjectInitializer()); RepositoryServiceUtil.createRepository(pluginInitContext.commandExecutor(), mds, Author.SYSTEM, - XDS_CENTRAL_DOGMA_PROJECT, "my-group", false, null) + INTERNAL_PROJECT_XDS, "my-group", false, null) .join(); } diff --git a/xds/src/test/java/com/linecorp/centraldogma/xds/internal/XdsEndpointReadServiceTest.java b/xds/src/test/java/com/linecorp/centraldogma/xds/internal/XdsEndpointReadServiceTest.java index c0eecb8bb..b0afb6159 100644 --- a/xds/src/test/java/com/linecorp/centraldogma/xds/internal/XdsEndpointReadServiceTest.java +++ b/xds/src/test/java/com/linecorp/centraldogma/xds/internal/XdsEndpointReadServiceTest.java @@ -15,7 +15,7 @@ */ package com.linecorp.centraldogma.xds.internal; -import static com.linecorp.centraldogma.xds.internal.ControlPlanePlugin.XDS_CENTRAL_DOGMA_PROJECT; +import static com.linecorp.centraldogma.server.internal.storage.InternalProjectConstants.INTERNAL_PROJECT_XDS; import static com.linecorp.centraldogma.xds.internal.ControlPlaneService.ENDPOINTS_DIRECTORY; import static com.linecorp.centraldogma.xds.internal.ControlPlaneService.K8S_ENDPOINTS_DIRECTORY; import static com.linecorp.centraldogma.xds.internal.XdsResourceManager.JSON_MESSAGE_MARSHALLER; @@ -221,7 +221,7 @@ private static AggregatedHttpResponse getK8sEndpoint(String id) { private static void pushJsonEndpoint(String endpointId, ClusterLoadAssignment endpoint) throws Exception { - dogma.client().forRepo(XDS_CENTRAL_DOGMA_PROJECT, GROUP) + dogma.client().forRepo(INTERNAL_PROJECT_XDS, GROUP) .commit("Add JSON endpoint: " + endpointId, Change.ofJsonUpsert(ENDPOINTS_DIRECTORY + endpointId + ".json", Jackson.readTree( @@ -231,7 +231,7 @@ private static void pushJsonEndpoint(String endpointId, ClusterLoadAssignment en private static void pushYamlEndpoint(String endpointId, ClusterLoadAssignment endpoint) throws Exception { - dogma.client().forRepo(XDS_CENTRAL_DOGMA_PROJECT, GROUP) + dogma.client().forRepo(INTERNAL_PROJECT_XDS, GROUP) .commit("Add YAML endpoint: " + endpointId, Change.ofYamlUpsert(ENDPOINTS_DIRECTORY + endpointId + ".yaml", JSON_MESSAGE_MARSHALLER.writeValueAsString(endpoint))) @@ -240,7 +240,7 @@ private static void pushYamlEndpoint(String endpointId, ClusterLoadAssignment en private static void pushJsonK8sEndpoint(String endpointId, ClusterLoadAssignment endpoint) throws Exception { - dogma.client().forRepo(XDS_CENTRAL_DOGMA_PROJECT, GROUP) + dogma.client().forRepo(INTERNAL_PROJECT_XDS, GROUP) .commit("Add JSON k8s endpoint: " + endpointId, Change.ofJsonUpsert(K8S_ENDPOINTS_DIRECTORY + endpointId + ".json", Jackson.readTree( @@ -250,7 +250,7 @@ private static void pushJsonK8sEndpoint(String endpointId, ClusterLoadAssignment private static void pushYamlK8sEndpoint(String endpointId, ClusterLoadAssignment endpoint) throws Exception { - dogma.client().forRepo(XDS_CENTRAL_DOGMA_PROJECT, GROUP) + dogma.client().forRepo(INTERNAL_PROJECT_XDS, GROUP) .commit("Add YAML k8s endpoint: " + endpointId, Change.ofYamlUpsert(K8S_ENDPOINTS_DIRECTORY + endpointId + ".yaml", JSON_MESSAGE_MARSHALLER.writeValueAsString(endpoint))) diff --git a/xds/src/test/java/com/linecorp/centraldogma/xds/internal/XdsLegacyJsonCompatibilityTest.java b/xds/src/test/java/com/linecorp/centraldogma/xds/internal/XdsLegacyJsonCompatibilityTest.java index 23b2427cf..01d49525e 100644 --- a/xds/src/test/java/com/linecorp/centraldogma/xds/internal/XdsLegacyJsonCompatibilityTest.java +++ b/xds/src/test/java/com/linecorp/centraldogma/xds/internal/XdsLegacyJsonCompatibilityTest.java @@ -15,7 +15,7 @@ */ package com.linecorp.centraldogma.xds.internal; -import static com.linecorp.centraldogma.xds.internal.ControlPlanePlugin.XDS_CENTRAL_DOGMA_PROJECT; +import static com.linecorp.centraldogma.server.internal.storage.InternalProjectConstants.INTERNAL_PROJECT_XDS; import static com.linecorp.centraldogma.xds.internal.ControlPlaneService.CLUSTERS_DIRECTORY; import static com.linecorp.centraldogma.xds.internal.ControlPlaneService.ENDPOINTS_DIRECTORY; import static com.linecorp.centraldogma.xds.internal.ControlPlaneService.LISTENERS_DIRECTORY; @@ -191,14 +191,14 @@ void deleteEndpoint_removesLegacyJson() throws IOException { private static void pushLegacyJson(String path, T resource) throws IOException { final JsonNode jsonNode = Jackson.readTree(JSON_MESSAGE_MARSHALLER.writeValueAsString(resource)); dogma.client() - .forRepo(XDS_CENTRAL_DOGMA_PROJECT, GROUP) + .forRepo(INTERNAL_PROJECT_XDS, GROUP) .commit("Add legacy " + path, Change.ofJsonUpsert(path, jsonNode)) .push() .join(); } private static Repository repo() { - return dogma.projectManager().get(XDS_CENTRAL_DOGMA_PROJECT).repos().get(GROUP); + return dogma.projectManager().get(INTERNAL_PROJECT_XDS).repos().get(GROUP); } private static void assertFileExists(Repository repo, String path) { diff --git a/xds/src/test/java/com/linecorp/centraldogma/xds/internal/XdsMirrorFileValidatorTest.java b/xds/src/test/java/com/linecorp/centraldogma/xds/internal/XdsMirrorFileValidatorTest.java new file mode 100644 index 000000000..bdf6a2073 --- /dev/null +++ b/xds/src/test/java/com/linecorp/centraldogma/xds/internal/XdsMirrorFileValidatorTest.java @@ -0,0 +1,225 @@ +/* + * Copyright 2026 LY Corporation + * + * LY Corporation licenses this file to you 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: + * + * https://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 com.linecorp.centraldogma.xds.internal; + +import static com.linecorp.centraldogma.server.internal.storage.InternalProjectConstants.INTERNAL_PROJECT_XDS; +import static com.linecorp.centraldogma.xds.internal.XdsResourceManager.JSON_MESSAGE_MARSHALLER; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.google.protobuf.Message; +import com.google.protobuf.util.Durations; + +import com.linecorp.centraldogma.common.Change; +import com.linecorp.centraldogma.common.MirrorException; +import com.linecorp.centraldogma.internal.Jackson; +import com.linecorp.centraldogma.xds.k8s.v1.KubernetesEndpointAggregator; +import com.linecorp.centraldogma.xds.k8s.v1.KubernetesLocalityLbEndpoints; + +import io.envoyproxy.envoy.config.cluster.v3.Cluster; +import io.envoyproxy.envoy.config.core.v3.Address; +import io.envoyproxy.envoy.config.core.v3.SocketAddress; +import io.envoyproxy.envoy.config.endpoint.v3.ClusterLoadAssignment; +import io.envoyproxy.envoy.config.endpoint.v3.Endpoint; +import io.envoyproxy.envoy.config.endpoint.v3.Endpoint.Builder; +import io.envoyproxy.envoy.config.endpoint.v3.LbEndpoint; +import io.envoyproxy.envoy.config.endpoint.v3.LocalityLbEndpoints; +import io.envoyproxy.envoy.config.listener.v3.ApiListener; +import io.envoyproxy.envoy.config.listener.v3.Listener; +import io.envoyproxy.envoy.config.route.v3.RouteConfiguration; +import io.envoyproxy.envoy.config.route.v3.VirtualHost; + +class XdsMirrorFileValidatorTest { + + private static final String REPO_NAME = "my-group"; + + private static final XdsMirrorFileValidator VALIDATOR = new XdsMirrorFileValidator(); + + private static Change yamlChangeOf(String path, Message proto) { + try { + final String json = JSON_MESSAGE_MARSHALLER.writeValueAsString(proto); + return Change.ofYamlUpsert(path, Jackson.readTree(json)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static Cluster sampleCluster() { + return Cluster.newBuilder() + .setName("test-cluster") + .setConnectTimeout(Durations.fromSeconds(5)) + .build(); + } + + private static Listener sampleListener() { + return Listener.newBuilder() + .setName("test-listener") + .setApiListener(ApiListener.newBuilder().build()) + .build(); + } + + private static RouteConfiguration sampleRoute() { + return RouteConfiguration.newBuilder() + .setName("test-route") + .addVirtualHosts(VirtualHost.newBuilder() + .setName("local") + .addDomains("*")) + .build(); + } + + private static ClusterLoadAssignment sampleEndpoint() { + final Builder endpoint = + Endpoint.newBuilder().setAddress(Address.newBuilder().setSocketAddress( + SocketAddress.newBuilder().setAddress("127.0.0.1").setPortValue(8080))); + return ClusterLoadAssignment.newBuilder() + .setClusterName("test-cluster") + .addEndpoints(LocalityLbEndpoints.newBuilder() + .addLbEndpoints( + LbEndpoint.newBuilder() + .setEndpoint(endpoint))) + .build(); + } + + private static KubernetesEndpointAggregator sampleAggregator() { + return KubernetesEndpointAggregator.newBuilder() + .setName("groups/g1/k8s/endpointAggregators/agg1") + .setClusterName("agg-cluster") + .addLocalityLbEndpoints( + KubernetesLocalityLbEndpoints.newBuilder()) + .build(); + } + + @Test + void clusterFile_validContent_passes() { + assertThatCode(() -> VALIDATOR.validate( + INTERNAL_PROJECT_XDS, REPO_NAME, + yamlChangeOf("/clusters/my-cluster.yaml", sampleCluster()))) + .doesNotThrowAnyException(); + } + + @Test + void listenerFile_validContent_passes() { + assertThatCode(() -> VALIDATOR.validate( + INTERNAL_PROJECT_XDS, REPO_NAME, + yamlChangeOf("/listeners/my-listener.yaml", sampleListener()))) + .doesNotThrowAnyException(); + } + + @Test + void routeFile_validContent_passes() { + assertThatCode(() -> VALIDATOR.validate( + INTERNAL_PROJECT_XDS, REPO_NAME, + yamlChangeOf("/routes/my-route.yaml", sampleRoute()))) + .doesNotThrowAnyException(); + } + + @Test + void endpointFile_validContent_passes() { + assertThatCode(() -> VALIDATOR.validate( + INTERNAL_PROJECT_XDS, REPO_NAME, + yamlChangeOf("/endpoints/my-endpoint.yaml", sampleEndpoint()))) + .doesNotThrowAnyException(); + } + + @Test + void k8sAggregatorFile_validContent_passes() { + assertThatCode(() -> VALIDATOR.validate( + INTERNAL_PROJECT_XDS, REPO_NAME, + yamlChangeOf("/k8s/endpointAggregators/agg1.yaml", sampleAggregator()))) + .doesNotThrowAnyException(); + } + + @Test + void nonXdsProject_skipped() { + assertThatCode(() -> VALIDATOR.validate( + "other-project", REPO_NAME, + yamlChangeOf("/clusters/my-cluster.yaml", sampleCluster()))) + .doesNotThrowAnyException(); + } + + @Test + void removeChange_skipped() { + assertThatCode(() -> VALIDATOR.validate( + INTERNAL_PROJECT_XDS, REPO_NAME, + Change.ofRemoval("/clusters/my-cluster.yaml"))) + .doesNotThrowAnyException(); + } + + @Test + void invalidContent_rejected() throws JsonProcessingException { + final Change badChange = + Change.ofYamlUpsert("/clusters/bad.yaml", + Jackson.readTree("{\"not_a_cluster_field\": true}")); + assertThatThrownBy(() -> VALIDATOR.validate( + INTERNAL_PROJECT_XDS, REPO_NAME, badChange)) + .isInstanceOf(MirrorException.class) + .hasMessageContaining("/clusters/bad.yaml"); + } + + @Test + void typeMismatch_listenerYamlInClustersDir_rejected() { + assertThatThrownBy(() -> VALIDATOR.validate( + INTERNAL_PROJECT_XDS, REPO_NAME, + yamlChangeOf("/clusters/wrong-type.yaml", sampleListener()))) + .isInstanceOf(MirrorException.class) + .hasMessageContaining("/clusters/wrong-type.yaml"); + } + + @Test + void typeMismatch_clusterYamlInListenersDir_rejected() { + assertThatThrownBy(() -> VALIDATOR.validate( + INTERNAL_PROJECT_XDS, REPO_NAME, + yamlChangeOf("/listeners/wrong-type.yaml", sampleCluster()))) + .isInstanceOf(MirrorException.class) + .hasMessageContaining("/listeners/wrong-type.yaml"); + } + + @Test + void k8sEndpointsFile_rejected() { + assertThatThrownBy(() -> VALIDATOR.validate( + INTERNAL_PROJECT_XDS, REPO_NAME, + yamlChangeOf("/k8s/endpoints/foo.yaml", sampleEndpoint()))) + .isInstanceOf(MirrorException.class) + .hasMessageContaining("/k8s/endpoints/") + .hasMessageContaining("Kubernetes controller"); + } + + @Test + void unexpectedPath_rejected() { + assertThatThrownBy(() -> VALIDATOR.validate( + INTERNAL_PROJECT_XDS, REPO_NAME, + Change.ofTextUpsert("/README.yaml", "hello"))) + .isInstanceOf(MirrorException.class) + .hasMessageContaining("/README.yaml") + .hasMessageContaining("unexpected file path"); + } + + @Test + void unknownTopLevelDir_rejected() { + assertThatThrownBy(() -> VALIDATOR.validate( + INTERNAL_PROJECT_XDS, REPO_NAME, + yamlChangeOf("/unknown/resource.yaml", sampleCluster()))) + .isInstanceOf(MirrorException.class) + .hasMessageContaining("/unknown/resource.yaml") + .hasMessageContaining("unexpected file path"); + } +} diff --git a/xds/src/test/java/com/linecorp/centraldogma/xds/internal/XdsYamlCompatibilityTest.java b/xds/src/test/java/com/linecorp/centraldogma/xds/internal/XdsYamlCompatibilityTest.java index d48d2b596..61ab237a0 100644 --- a/xds/src/test/java/com/linecorp/centraldogma/xds/internal/XdsYamlCompatibilityTest.java +++ b/xds/src/test/java/com/linecorp/centraldogma/xds/internal/XdsYamlCompatibilityTest.java @@ -15,7 +15,7 @@ */ package com.linecorp.centraldogma.xds.internal; -import static com.linecorp.centraldogma.xds.internal.ControlPlanePlugin.XDS_CENTRAL_DOGMA_PROJECT; +import static com.linecorp.centraldogma.server.internal.storage.InternalProjectConstants.INTERNAL_PROJECT_XDS; import static com.linecorp.centraldogma.xds.internal.ControlPlaneService.CLUSTERS_DIRECTORY; import static com.linecorp.centraldogma.xds.internal.ControlPlaneService.ENDPOINTS_DIRECTORY; import static com.linecorp.centraldogma.xds.internal.XdsResourceManager.JSON_MESSAGE_MARSHALLER; @@ -197,7 +197,7 @@ void jsonFileStillWorksAlongsideYaml() throws Exception { private static void pushYamlCluster(String clusterId, Cluster cluster) throws Exception { final String content = JSON_MESSAGE_MARSHALLER.writeValueAsString(cluster); - dogma.client().forRepo(XDS_CENTRAL_DOGMA_PROJECT, "foo") + dogma.client().forRepo(INTERNAL_PROJECT_XDS, "foo") .commit("Add YAML cluster: " + clusterId, Change.ofYamlUpsert(CLUSTERS_DIRECTORY + clusterId + ".yaml", content)) .push().join(); @@ -206,7 +206,7 @@ private static void pushYamlCluster(String clusterId, Cluster cluster) throws Ex private static void pushYamlEndpoint(String endpointId, ClusterLoadAssignment endpoint) throws Exception { final String content = JSON_MESSAGE_MARSHALLER.writeValueAsString(endpoint); - dogma.client().forRepo(XDS_CENTRAL_DOGMA_PROJECT, "foo") + dogma.client().forRepo(INTERNAL_PROJECT_XDS, "foo") .commit("Add YAML endpoint: " + endpointId, Change.ofYamlUpsert(ENDPOINTS_DIRECTORY + endpointId + ".yaml", content)) .push().join(); @@ -244,7 +244,7 @@ private static AggregatedHttpResponse deleteCluster(String clusterName) { } private static Repository xdsRepo(String group) { - return dogma.projectManager().get(XDS_CENTRAL_DOGMA_PROJECT).repos().get(group); + return dogma.projectManager().get(INTERNAL_PROJECT_XDS).repos().get(group); } private static void checkClusterViaDiscovery(String clusterName, Cluster expectedCluster, diff --git a/xds/src/test/java/com/linecorp/centraldogma/xds/k8s/v1/AggregatingMultipleKubernetesTest.java b/xds/src/test/java/com/linecorp/centraldogma/xds/k8s/v1/AggregatingMultipleKubernetesTest.java index 081ac6c3a..c3063cf81 100644 --- a/xds/src/test/java/com/linecorp/centraldogma/xds/k8s/v1/AggregatingMultipleKubernetesTest.java +++ b/xds/src/test/java/com/linecorp/centraldogma/xds/k8s/v1/AggregatingMultipleKubernetesTest.java @@ -16,7 +16,7 @@ package com.linecorp.centraldogma.xds.k8s.v1; import static com.google.common.collect.ImmutableList.toImmutableList; -import static com.linecorp.centraldogma.xds.internal.ControlPlanePlugin.XDS_CENTRAL_DOGMA_PROJECT; +import static com.linecorp.centraldogma.server.internal.storage.InternalProjectConstants.INTERNAL_PROJECT_XDS; import static com.linecorp.centraldogma.xds.internal.ControlPlaneService.K8S_ENDPOINTS_DIRECTORY; import static com.linecorp.centraldogma.xds.internal.XdsTestUtil.createGroup; import static com.linecorp.centraldogma.xds.k8s.v1.XdsKubernetesService.K8S_ENDPOINT_AGGREGATORS_DIRECTORY; @@ -128,7 +128,7 @@ void aggregateMultipleKubernetes() throws Exception { .build(); assertAggregator(json, expectedAggregator); - final Repository fooGroup = dogma.projectManager().get(XDS_CENTRAL_DOGMA_PROJECT).repos().get("foo"); + final Repository fooGroup = dogma.projectManager().get(INTERNAL_PROJECT_XDS).repos().get("foo"); final Entry aggregatorEntry = fooGroup.get(Revision.HEAD, Query.ofYaml( K8S_ENDPOINT_AGGREGATORS_DIRECTORY + aggregatorId + ".yaml")).join(); diff --git a/xds/src/test/java/com/linecorp/centraldogma/xds/k8s/v1/XdsKubernetesServiceTest.java b/xds/src/test/java/com/linecorp/centraldogma/xds/k8s/v1/XdsKubernetesServiceTest.java index 433fa61e6..59ccb4a47 100644 --- a/xds/src/test/java/com/linecorp/centraldogma/xds/k8s/v1/XdsKubernetesServiceTest.java +++ b/xds/src/test/java/com/linecorp/centraldogma/xds/k8s/v1/XdsKubernetesServiceTest.java @@ -17,8 +17,8 @@ import static com.google.common.collect.ImmutableList.toImmutableList; import static com.linecorp.centraldogma.internal.CredentialUtil.credentialName; +import static com.linecorp.centraldogma.server.internal.storage.InternalProjectConstants.INTERNAL_PROJECT_XDS; import static com.linecorp.centraldogma.xds.endpoint.v1.XdsEndpointServiceTest.checkEndpointsViaDiscoveryRequest; -import static com.linecorp.centraldogma.xds.internal.ControlPlanePlugin.XDS_CENTRAL_DOGMA_PROJECT; import static com.linecorp.centraldogma.xds.internal.ControlPlaneService.K8S_ENDPOINTS_DIRECTORY; import static com.linecorp.centraldogma.xds.internal.XdsResourceManager.JSON_MESSAGE_MARSHALLER; import static com.linecorp.centraldogma.xds.internal.XdsTestUtil.createGroup; @@ -152,7 +152,7 @@ private static void setUpK8s() { private static void putCredential() { final CreateCredentialRequest repoCredentialRequest = new CreateCredentialRequest( "repo-credential", - new AccessTokenCredential(credentialName(XDS_CENTRAL_DOGMA_PROJECT, "foo", "repo-credential"), + new AccessTokenCredential(credentialName(INTERNAL_PROJECT_XDS, "foo", "repo-credential"), "secret") ); dogma.httpClient().prepare() @@ -162,7 +162,7 @@ private static void putCredential() { // This is needed for the backward compatibility test. final CreateCredentialRequest projectCredentialRequest = new CreateCredentialRequest( "project-credential", - new AccessTokenCredential(credentialName(XDS_CENTRAL_DOGMA_PROJECT, "project-credential"), + new AccessTokenCredential(credentialName(INTERNAL_PROJECT_XDS, "project-credential"), "secret") ); dogma.httpClient().prepare() @@ -223,7 +223,7 @@ void createEndpointAggregatorsRequest(String credentialId) throws IOException { aggregator.toBuilder().setClusterName(clusterName) // cluster name is set by the service. .build(); assertAggregator(json, expectedAggregator); - final Repository fooGroup = dogma.projectManager().get(XDS_CENTRAL_DOGMA_PROJECT).repos().get("foo"); + final Repository fooGroup = dogma.projectManager().get(INTERNAL_PROJECT_XDS).repos().get("foo"); final Entry entry = fooGroup.get(Revision.HEAD, Query.ofYaml( K8S_ENDPOINT_AGGREGATORS_DIRECTORY + aggregatorId + ".yaml")).join(); @@ -381,14 +381,14 @@ void createAggregator_migratesLegacyJsonEndpoint() throws IOException { final String aggregatorId = "k8s-mig-cluster/1"; final String clusterName = "groups/foo/k8s/clusters/" + aggregatorId; final Repository fooGroup = - dogma.projectManager().get(XDS_CENTRAL_DOGMA_PROJECT).repos().get("foo"); + dogma.projectManager().get(INTERNAL_PROJECT_XDS).repos().get("foo"); // Pre-push a legacy .json endpoint file — simulates an old server that wrote .json. final String legacyJsonPath = K8S_ENDPOINTS_DIRECTORY + aggregatorId + ".json"; final ClusterLoadAssignment legacyEndpoints = clusterLoadAssignment(clusterName, 30000); final JsonNode legacyJsonNode = Jackson.readTree( JSON_MESSAGE_MARSHALLER.writeValueAsString(legacyEndpoints)); - dogma.client().forRepo(XDS_CENTRAL_DOGMA_PROJECT, "foo") + dogma.client().forRepo(INTERNAL_PROJECT_XDS, "foo") .commit("Add legacy k8s JSON endpoint", Change.ofJsonUpsert(legacyJsonPath, legacyJsonNode)) .push().join();
{repoName}
{projectName}
{repoName}
{projectName}