Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
7b197a72aa
commit
4316e6895a
|
@ -10,6 +10,11 @@ include:
|
|||
- /ci/allure-report.yml
|
||||
- /ci/knapsack-report.yml
|
||||
|
||||
stages:
|
||||
- test
|
||||
- report
|
||||
- notify
|
||||
|
||||
# ==========================================
|
||||
# Templates
|
||||
# ==========================================
|
||||
|
@ -72,8 +77,8 @@ trigger-omnibus:
|
|||
|
||||
download-knapsack-report:
|
||||
extends:
|
||||
- .ruby-image
|
||||
- .bundle-install
|
||||
- .ruby-image
|
||||
- .rules:prepare
|
||||
stage: .pre
|
||||
script:
|
||||
|
@ -87,8 +92,8 @@ download-knapsack-report:
|
|||
# e2e test jobs run on separate runner which has separate cache setup
|
||||
cache-gems:
|
||||
extends:
|
||||
- .ruby-image
|
||||
- .bundle-install
|
||||
- .ruby-image
|
||||
- .qa-cache-push
|
||||
- .rules:prepare
|
||||
stage: .pre
|
||||
|
@ -441,28 +446,29 @@ allure-report:
|
|||
extends:
|
||||
- .generate-allure-report-base
|
||||
- .rules:report:allure-report
|
||||
stage: .post
|
||||
stage: report
|
||||
variables:
|
||||
GITLAB_AUTH_TOKEN: $GITLAB_QA_MR_ALLURE_REPORT_TOKEN
|
||||
ALLURE_PROJECT_PATH: $CI_PROJECT_PATH
|
||||
ALLURE_MERGE_REQUEST_IID: $CI_MERGE_REQUEST_IID
|
||||
ALLURE_JOB_NAME: e2e-package-and-test
|
||||
GIT_STRATEGY: none
|
||||
|
||||
upload-knapsack-report:
|
||||
extends:
|
||||
- .generate-knapsack-report-base
|
||||
- .ruby-image
|
||||
- .bundle-install
|
||||
- .ruby-image
|
||||
- .rules:report:process-results
|
||||
stage: .post
|
||||
stage: report
|
||||
when: always
|
||||
|
||||
relate-test-failures:
|
||||
stage: .post
|
||||
extends:
|
||||
- .ruby-image
|
||||
- .bundle-install
|
||||
- .ruby-image
|
||||
- .rules:report:process-results
|
||||
stage: report
|
||||
variables:
|
||||
QA_FAILURES_REPORTING_PROJECT: gitlab-org/gitlab
|
||||
QA_FAILURES_MAX_DIFF_RATIO: "0.15"
|
||||
|
@ -476,14 +482,15 @@ relate-test-failures:
|
|||
--max-diff-ratio "$QA_FAILURES_MAX_DIFF_RATIO"
|
||||
|
||||
generate-test-session:
|
||||
stage: .post
|
||||
extends:
|
||||
- .ruby-image
|
||||
- .bundle-install
|
||||
- .ruby-image
|
||||
- .rules:report:process-results
|
||||
stage: report
|
||||
variables:
|
||||
QA_TESTCASE_SESSIONS_PROJECT: gitlab-org/quality/testcase-sessions
|
||||
GITLAB_QA_ACCESS_TOKEN: $QA_TEST_SESSION_TOKEN
|
||||
GITLAB_CI_API_TOKEN: $QA_GITLAB_CI_TOKEN
|
||||
when: always
|
||||
script:
|
||||
- |
|
||||
|
@ -499,12 +506,10 @@ generate-test-session:
|
|||
notify-slack:
|
||||
extends:
|
||||
- .notify-slack-qa
|
||||
- .ruby-image
|
||||
- .bundle-install
|
||||
- .ruby-image
|
||||
- .rules:report:process-results
|
||||
stage: .post
|
||||
needs:
|
||||
- generate-test-session
|
||||
stage: notify
|
||||
variables:
|
||||
ALLURE_JOB_NAME: e2e-package-and-test
|
||||
SLACK_ICON_EMOJI: ci_failing
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
import axios from '~/lib/utils/axios_utils';
|
||||
import { buildApiUrl } from '~/api/api_utils';
|
||||
|
||||
// the :request_path is loading API-like resources, not part of our REST API.
|
||||
// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82784#note_1077703806
|
||||
const HARBOR_REPOSITORIES_PATH = '/:request_path.json';
|
||||
const HARBOR_ARTIFACTS_PATH = '/:request_path/:repo_name/artifacts.json';
|
||||
const HARBOR_TAGS_PATH = '/:request_path/:repo_name/artifacts/:digest/tags.json';
|
||||
|
||||
export function getHarborRepositoriesList({ requestPath, limit, page, sort, search = '' }) {
|
||||
const url = buildApiUrl(HARBOR_REPOSITORIES_PATH).replace('/:request_path', requestPath);
|
||||
|
||||
return axios.get(url, {
|
||||
params: {
|
||||
limit,
|
||||
page,
|
||||
search,
|
||||
sort,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function getHarborArtifacts({ requestPath, repoName, limit, page, sort, search = '' }) {
|
||||
const url = buildApiUrl(HARBOR_ARTIFACTS_PATH)
|
||||
.replace('/:request_path', requestPath)
|
||||
.replace(':repo_name', repoName);
|
||||
|
||||
return axios.get(url, {
|
||||
params: {
|
||||
limit,
|
||||
page,
|
||||
search,
|
||||
sort,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function getHarborTags({ requestPath, repoName, digest }) {
|
||||
const url = buildApiUrl(HARBOR_TAGS_PATH)
|
||||
.replace('/:request_path', requestPath)
|
||||
.replace(':repo_name', repoName)
|
||||
.replace(':digest', digest);
|
||||
|
||||
return axios.get(url);
|
||||
}
|
|
@ -31,3 +31,10 @@ export const timelineListI18n = Object.freeze({
|
|||
'Incident|Something went wrong while updating the incident timeline event.',
|
||||
),
|
||||
});
|
||||
|
||||
export const timelineItemI18n = Object.freeze({
|
||||
delete: __('Delete'),
|
||||
edit: __('Edit'),
|
||||
moreActions: __('More actions'),
|
||||
timeUTC: __('%{time} UTC'),
|
||||
});
|
||||
|
|
|
@ -1,17 +1,12 @@
|
|||
<script>
|
||||
import { GlDropdown, GlDropdownItem, GlIcon, GlSafeHtmlDirective, GlSprintf } from '@gitlab/ui';
|
||||
import { formatDate } from '~/lib/utils/datetime_utility';
|
||||
import { __ } from '~/locale';
|
||||
import { timelineItemI18n } from './constants';
|
||||
import { getEventIcon } from './utils';
|
||||
|
||||
export default {
|
||||
name: 'IncidentTimelineEventListItem',
|
||||
i18n: {
|
||||
delete: __('Delete'),
|
||||
edit: __('Edit'),
|
||||
moreActions: __('More actions'),
|
||||
timeUTC: __('%{time} UTC'),
|
||||
},
|
||||
i18n: timelineItemI18n,
|
||||
components: {
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
<script>
|
||||
import { GlEmptyState } from '@gitlab/ui';
|
||||
import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
|
||||
import TagsLoader from '~/packages_and_registries/shared/components/tags_loader.vue';
|
||||
import {
|
||||
NO_ARTIFACTS_TITLE,
|
||||
NO_TAGS_MATCHING_FILTERS_TITLE,
|
||||
NO_TAGS_MATCHING_FILTERS_DESCRIPTION,
|
||||
} from '~/packages_and_registries/harbor_registry/constants';
|
||||
import ArtifactsListRow from '~/packages_and_registries/harbor_registry/components/details/artifacts_list_row.vue';
|
||||
|
||||
export default {
|
||||
name: 'TagsList',
|
||||
components: {
|
||||
GlEmptyState,
|
||||
ArtifactsListRow,
|
||||
TagsLoader,
|
||||
RegistryList,
|
||||
},
|
||||
inject: ['noContainersImage'],
|
||||
props: {
|
||||
artifacts: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
filter: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
pageInfo: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tags: [],
|
||||
tagsPageInfo: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasNoTags() {
|
||||
return this.artifacts.length === 0;
|
||||
},
|
||||
emptyStateTitle() {
|
||||
return this.filter ? NO_TAGS_MATCHING_FILTERS_TITLE : NO_ARTIFACTS_TITLE;
|
||||
},
|
||||
emptyStateDescription() {
|
||||
return this.filter ? NO_TAGS_MATCHING_FILTERS_DESCRIPTION : '';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
fetchNextPage() {
|
||||
this.$emit('next-page');
|
||||
},
|
||||
fetchPreviousPage() {
|
||||
this.$emit('prev-page');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<tags-loader v-if="isLoading" />
|
||||
<template v-else>
|
||||
<gl-empty-state
|
||||
v-if="hasNoTags"
|
||||
:title="emptyStateTitle"
|
||||
:svg-path="noContainersImage"
|
||||
:description="emptyStateDescription"
|
||||
class="gl-mx-auto gl-my-0"
|
||||
/>
|
||||
<template v-else>
|
||||
<registry-list
|
||||
:pagination="pageInfo"
|
||||
:items="artifacts"
|
||||
:hidden-delete="true"
|
||||
id-property="name"
|
||||
@prev-page="fetchPreviousPage"
|
||||
@next-page="fetchNextPage"
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<artifacts-list-row :artifact="item" />
|
||||
</template>
|
||||
</registry-list>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,133 @@
|
|||
<script>
|
||||
import { GlTooltipDirective, GlSprintf, GlIcon } from '@gitlab/ui';
|
||||
import { numberToHumanSize } from '~/lib/utils/number_utils';
|
||||
import { n__ } from '~/locale';
|
||||
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||
import ListItem from '~/vue_shared/components/registry/list_item.vue';
|
||||
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
import {
|
||||
DIGEST_LABEL,
|
||||
CREATED_AT_LABEL,
|
||||
NOT_AVAILABLE_TEXT,
|
||||
NOT_AVAILABLE_SIZE,
|
||||
} from '~/packages_and_registries/harbor_registry/constants';
|
||||
import { artifactPullCommand } from '~/packages_and_registries/harbor_registry/utils';
|
||||
|
||||
export default {
|
||||
name: 'TagsListRow',
|
||||
components: {
|
||||
GlSprintf,
|
||||
GlIcon,
|
||||
ListItem,
|
||||
ClipboardButton,
|
||||
TimeAgoTooltip,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
inject: ['repositoryUrl', 'harborIntegrationProjectName'],
|
||||
props: {
|
||||
artifact: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
digestLabel: DIGEST_LABEL,
|
||||
createdAtLabel: CREATED_AT_LABEL,
|
||||
},
|
||||
computed: {
|
||||
formattedSize() {
|
||||
return this.artifact.size
|
||||
? numberToHumanSize(Number(this.artifact.size))
|
||||
: NOT_AVAILABLE_SIZE;
|
||||
},
|
||||
tagsCountText() {
|
||||
const count = this.artifact?.tags.length ? this.artifact?.tags.length : 0;
|
||||
|
||||
return n__('%d tag', '%d tags', count);
|
||||
},
|
||||
shortDigest() {
|
||||
// remove sha256: from the string, and show only the first 7 char
|
||||
const PREFIX_LENGTH = 'sha256:'.length;
|
||||
const DIGEST_LENGTH = 7;
|
||||
return (
|
||||
this.artifact.digest?.substring(PREFIX_LENGTH, PREFIX_LENGTH + DIGEST_LENGTH) ??
|
||||
NOT_AVAILABLE_TEXT
|
||||
);
|
||||
},
|
||||
getPullCommand() {
|
||||
if (this.artifact?.digest) {
|
||||
const { image } = this.$route.params;
|
||||
return artifactPullCommand({
|
||||
digest: this.artifact.digest,
|
||||
imageName: image,
|
||||
repositoryUrl: this.repositoryUrl,
|
||||
harborProjectName: this.harborIntegrationProjectName,
|
||||
});
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
linkTo() {
|
||||
const { project, image } = this.$route.params;
|
||||
|
||||
return { name: 'details', params: { project, image, digest: this.artifact.digest } };
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<list-item v-bind="$attrs">
|
||||
<template #left-primary>
|
||||
<div class="gl-display-flex gl-align-items-center">
|
||||
<router-link
|
||||
class="gl-text-body gl-font-weight-bold gl-word-break-all"
|
||||
data-testid="name"
|
||||
:to="linkTo"
|
||||
>
|
||||
{{ artifact.digest }}
|
||||
</router-link>
|
||||
<clipboard-button
|
||||
v-if="getPullCommand"
|
||||
:title="getPullCommand"
|
||||
:text="getPullCommand"
|
||||
category="tertiary"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #left-secondary>
|
||||
<span class="gl-mr-2" data-testid="size">
|
||||
{{ formattedSize }}
|
||||
</span>
|
||||
<span id="tagsCount" data-testid="tags-count">
|
||||
<gl-icon name="tag" class="gl-mr-2" />
|
||||
{{ tagsCountText }}
|
||||
</span>
|
||||
</template>
|
||||
<template #right-primary>
|
||||
<span data-testid="time">
|
||||
<gl-sprintf :message="$options.i18n.createdAtLabel">
|
||||
<template #timeInfo>
|
||||
<time-ago-tooltip :time="artifact.pushTime" />
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</span>
|
||||
</template>
|
||||
<template #right-secondary>
|
||||
<span data-testid="digest">
|
||||
<gl-sprintf :message="$options.i18n.digestLabel">
|
||||
<template #imageId>{{ shortDigest }}</template>
|
||||
</gl-sprintf>
|
||||
</span>
|
||||
<clipboard-button
|
||||
v-if="artifact.digest"
|
||||
:title="artifact.digest"
|
||||
:text="artifact.digest"
|
||||
category="tertiary"
|
||||
/>
|
||||
</template>
|
||||
</list-item>
|
||||
</template>
|
|
@ -0,0 +1,44 @@
|
|||
<script>
|
||||
import { isEmpty } from 'lodash';
|
||||
import { n__, s__ } from '~/locale';
|
||||
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
|
||||
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
|
||||
import timeagoMixin from '~/vue_shared/mixins/timeago';
|
||||
import { ROOT_IMAGE_TEXT } from '~/packages_and_registries/harbor_registry/constants/index';
|
||||
|
||||
export default {
|
||||
name: 'DetailsHeader',
|
||||
components: { TitleArea, MetadataItem },
|
||||
mixins: [timeagoMixin],
|
||||
props: {
|
||||
imagesDetail: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
artifactCountText() {
|
||||
if (isEmpty(this.imagesDetail)) {
|
||||
return s__('HarborRegistry|-- artifacts');
|
||||
}
|
||||
return n__('%d artifact', '%d artifacts', this.imagesDetail.artifactCount);
|
||||
},
|
||||
repositoryFullName() {
|
||||
return this.imagesDetail.name || ROOT_IMAGE_TEXT;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<title-area>
|
||||
<template #title>
|
||||
<span data-testid="title">
|
||||
{{ repositoryFullName }}
|
||||
</span>
|
||||
</template>
|
||||
<template #metadata-tags-count>
|
||||
<metadata-item icon="package" :text="artifactCountText" data-testid="artifacts-count" />
|
||||
</template>
|
||||
</title-area>
|
||||
</template>
|
|
@ -0,0 +1,69 @@
|
|||
<script>
|
||||
// We are using gl-breadcrumb only at the last child of the handwritten breadcrumb
|
||||
// until this gitlab-ui issue is resolved: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1079
|
||||
//
|
||||
// See the CSS workaround in app/assets/stylesheets/pages/registry.scss when this file is changed.
|
||||
import { GlBreadcrumb, GlIcon } from '@gitlab/ui';
|
||||
import { isArray, last } from 'lodash';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlBreadcrumb,
|
||||
GlIcon,
|
||||
},
|
||||
computed: {
|
||||
rootRoute() {
|
||||
return this.$router.options.routes.find((r) => r.meta.root);
|
||||
},
|
||||
isRootRoute() {
|
||||
return this.$route.name === this.rootRoute.name;
|
||||
},
|
||||
currentRoute() {
|
||||
const currentName = this.$route.meta.nameGenerator();
|
||||
const currentHref = this.$route.meta.hrefGenerator();
|
||||
let routeInfoList = [
|
||||
{
|
||||
text: currentName,
|
||||
to: currentHref,
|
||||
},
|
||||
];
|
||||
|
||||
if (isArray(currentName) && isArray(currentHref)) {
|
||||
routeInfoList = currentName.map((name, index) => {
|
||||
return {
|
||||
text: name,
|
||||
to: currentHref[index],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return routeInfoList;
|
||||
},
|
||||
isLoaded() {
|
||||
return this.isRootRoute || last(this.currentRoute).text;
|
||||
},
|
||||
allCrumbs() {
|
||||
let crumbs = [
|
||||
{
|
||||
text: this.rootRoute.meta.nameGenerator(),
|
||||
to: this.rootRoute.path,
|
||||
},
|
||||
];
|
||||
if (!this.isRootRoute) {
|
||||
crumbs = crumbs.concat(this.currentRoute);
|
||||
}
|
||||
return crumbs;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-breadcrumb :key="isLoaded" :items="allCrumbs">
|
||||
<template #separator>
|
||||
<span class="gl-mx-n5">
|
||||
<gl-icon name="chevron-lg-right" :size="8" />
|
||||
</span>
|
||||
</template>
|
||||
</gl-breadcrumb>
|
||||
</template>
|
|
@ -32,7 +32,7 @@ export default {
|
|||
},
|
||||
},
|
||||
i18n: {
|
||||
HARBOR_REGISTRY_TITLE,
|
||||
harborRegistryTitle: HARBOR_REGISTRY_TITLE,
|
||||
},
|
||||
computed: {
|
||||
imagesCountText() {
|
||||
|
@ -48,7 +48,7 @@ export default {
|
|||
|
||||
<template>
|
||||
<title-area
|
||||
:title="$options.i18n.HARBOR_REGISTRY_TITLE"
|
||||
:title="$options.i18n.harborRegistryTitle"
|
||||
:info-messages="infoMessages"
|
||||
:metadata-loading="metadataLoading"
|
||||
>
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
<script>
|
||||
import { GlIcon, GlSprintf, GlSkeletonLoader } from '@gitlab/ui';
|
||||
import { GlIcon, GlSkeletonLoader } from '@gitlab/ui';
|
||||
import { n__ } from '~/locale';
|
||||
|
||||
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||
import ListItem from '~/vue_shared/components/registry/list_item.vue';
|
||||
import { getNameFromParams } from '~/packages_and_registries/harbor_registry/utils';
|
||||
|
||||
export default {
|
||||
name: 'HarborListRow',
|
||||
components: {
|
||||
ClipboardButton,
|
||||
GlSprintf,
|
||||
GlIcon,
|
||||
ListItem,
|
||||
GlSkeletonLoader,
|
||||
|
@ -26,19 +25,18 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
id() {
|
||||
return this.item.id;
|
||||
linkTo() {
|
||||
const { projectName, imageName } = getNameFromParams(this.item.name);
|
||||
|
||||
return { name: 'details', params: { project: projectName, image: imageName } };
|
||||
},
|
||||
artifactCountText() {
|
||||
return n__(
|
||||
'HarborRegistry|%{count} Tag',
|
||||
'HarborRegistry|%{count} Tags',
|
||||
'HarborRegistry|%d artifact',
|
||||
'HarborRegistry|%d artifacts',
|
||||
this.item.artifactCount,
|
||||
);
|
||||
},
|
||||
imageName() {
|
||||
return this.item.name;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -50,9 +48,9 @@ export default {
|
|||
class="gl-text-body gl-font-weight-bold"
|
||||
data-testid="details-link"
|
||||
data-qa-selector="registry_image_content"
|
||||
:to="{ name: 'details', params: { id } }"
|
||||
:to="linkTo"
|
||||
>
|
||||
{{ imageName }}
|
||||
{{ item.name }}
|
||||
</router-link>
|
||||
<clipboard-button
|
||||
v-if="item.location"
|
||||
|
@ -63,13 +61,9 @@ export default {
|
|||
</template>
|
||||
<template #left-secondary>
|
||||
<template v-if="!metadataLoading">
|
||||
<span class="gl-display-flex gl-align-items-center" data-testid="tags-count">
|
||||
<gl-icon name="tag" class="gl-mr-2" />
|
||||
<gl-sprintf :message="artifactCountText">
|
||||
<template #count>
|
||||
{{ item.artifactCount }}
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
<span class="gl-display-flex gl-align-items-center" data-testid="artifacts-count">
|
||||
<gl-icon name="package" class="gl-mr-2" />
|
||||
{{ artifactCountText }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -16,14 +16,4 @@ export const SORT_FIELD_MAPPING = {
|
|||
CREATED: CREATED_SORT_FIELD_KEY,
|
||||
};
|
||||
|
||||
/* eslint-disable @gitlab/require-i18n-strings */
|
||||
export const dockerBuildCommand = (repositoryUrl) => {
|
||||
return `docker build -t ${repositoryUrl} .`;
|
||||
};
|
||||
export const dockerPushCommand = (repositoryUrl) => {
|
||||
return `docker push ${repositoryUrl}`;
|
||||
};
|
||||
export const dockerLoginCommand = (registryHostUrlWithPort) => {
|
||||
return `docker login ${registryHostUrlWithPort}`;
|
||||
};
|
||||
/* eslint-enable @gitlab/require-i18n-strings */
|
||||
export const DEFAULT_PER_PAGE = 10;
|
||||
|
|
|
@ -1,22 +1,10 @@
|
|||
import { s__, __ } from '~/locale';
|
||||
|
||||
export const UPDATED_AT = s__('HarborRegistry|Last updated %{time}');
|
||||
|
||||
export const MISSING_OR_DELETED_IMAGE_TITLE = s__(
|
||||
'HarborRegistry|The image repository could not be found.',
|
||||
export const FETCH_ARTIFACT_LIST_ERROR_MESSAGE = s__(
|
||||
'HarborRegistry|Something went wrong while fetching the artifact list.',
|
||||
);
|
||||
|
||||
export const MISSING_OR_DELETED_IMAGE_MESSAGE = s__(
|
||||
'HarborRegistry|The requested image repository does not exist or has been deleted. If you think this is an error, try refreshing the page.',
|
||||
);
|
||||
|
||||
export const NO_TAGS_TITLE = s__('HarborRegistry|This image has no active tags');
|
||||
|
||||
export const NO_TAGS_MESSAGE = s__(
|
||||
`HarborRegistry|The last tag related to this image was recently removed.
|
||||
This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process.
|
||||
If you have any questions, contact your administrator.`,
|
||||
);
|
||||
export const NO_ARTIFACTS_TITLE = s__('HarborRegistry|This image has no artifacts');
|
||||
|
||||
export const NO_TAGS_MATCHING_FILTERS_TITLE = s__('HarborRegistry|The filter returned no results');
|
||||
|
||||
|
@ -26,14 +14,14 @@ export const NO_TAGS_MATCHING_FILTERS_DESCRIPTION = s__(
|
|||
|
||||
export const DIGEST_LABEL = s__('HarborRegistry|Digest: %{imageId}');
|
||||
export const CREATED_AT_LABEL = s__('HarborRegistry|Published %{timeInfo}');
|
||||
export const PUBLISHED_DETAILS_ROW_TEXT = s__(
|
||||
'HarborRegistry|Published to the %{repositoryPath} image repository at %{time} on %{date}',
|
||||
);
|
||||
export const MANIFEST_DETAILS_ROW_TEST = s__('HarborRegistry|Manifest digest: %{digest}');
|
||||
export const CONFIGURATION_DETAILS_ROW_TEST = s__('HarborRegistry|Configuration digest: %{digest}');
|
||||
export const MISSING_MANIFEST_WARNING_TOOLTIP = s__(
|
||||
'HarborRegistry|Invalid tag: missing manifest digest',
|
||||
);
|
||||
|
||||
export const NOT_AVAILABLE_TEXT = __('Not applicable.');
|
||||
export const NOT_AVAILABLE_SIZE = __('0 bytes');
|
||||
|
||||
export const TOKEN_TYPE_TAG_NAME = 'tag_name';
|
||||
|
||||
export const FETCH_TAGS_ERROR_MESSAGE = s__(
|
||||
'HarborRegistry|Something went wrong while fetching the tags.',
|
||||
);
|
||||
|
||||
export const TAG_LABEL = s__('HarborRegistry|Tag');
|
||||
|
|
|
@ -9,6 +9,11 @@ export const CONNECTION_ERROR_TITLE = s__('HarborRegistry|Harbor connection erro
|
|||
export const CONNECTION_ERROR_MESSAGE = s__(
|
||||
`HarborRegistry|We are having trouble connecting to the Harbor Registry. Please try refreshing the page. If this error persists, please review %{docLinkStart}the troubleshooting documentation%{docLinkEnd}.`,
|
||||
);
|
||||
|
||||
export const FETCH_IMAGES_LIST_ERROR_MESSAGE = s__(
|
||||
'HarborRegistry|Something went wrong while fetching the repository list.',
|
||||
);
|
||||
|
||||
export const LIST_INTRO_TEXT = s__(
|
||||
`HarborRegistry|With the Harbor Registry, every project can have its own space to store images. %{docLinkStart}More information%{docLinkEnd}`,
|
||||
);
|
||||
|
@ -26,6 +31,13 @@ export const EMPTY_RESULT_MESSAGE = s__(
|
|||
'HarborRegistry|To widen your search, change or remove the filters above.',
|
||||
);
|
||||
|
||||
export const EMPTY_IMAGES_TITLE = s__(
|
||||
'HarborRegistry|There are no harbor images stored for this project',
|
||||
);
|
||||
export const EMPTY_IMAGES_MESSAGE = s__(
|
||||
'HarborRegistry|With the Harbor Registry, every project can connect to a harbor space to store its Docker images.',
|
||||
);
|
||||
|
||||
export const SORT_FIELDS = [
|
||||
{ orderBy: 'UPDATED', label: __('Updated') },
|
||||
{ orderBy: 'CREATED', label: __('Created') },
|
||||
|
|
|
@ -3,14 +3,8 @@ import Vue from 'vue';
|
|||
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||
import PerformancePlugin from '~/performance/vue_performance_plugin';
|
||||
import Translate from '~/vue_shared/translate';
|
||||
import RegistryBreadcrumb from '~/packages_and_registries/shared/components/registry_breadcrumb.vue';
|
||||
import RegistryBreadcrumb from '~/packages_and_registries/harbor_registry/components/harbor_registry_breadcrumb.vue';
|
||||
import { renderBreadcrumb } from '~/packages_and_registries/shared/utils';
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
import {
|
||||
dockerBuildCommand,
|
||||
dockerPushCommand,
|
||||
dockerLoginCommand,
|
||||
} from '~/packages_and_registries/harbor_registry/constants';
|
||||
import createRouter from './router';
|
||||
import HarborRegistryExplorer from './pages/index.vue';
|
||||
|
||||
|
@ -35,13 +29,27 @@ export default (id) => {
|
|||
return null;
|
||||
}
|
||||
|
||||
const { endpoint, connectionError, invalidPathError, isGroupPage, ...config } = el.dataset;
|
||||
const {
|
||||
endpoint,
|
||||
connectionError,
|
||||
invalidPathError,
|
||||
isGroupPage,
|
||||
noContainersImage,
|
||||
containersErrorImage,
|
||||
repositoryUrl,
|
||||
harborIntegrationProjectName,
|
||||
projectName,
|
||||
} = el.dataset;
|
||||
|
||||
const breadCrumbState = Vue.observable({
|
||||
name: '',
|
||||
href: '',
|
||||
updateName(value) {
|
||||
this.name = value;
|
||||
},
|
||||
updateHref(value) {
|
||||
this.href = value;
|
||||
},
|
||||
});
|
||||
|
||||
const router = createRouter(endpoint, breadCrumbState);
|
||||
|
@ -53,16 +61,16 @@ export default (id) => {
|
|||
provide() {
|
||||
return {
|
||||
breadCrumbState,
|
||||
config: {
|
||||
...config,
|
||||
connectionError: parseBoolean(connectionError),
|
||||
invalidPathError: parseBoolean(invalidPathError),
|
||||
isGroupPage: parseBoolean(isGroupPage),
|
||||
helpPagePath: helpPagePath('user/packages/container_registry/index'),
|
||||
},
|
||||
dockerBuildCommand: dockerBuildCommand(config.repositoryUrl),
|
||||
dockerPushCommand: dockerPushCommand(config.repositoryUrl),
|
||||
dockerLoginCommand: dockerLoginCommand(config.registryHostUrlWithPort),
|
||||
endpoint,
|
||||
connectionError: parseBoolean(connectionError),
|
||||
invalidPathError: parseBoolean(invalidPathError),
|
||||
isGroupPage: parseBoolean(isGroupPage),
|
||||
repositoryUrl,
|
||||
harborIntegrationProjectName,
|
||||
projectName,
|
||||
containersErrorImage,
|
||||
noContainersImage,
|
||||
helpPagePath: '',
|
||||
};
|
||||
},
|
||||
render(createElement) {
|
||||
|
|
|
@ -1,200 +0,0 @@
|
|||
const mockRequestFn = (mockData) => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(mockData);
|
||||
}, 2000);
|
||||
});
|
||||
};
|
||||
export const harborListResponse = () => {
|
||||
const harborListResponseData = {
|
||||
repositories: [
|
||||
{
|
||||
artifactCount: 1,
|
||||
creationTime: '2022-03-02T06:35:53.205Z',
|
||||
id: 25,
|
||||
name: 'shao/flinkx',
|
||||
projectId: 21,
|
||||
pullCount: 0,
|
||||
updateTime: '2022-03-02T06:35:53.205Z',
|
||||
location: 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas',
|
||||
},
|
||||
{
|
||||
artifactCount: 1,
|
||||
creationTime: '2022-03-02T06:35:53.205Z',
|
||||
id: 26,
|
||||
name: 'shao/flinkx1',
|
||||
projectId: 21,
|
||||
pullCount: 0,
|
||||
updateTime: '2022-03-02T06:35:53.205Z',
|
||||
location: 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas',
|
||||
},
|
||||
{
|
||||
artifactCount: 1,
|
||||
creationTime: '2022-03-02T06:35:53.205Z',
|
||||
id: 27,
|
||||
name: 'shao/flinkx2',
|
||||
projectId: 21,
|
||||
pullCount: 0,
|
||||
updateTime: '2022-03-02T06:35:53.205Z',
|
||||
location: 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas',
|
||||
},
|
||||
],
|
||||
totalCount: 3,
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
},
|
||||
};
|
||||
|
||||
return mockRequestFn(harborListResponseData);
|
||||
};
|
||||
|
||||
export const getHarborRegistryImageDetail = () => {
|
||||
const harborRegistryImageDetailData = {
|
||||
artifactCount: 1,
|
||||
creationTime: '2022-03-02T06:35:53.205Z',
|
||||
id: 25,
|
||||
name: 'shao/flinkx',
|
||||
projectId: 21,
|
||||
pullCount: 0,
|
||||
updateTime: '2022-03-02T06:35:53.205Z',
|
||||
location: 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas',
|
||||
tagsCount: 10,
|
||||
};
|
||||
|
||||
return mockRequestFn(harborRegistryImageDetailData);
|
||||
};
|
||||
|
||||
export const harborTagsResponse = () => {
|
||||
const harborTagsResponseData = {
|
||||
tags: [
|
||||
{
|
||||
digest: 'sha256:7f386a1844faf341353e1c20f2f39f11f397604fedc475435d13f756eeb235d1',
|
||||
location:
|
||||
'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:02310e655103823920157bc4410ea361dc638bc2cda59667d2cb1f2a988e264c',
|
||||
path:
|
||||
'gitlab-org/gitlab/gitlab-ee-qa/cache:02310e655103823920157bc4410ea361dc638bc2cda59667d2cb1f2a988e264c',
|
||||
name: '02310e655103823920157bc4410ea361dc638bc2cda59667d2cb1f2a988e264c',
|
||||
revision: 'f53bde3d44699e04e11cf15fb415046a0913e2623d878d89bc21adb2cbda5255',
|
||||
shortRevision: 'f53bde3d4',
|
||||
createdAt: '2022-03-02T23:59:05+00:00',
|
||||
totalSize: '6623124',
|
||||
},
|
||||
{
|
||||
digest: 'sha256:4554416b84c4568fe93086620b637064ed029737aabe7308b96d50e3d9d92ed7',
|
||||
location:
|
||||
'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:02deb4dddf177212b50e883d5e4f6c03731fad1a18cd27261736cd9dbba79160',
|
||||
path:
|
||||
'gitlab-org/gitlab/gitlab-ee-qa/cache:02deb4dddf177212b50e883d5e4f6c03731fad1a18cd27261736cd9dbba79160',
|
||||
name: '02deb4dddf177212b50e883d5e4f6c03731fad1a18cd27261736cd9dbba79160',
|
||||
revision: 'e1fe52d8bab66d71bd54a6b8784d3b9edbc68adbd6ea87f5fa44d9974144ef9e',
|
||||
shortRevision: 'e1fe52d8b',
|
||||
createdAt: '2022-02-10T01:09:56+00:00',
|
||||
totalSize: '920760',
|
||||
},
|
||||
{
|
||||
digest: 'sha256:14f37b60e52b9ce0e9f8f7094b311d265384798592f783487c30aaa3d58e6345',
|
||||
location:
|
||||
'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:03bc5971bab1e849ba52a20a31e7273053f22b2ddb1d04bd6b77d53a2635727a',
|
||||
path:
|
||||
'gitlab-org/gitlab/gitlab-ee-qa/cache:03bc5971bab1e849ba52a20a31e7273053f22b2ddb1d04bd6b77d53a2635727a',
|
||||
name: '03bc5971bab1e849ba52a20a31e7273053f22b2ddb1d04bd6b77d53a2635727a',
|
||||
revision: 'c72770c6eb93c421bc496964b4bffc742b1ec2e642cdab876be7afda1856029f',
|
||||
shortRevision: 'c72770c6e',
|
||||
createdAt: '2021-12-22T04:48:48+00:00',
|
||||
totalSize: '48609053',
|
||||
},
|
||||
{
|
||||
digest: 'sha256:e925e3b8277ea23f387ed5fba5e78280cfac7cfb261a78cf046becf7b6a3faae',
|
||||
location:
|
||||
'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:03f495bc5714bff78bb14293320d336afdf47fd47ddff0c3c5f09f8da86d5d19',
|
||||
path:
|
||||
'gitlab-org/gitlab/gitlab-ee-qa/cache:03f495bc5714bff78bb14293320d336afdf47fd47ddff0c3c5f09f8da86d5d19',
|
||||
name: '03f495bc5714bff78bb14293320d336afdf47fd47ddff0c3c5f09f8da86d5d19',
|
||||
revision: '1ac2a43194f4e15166abdf3f26e6ec92215240490b9cac834d63de1a3d87494a',
|
||||
shortRevision: '1ac2a4319',
|
||||
createdAt: '2022-03-09T11:02:27+00:00',
|
||||
totalSize: '35141894',
|
||||
},
|
||||
{
|
||||
digest: 'sha256:7d8303fd5c077787a8c879f8f66b69e2b5605f48ccd3f286e236fb0749fcc1ca',
|
||||
location:
|
||||
'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:05a4e58231e54b70aab2d6f22ba4fbe10e48aa4ddcbfef11c5662241c2ae4fda',
|
||||
path:
|
||||
'gitlab-org/gitlab/gitlab-ee-qa/cache:05a4e58231e54b70aab2d6f22ba4fbe10e48aa4ddcbfef11c5662241c2ae4fda',
|
||||
name: '05a4e58231e54b70aab2d6f22ba4fbe10e48aa4ddcbfef11c5662241c2ae4fda',
|
||||
revision: 'cf8fee086701016e1a84e6824f0c896951fef4cce9d4745459558b87eec3232c',
|
||||
shortRevision: 'cf8fee086',
|
||||
createdAt: '2022-01-21T11:31:43+00:00',
|
||||
totalSize: '48716070',
|
||||
},
|
||||
{
|
||||
digest: 'sha256:b33611cefe20e4a41a6e0dce356a5d7ef3c177ea7536a58652f5b3a4f2f83549',
|
||||
location:
|
||||
'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:093d2746876997723541aec8b88687a4cdb3b5bbb0279c5089b7891317741a9a',
|
||||
path:
|
||||
'gitlab-org/gitlab/gitlab-ee-qa/cache:093d2746876997723541aec8b88687a4cdb3b5bbb0279c5089b7891317741a9a',
|
||||
name: '093d2746876997723541aec8b88687a4cdb3b5bbb0279c5089b7891317741a9a',
|
||||
revision: '1a4b48198b13d55242c5164e64d41c4e9f75b5d9506bc6e0efc1534dd0dd1f15',
|
||||
shortRevision: '1a4b48198',
|
||||
createdAt: '2022-01-21T11:31:51+00:00',
|
||||
totalSize: '6623127',
|
||||
},
|
||||
{
|
||||
digest: 'sha256:d25c3c020e2dbd4711a67b9fe308f4cbb7b0bb21815e722a02f91c570dc5d519',
|
||||
location:
|
||||
'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:09698b3fae81dfd6e02554dbc82930f304a6356c8f541c80e8598a42aed985f7',
|
||||
path:
|
||||
'gitlab-org/gitlab/gitlab-ee-qa/cache:09698b3fae81dfd6e02554dbc82930f304a6356c8f541c80e8598a42aed985f7',
|
||||
name: '09698b3fae81dfd6e02554dbc82930f304a6356c8f541c80e8598a42aed985f7',
|
||||
revision: '03e2e2777dde01c30469ee8c710973dd08a7a4f70494d7dc1583c24b525d7f61',
|
||||
shortRevision: '03e2e2777',
|
||||
createdAt: '2022-03-02T23:58:20+00:00',
|
||||
totalSize: '911377',
|
||||
},
|
||||
{
|
||||
digest: 'sha256:fb760e4d2184e9e8e39d6917534d4610fe01009734698a5653b2de1391ba28f4',
|
||||
location:
|
||||
'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:09b830c3eaf80d547f3b523d8e242a2c411085c349dab86c520f36c7b7644f95',
|
||||
path:
|
||||
'gitlab-org/gitlab/gitlab-ee-qa/cache:09b830c3eaf80d547f3b523d8e242a2c411085c349dab86c520f36c7b7644f95',
|
||||
name: '09b830c3eaf80d547f3b523d8e242a2c411085c349dab86c520f36c7b7644f95',
|
||||
revision: '350e78d60646bf6967244448c6aaa14d21ecb9a0c6cf87e9ff0361cbe59b9012',
|
||||
shortRevision: '350e78d60',
|
||||
createdAt: '2022-01-19T13:49:14+00:00',
|
||||
totalSize: '48710241',
|
||||
},
|
||||
{
|
||||
digest: 'sha256:407250f380cea92729cbc038c420e74900f53b852e11edc6404fe75a0fd2c402',
|
||||
location:
|
||||
'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:0d03504a17b467eafc8c96bde70af26c74bd459a32b7eb2dd189dd6b3c121557',
|
||||
path:
|
||||
'gitlab-org/gitlab/gitlab-ee-qa/cache:0d03504a17b467eafc8c96bde70af26c74bd459a32b7eb2dd189dd6b3c121557',
|
||||
name: '0d03504a17b467eafc8c96bde70af26c74bd459a32b7eb2dd189dd6b3c121557',
|
||||
revision: '76038370b7f3904364891457c4a6a234897255e6b9f45d0a852bf3a7e5257e18',
|
||||
shortRevision: '76038370b',
|
||||
createdAt: '2022-01-24T12:56:22+00:00',
|
||||
totalSize: '280065',
|
||||
},
|
||||
{
|
||||
digest: 'sha256:ada87f25218542951ce6720c27f3d0758e90c2540bd129f5cfb9e15b31e07b07',
|
||||
location:
|
||||
'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:0eb20a4a7cac2ebea821d420b3279654fe550fd8502f1785c1927aa84e5949eb',
|
||||
path:
|
||||
'gitlab-org/gitlab/gitlab-ee-qa/cache:0eb20a4a7cac2ebea821d420b3279654fe550fd8502f1785c1927aa84e5949eb',
|
||||
name: '0eb20a4a7cac2ebea821d420b3279654fe550fd8502f1785c1927aa84e5949eb',
|
||||
revision: '3d4b49a7bbb36c48bb721f4d0e76e7950bec3878ee29cdfdd6da39f575d6d37f',
|
||||
shortRevision: '3d4b49a7b',
|
||||
createdAt: '2022-02-17T17:37:52+00:00',
|
||||
totalSize: '48655767',
|
||||
},
|
||||
],
|
||||
totalCount: 10,
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: true,
|
||||
},
|
||||
};
|
||||
|
||||
return mockRequestFn(harborTagsResponseData);
|
||||
};
|
|
@ -0,0 +1,156 @@
|
|||
<script>
|
||||
import { GlFilteredSearchToken } from '@gitlab/ui';
|
||||
import {
|
||||
NAME_SORT_FIELD,
|
||||
ROOT_IMAGE_TEXT,
|
||||
DEFAULT_PER_PAGE,
|
||||
FETCH_ARTIFACT_LIST_ERROR_MESSAGE,
|
||||
TOKEN_TYPE_TAG_NAME,
|
||||
TAG_LABEL,
|
||||
} from '~/packages_and_registries/harbor_registry/constants/index';
|
||||
import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
|
||||
import { createAlert } from '~/flash';
|
||||
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||
import TagsLoader from '~/packages_and_registries/shared/components/tags_loader.vue';
|
||||
import ArtifactsList from '~/packages_and_registries/harbor_registry/components/details/artifacts_list.vue';
|
||||
import PersistedSearch from '~/packages_and_registries/shared/components/persisted_search.vue';
|
||||
import DetailsHeader from '~/packages_and_registries/harbor_registry/components/details/details_header.vue';
|
||||
import {
|
||||
extractSortingDetail,
|
||||
parseFilter,
|
||||
formatPagination,
|
||||
} from '~/packages_and_registries/harbor_registry/utils';
|
||||
import { getHarborArtifacts } from '~/rest_api';
|
||||
|
||||
export default {
|
||||
name: 'HarborDetailsPage',
|
||||
components: {
|
||||
ArtifactsList,
|
||||
TagsLoader,
|
||||
DetailsHeader,
|
||||
PersistedSearch,
|
||||
},
|
||||
inject: ['endpoint', 'breadCrumbState'],
|
||||
searchConfig: { nameSortFields: [NAME_SORT_FIELD] },
|
||||
tokens: [
|
||||
{
|
||||
type: TOKEN_TYPE_TAG_NAME,
|
||||
icon: 'tag',
|
||||
title: TAG_LABEL,
|
||||
unique: true,
|
||||
token: GlFilteredSearchToken,
|
||||
operators: OPERATOR_IS_ONLY,
|
||||
},
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
artifactsList: [],
|
||||
pageInfo: {},
|
||||
mutationLoading: false,
|
||||
deleteAlertType: null,
|
||||
isLoading: true,
|
||||
filterString: '',
|
||||
sorting: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
currentPage() {
|
||||
return this.pageInfo.page || 1;
|
||||
},
|
||||
imagesDetail() {
|
||||
return {
|
||||
name: this.fullName,
|
||||
artifactCount: this.pageInfo?.total || 0,
|
||||
};
|
||||
},
|
||||
fullName() {
|
||||
const { project, image } = this.$route.params;
|
||||
|
||||
if (project && image) {
|
||||
return `${project}/${image}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.updateBreadcrumb();
|
||||
},
|
||||
methods: {
|
||||
updateBreadcrumb() {
|
||||
const name = this.fullName || ROOT_IMAGE_TEXT;
|
||||
this.breadCrumbState.updateName(name);
|
||||
this.breadCrumbState.updateHref(this.$route.path);
|
||||
},
|
||||
handleSearchUpdate({ sort, filters }) {
|
||||
this.sorting = sort;
|
||||
this.filterString = parseFilter(filters, 'digest');
|
||||
|
||||
this.fetchArtifacts(1);
|
||||
},
|
||||
fetchPrevPage() {
|
||||
const prevPageNum = this.currentPage - 1;
|
||||
this.fetchArtifacts(prevPageNum);
|
||||
},
|
||||
fetchNextPage() {
|
||||
const nextPageNum = this.currentPage + 1;
|
||||
this.fetchArtifacts(nextPageNum);
|
||||
},
|
||||
fetchArtifacts(requestPage) {
|
||||
this.isLoading = true;
|
||||
|
||||
const { orderBy, sort } = extractSortingDetail(this.sorting);
|
||||
const sortOptions = `${orderBy} ${sort}`;
|
||||
|
||||
const { image } = this.$route.params;
|
||||
|
||||
const params = {
|
||||
requestPath: this.endpoint,
|
||||
repoName: image,
|
||||
limit: DEFAULT_PER_PAGE,
|
||||
page: requestPage,
|
||||
sort: sortOptions,
|
||||
search: this.filterString,
|
||||
};
|
||||
|
||||
getHarborArtifacts(params)
|
||||
.then((res) => {
|
||||
this.pageInfo = formatPagination(res.headers);
|
||||
|
||||
this.artifactsList = (res?.data || []).map((artifact) => {
|
||||
return convertObjectPropsToCamelCase(artifact);
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
createAlert({ message: FETCH_ARTIFACT_LIST_ERROR_MESSAGE });
|
||||
})
|
||||
.finally(() => {
|
||||
this.isLoading = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gl-my-3">
|
||||
<details-header :images-detail="imagesDetail" />
|
||||
<persisted-search
|
||||
class="gl-mb-5"
|
||||
:sortable-fields="$options.searchConfig.nameSortFields"
|
||||
:default-order="$options.searchConfig.nameSortFields[0].orderBy"
|
||||
default-sort="asc"
|
||||
:tokens="$options.tokens"
|
||||
@update="handleSearchUpdate"
|
||||
/>
|
||||
<tags-loader v-if="isLoading" />
|
||||
<artifacts-list
|
||||
v-else
|
||||
:filter="filterString"
|
||||
:is-loading="isLoading"
|
||||
:artifacts="artifactsList"
|
||||
:page-info="pageInfo"
|
||||
@prev-page="fetchPrevPage"
|
||||
@next-page="fetchNextPage"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
|
@ -1,19 +1,31 @@
|
|||
<script>
|
||||
import { GlEmptyState, GlSprintf, GlLink, GlSkeletonLoader } from '@gitlab/ui';
|
||||
import { escape } from 'lodash';
|
||||
import HarborListHeader from '~/packages_and_registries/harbor_registry/components/list/harbor_list_header.vue';
|
||||
import PersistedSearch from '~/packages_and_registries/shared/components/persisted_search.vue';
|
||||
import HarborList from '~/packages_and_registries/harbor_registry/components/list/harbor_list.vue';
|
||||
import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
|
||||
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||
import {
|
||||
extractSortingDetail,
|
||||
formatPagination,
|
||||
parseFilter,
|
||||
dockerBuildCommand,
|
||||
dockerPushCommand,
|
||||
dockerLoginCommand,
|
||||
} from '~/packages_and_registries/harbor_registry/utils';
|
||||
import { createAlert } from '~/flash';
|
||||
import {
|
||||
SORT_FIELDS,
|
||||
CONNECTION_ERROR_TITLE,
|
||||
CONNECTION_ERROR_MESSAGE,
|
||||
EMPTY_RESULT_TITLE,
|
||||
EMPTY_RESULT_MESSAGE,
|
||||
DEFAULT_PER_PAGE,
|
||||
FETCH_IMAGES_LIST_ERROR_MESSAGE,
|
||||
EMPTY_IMAGES_TITLE,
|
||||
EMPTY_IMAGES_MESSAGE,
|
||||
} from '~/packages_and_registries/harbor_registry/constants';
|
||||
import Tracking from '~/tracking';
|
||||
import { harborListResponse } from '../mock_api';
|
||||
import { getHarborRepositoriesList } from '~/rest_api';
|
||||
|
||||
export default {
|
||||
name: 'HarborListPage',
|
||||
|
@ -31,17 +43,26 @@ export default {
|
|||
),
|
||||
},
|
||||
mixins: [Tracking.mixin()],
|
||||
inject: ['config', 'dockerBuildCommand', 'dockerPushCommand', 'dockerLoginCommand'],
|
||||
inject: [
|
||||
'endpoint',
|
||||
'repositoryUrl',
|
||||
'harborIntegrationProjectName',
|
||||
'projectName',
|
||||
'isGroupPage',
|
||||
'connectionError',
|
||||
'invalidPathError',
|
||||
'containersErrorImage',
|
||||
'helpPagePath',
|
||||
'noContainersImage',
|
||||
],
|
||||
loader: {
|
||||
repeat: 10,
|
||||
width: 1000,
|
||||
height: 40,
|
||||
},
|
||||
i18n: {
|
||||
CONNECTION_ERROR_TITLE,
|
||||
CONNECTION_ERROR_MESSAGE,
|
||||
EMPTY_RESULT_TITLE,
|
||||
EMPTY_RESULT_MESSAGE,
|
||||
connectionErrorTitle: CONNECTION_ERROR_TITLE,
|
||||
connectionErrorMessage: CONNECTION_ERROR_MESSAGE,
|
||||
},
|
||||
searchConfig: SORT_FIELDS,
|
||||
data() {
|
||||
|
@ -56,42 +77,81 @@ export default {
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
dockerCommand() {
|
||||
return {
|
||||
build: dockerBuildCommand({
|
||||
repositoryUrl: this.repositoryUrl,
|
||||
harborProjectName: this.harborIntegrationProjectName,
|
||||
projectName: this.projectName,
|
||||
}),
|
||||
push: dockerPushCommand({
|
||||
repositoryUrl: this.repositoryUrl,
|
||||
harborProjectName: this.harborIntegrationProjectName,
|
||||
projectName: this.projectName,
|
||||
}),
|
||||
login: dockerLoginCommand(this.repositoryUrl),
|
||||
};
|
||||
},
|
||||
showCommands() {
|
||||
return !this.isLoading && !this.config?.isGroupPage && this.images?.length;
|
||||
return !this.isLoading && !this.isGroupPage && this.images?.length;
|
||||
},
|
||||
showConnectionError() {
|
||||
return this.config.connectionError || this.config.invalidPathError;
|
||||
return this.connectionError || this.invalidPathError;
|
||||
},
|
||||
currentPage() {
|
||||
return this.pageInfo.page || 1;
|
||||
},
|
||||
emptyStateTexts() {
|
||||
return {
|
||||
title: this.name ? EMPTY_RESULT_TITLE : EMPTY_IMAGES_TITLE,
|
||||
message: this.name ? EMPTY_RESULT_MESSAGE : EMPTY_IMAGES_MESSAGE,
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
fetchHarborImages() {
|
||||
// TODO: Waiting for harbor api integration to finish: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82777
|
||||
fetchHarborImages(requestPage) {
|
||||
this.isLoading = true;
|
||||
|
||||
harborListResponse()
|
||||
const { orderBy, sort } = extractSortingDetail(this.sorting);
|
||||
const sortOptions = `${orderBy} ${sort}`;
|
||||
|
||||
const params = {
|
||||
requestPath: this.endpoint,
|
||||
limit: DEFAULT_PER_PAGE,
|
||||
search: this.name,
|
||||
page: requestPage,
|
||||
sort: sortOptions,
|
||||
};
|
||||
|
||||
getHarborRepositoriesList(params)
|
||||
.then((res) => {
|
||||
this.images = res?.repositories || [];
|
||||
this.totalCount = res?.totalCount || 0;
|
||||
this.pageInfo = res?.pageInfo || {};
|
||||
this.images = (res?.data || []).map((item) => {
|
||||
return convertObjectPropsToCamelCase(item);
|
||||
});
|
||||
const pagination = formatPagination(res.headers);
|
||||
|
||||
this.totalCount = pagination?.total || 0;
|
||||
this.pageInfo = pagination;
|
||||
|
||||
this.isLoading = false;
|
||||
})
|
||||
.catch(() => {});
|
||||
.catch(() => {
|
||||
createAlert({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE });
|
||||
});
|
||||
},
|
||||
handleSearchUpdate({ sort, filters }) {
|
||||
this.sorting = sort;
|
||||
this.name = parseFilter(filters, 'name');
|
||||
|
||||
const search = filters.find((i) => i.type === FILTERED_SEARCH_TERM);
|
||||
this.name = escape(search?.value?.data);
|
||||
|
||||
this.fetchHarborImages();
|
||||
this.fetchHarborImages(1);
|
||||
},
|
||||
fetchPrevPage() {
|
||||
// TODO: Waiting for harbor api integration to finish: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82777
|
||||
this.fetchHarborImages();
|
||||
const prevPageNum = this.currentPage - 1;
|
||||
this.fetchHarborImages(prevPageNum);
|
||||
},
|
||||
fetchNextPage() {
|
||||
// TODO: Waiting for harbor api integration to finish: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82777
|
||||
this.fetchHarborImages();
|
||||
const nextPageNum = this.currentPage + 1;
|
||||
this.fetchHarborImages(nextPageNum);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -101,14 +161,14 @@ export default {
|
|||
<div>
|
||||
<gl-empty-state
|
||||
v-if="showConnectionError"
|
||||
:title="$options.i18n.CONNECTION_ERROR_TITLE"
|
||||
:svg-path="config.containersErrorImage"
|
||||
:title="$options.i18n.connectionErrorTitle"
|
||||
:svg-path="containersErrorImage"
|
||||
>
|
||||
<template #description>
|
||||
<p>
|
||||
<gl-sprintf :message="$options.i18n.CONNECTION_ERROR_MESSAGE">
|
||||
<gl-sprintf :message="$options.i18n.connectionErrorMessage">
|
||||
<template #docLink="{ content }">
|
||||
<gl-link :href="`${config.helpPagePath}#docker-connection-error`" target="_blank">
|
||||
<gl-link :href="`${helpPagePath}#docker-connection-error`" target="_blank">
|
||||
{{ content }}
|
||||
</gl-link>
|
||||
</template>
|
||||
|
@ -120,14 +180,14 @@ export default {
|
|||
<harbor-list-header
|
||||
:metadata-loading="isLoading"
|
||||
:images-count="totalCount"
|
||||
:help-page-path="config.helpPagePath"
|
||||
:help-page-path="helpPagePath"
|
||||
>
|
||||
<template #commands>
|
||||
<cli-commands
|
||||
v-if="showCommands"
|
||||
:docker-build-command="dockerBuildCommand"
|
||||
:docker-push-command="dockerPushCommand"
|
||||
:docker-login-command="dockerLoginCommand"
|
||||
:docker-build-command="dockerCommand.build"
|
||||
:docker-push-command="dockerCommand.push"
|
||||
:docker-login-command="dockerCommand.login"
|
||||
/>
|
||||
</template>
|
||||
</harbor-list-header>
|
||||
|
@ -152,26 +212,24 @@ export default {
|
|||
</gl-skeleton-loader>
|
||||
</div>
|
||||
<template v-else>
|
||||
<template v-if="images.length > 0 || name">
|
||||
<harbor-list
|
||||
v-if="images.length"
|
||||
:images="images"
|
||||
:meta-data-loading="isLoading"
|
||||
:page-info="pageInfo"
|
||||
@prev-page="fetchPrevPage"
|
||||
@next-page="fetchNextPage"
|
||||
/>
|
||||
<gl-empty-state
|
||||
v-else
|
||||
:svg-path="config.noContainersImage"
|
||||
data-testid="emptySearch"
|
||||
:title="$options.i18n.EMPTY_RESULT_TITLE"
|
||||
>
|
||||
<template #description>
|
||||
{{ $options.i18n.EMPTY_RESULT_MESSAGE }}
|
||||
</template>
|
||||
</gl-empty-state>
|
||||
</template>
|
||||
<harbor-list
|
||||
v-if="images.length"
|
||||
:images="images"
|
||||
:metadata-loading="isLoading"
|
||||
:page-info="pageInfo"
|
||||
@prev-page="fetchPrevPage"
|
||||
@next-page="fetchNextPage"
|
||||
/>
|
||||
<gl-empty-state
|
||||
v-else
|
||||
:svg-path="noContainersImage"
|
||||
data-testid="emptySearch"
|
||||
:title="emptyStateTexts.title"
|
||||
>
|
||||
<template #description>
|
||||
{{ emptyStateTexts.message }}
|
||||
</template>
|
||||
</gl-empty-state>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
|
|
|
@ -22,10 +22,11 @@ export default function createRouter(base, breadCrumbState) {
|
|||
},
|
||||
{
|
||||
name: 'details',
|
||||
path: '/:id',
|
||||
path: '/:project/:image',
|
||||
component: Details,
|
||||
meta: {
|
||||
nameGenerator: () => breadCrumbState.name,
|
||||
hrefGenerator: () => breadCrumbState.href,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
import { isFinite } from 'lodash';
|
||||
import {
|
||||
SORT_FIELD_MAPPING,
|
||||
TOKEN_TYPE_TAG_NAME,
|
||||
} from '~/packages_and_registries/harbor_registry/constants';
|
||||
import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
|
||||
import { normalizeHeaders, parseIntPagination } from '~/lib/utils/common_utils';
|
||||
|
||||
export const extractSortingDetail = (parsedSorting = '') => {
|
||||
const [orderBy, sortOrder] = parsedSorting.split('_');
|
||||
if (orderBy && sortOrder) {
|
||||
return {
|
||||
orderBy: SORT_FIELD_MAPPING[orderBy],
|
||||
sort: sortOrder.toLowerCase(),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
orderBy: '',
|
||||
sort: '',
|
||||
};
|
||||
};
|
||||
|
||||
export const parseFilter = (filters = [], defaultPrefix = '') => {
|
||||
/* eslint-disable @gitlab/require-i18n-strings */
|
||||
const prefixMap = {
|
||||
[FILTERED_SEARCH_TERM]: `${defaultPrefix}=`,
|
||||
[TOKEN_TYPE_TAG_NAME]: 'tags=',
|
||||
};
|
||||
/* eslint-enable @gitlab/require-i18n-strings */
|
||||
const filterList = [];
|
||||
filters.forEach((i) => {
|
||||
if (i.value?.data) {
|
||||
const filterVal = i.value?.data;
|
||||
const prefix = prefixMap[i.type];
|
||||
const filterString = `${prefix}${filterVal}`;
|
||||
|
||||
filterList.push(filterString);
|
||||
}
|
||||
});
|
||||
|
||||
return filterList.join(',');
|
||||
};
|
||||
|
||||
export const getNameFromParams = (fullName) => {
|
||||
const names = fullName.split('/');
|
||||
return {
|
||||
projectName: names[0] || '',
|
||||
imageName: names[1] || '',
|
||||
};
|
||||
};
|
||||
|
||||
export const formatPagination = (headers) => {
|
||||
const pagination = parseIntPagination(normalizeHeaders(headers)) || {};
|
||||
|
||||
if (pagination.nextPage || pagination.previousPage) {
|
||||
pagination.hasNextPage = isFinite(pagination.nextPage);
|
||||
pagination.hasPreviousPage = isFinite(pagination.previousPage);
|
||||
}
|
||||
|
||||
return pagination;
|
||||
};
|
||||
|
||||
/* eslint-disable @gitlab/require-i18n-strings */
|
||||
export const dockerBuildCommand = ({ repositoryUrl, harborProjectName, projectName = '' }) => {
|
||||
return `docker build -t ${repositoryUrl}/${harborProjectName}/${projectName} .`;
|
||||
};
|
||||
|
||||
export const dockerPushCommand = ({ repositoryUrl, harborProjectName, projectName = '' }) => {
|
||||
return `docker push ${repositoryUrl}/${harborProjectName}/${projectName}`;
|
||||
};
|
||||
|
||||
export const dockerLoginCommand = (repositoryUrl) => {
|
||||
return `docker login ${repositoryUrl}`;
|
||||
};
|
||||
|
||||
export const artifactPullCommand = ({ repositoryUrl, harborProjectName, imageName, digest }) => {
|
||||
return `docker pull ${repositoryUrl}/${harborProjectName}/${imageName}@${digest}`;
|
||||
};
|
||||
|
||||
export const tagPullCommand = ({ repositoryUrl, harborProjectName, imageName, tag }) => {
|
||||
return `docker pull ${repositoryUrl}/${harborProjectName}/${imageName}:${tag}`;
|
||||
};
|
||||
/* eslint-enable @gitlab/require-i18n-strings */
|
|
@ -18,6 +18,11 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
tokens: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -68,7 +73,7 @@ export default {
|
|||
v-if="mountRegistrySearch"
|
||||
:filters="filters"
|
||||
:sorting="sorting"
|
||||
:tokens="$options.tokens"
|
||||
:tokens="tokens"
|
||||
:sortable-fields="sortableFields"
|
||||
@sorting:changed="updateSortingAndEmitUpdate"
|
||||
@filter:changed="updateFilters"
|
||||
|
|
|
@ -6,6 +6,7 @@ export * from './api/bulk_imports_api';
|
|||
export * from './api/namespaces_api';
|
||||
export * from './api/tags_api';
|
||||
export * from './api/alert_management_alerts_api';
|
||||
export * from './api/harbor_registry';
|
||||
|
||||
// Note: It's not possible to spy on methods imported from this file in
|
||||
// Jest tests.
|
||||
|
|
|
@ -24,6 +24,10 @@ module Integrations
|
|||
s_("HarborIntegration|After the Harbor integration is activated, global variables '$HARBOR_USERNAME', '$HARBOR_HOST', '$HARBOR_OCI', '$HARBOR_PASSWORD', '$HARBOR_URL' and '$HARBOR_PROJECT' will be created for CI/CD use.")
|
||||
end
|
||||
|
||||
def hostname
|
||||
Gitlab::Utils.parse_url(url).hostname
|
||||
end
|
||||
|
||||
class << self
|
||||
def to_param
|
||||
name.demodulize.downcase
|
||||
|
|
|
@ -4,8 +4,9 @@
|
|||
#js-harbor-registry-list-group{ data: { endpoint: group_harbor_repositories_path(@group),
|
||||
"no_containers_image" => image_path('illustrations/docker-empty-state.svg'),
|
||||
"containers_error_image" => image_path('illustrations/docker-error-state.svg'),
|
||||
"repository_url" => 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas',
|
||||
"registry_host_url_with_port" => 'demo.harbor.com',
|
||||
"repository_url" => @group.harbor_integration.hostname,
|
||||
"harbor_integration_project_name" => @group.harbor_integration.project_name,
|
||||
full_path: @group.full_path,
|
||||
connection_error: (!!@connection_error).to_s,
|
||||
invalid_path_error: (!!@invalid_path_error).to_s,
|
||||
is_group_page: true.to_s } }
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
%p
|
||||
You have been mentioned in merge request #{merge_request_reference_link(@merge_request)}
|
||||
= (s_("Notify|You have been mentioned in merge request %{mr_link}") % { mr_link: merge_request_reference_link(@merge_request) }).html_safe
|
||||
|
||||
= render template: 'notify/new_merge_request_email'
|
||||
|
|
|
@ -25,7 +25,8 @@
|
|||
.input-group-append
|
||||
= clipboard_button(target: '#http_project_clone', title: _("Copy URL"), class: "input-group-text gl-button btn btn-icon btn-default")
|
||||
= render_if_exists 'projects/buttons/geo'
|
||||
%li.divider.mt-2
|
||||
= render_if_exists 'projects/buttons/kerberos_clone_field'
|
||||
%li.divider.mt-2
|
||||
%li.pt-2.gl-new-dropdown-item
|
||||
%label.label-bold{ class: 'gl-px-4!' }
|
||||
= _('Open in your IDE')
|
||||
|
@ -51,4 +52,3 @@
|
|||
%a.dropdown-item.open-with-link{ href: xcode_uri_to_repo(@project) }
|
||||
.gl-new-dropdown-item-text-wrapper
|
||||
= _("Xcode")
|
||||
= render_if_exists 'projects/buttons/kerberos_clone_field'
|
||||
|
|
|
@ -4,8 +4,9 @@
|
|||
#js-harbor-registry-list-project{ data: { endpoint: project_harbor_repositories_path(@project),
|
||||
"no_containers_image" => image_path('illustrations/docker-empty-state.svg'),
|
||||
"containers_error_image" => image_path('illustrations/docker-error-state.svg'),
|
||||
"repository_url" => 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas',
|
||||
"registry_host_url_with_port" => 'demo.harbor.com',
|
||||
"repository_url" => @project.harbor_integration.hostname,
|
||||
"harbor_integration_project_name" => @project.harbor_integration.project_name,
|
||||
"project_name" => @project.name,
|
||||
connection_error: (!!@connection_error).to_s,
|
||||
invalid_path_error: (!!@invalid_path_error).to_s,
|
||||
is_group_page: false.to_s, } }
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: skip_checking_namespace_in_query
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96559
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/370742
|
||||
milestone: '15.4'
|
||||
type: development
|
||||
group: group::source code
|
||||
default_enabled: false
|
|
@ -122,7 +122,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
|
|||
resources :email_campaigns, only: :index
|
||||
|
||||
namespace :harbor do
|
||||
resources :repositories, only: [:index] do
|
||||
resources :repositories, only: [:index, :show], constraints: { id: %r{[a-zA-Z./:0-9_\-]+} } do
|
||||
resources :artifacts, only: [:index] do
|
||||
resources :tags, only: [:index]
|
||||
end
|
||||
|
|
|
@ -466,7 +466,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
|
|||
end
|
||||
|
||||
namespace :harbor do
|
||||
resources :repositories, only: [:index, :show] do
|
||||
resources :repositories, only: [:index, :show], constraints: { id: %r{[a-zA-Z./:0-9_\-]+} } do
|
||||
resources :artifacts, only: [:index] do
|
||||
resources :tags, only: [:index]
|
||||
end
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddTemporaryIndexForOrphanedInvitedMembers < Gitlab::Database::Migration[2.0]
|
||||
disable_ddl_transaction!
|
||||
|
||||
TMP_INDEX_NAME = 'tmp_idx_orphaned_invited_members'
|
||||
|
||||
def up
|
||||
add_concurrent_index('members', :id, where: query_condition, name: TMP_INDEX_NAME)
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index_by_name('members', TMP_INDEX_NAME) if index_exists_by_name?('members', TMP_INDEX_NAME)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def query_condition
|
||||
'invite_token IS NULL and invite_accepted_at IS NOT NULL AND user_id IS NULL'
|
||||
end
|
||||
end
|
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class OrphanedInvitedMembersCleanup < Gitlab::Database::Migration[2.0]
|
||||
disable_ddl_transaction!
|
||||
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_main
|
||||
|
||||
def up
|
||||
# rubocop:disable Style/SymbolProc
|
||||
membership.where(query_condition).each_batch(of: 100) do |relation|
|
||||
relation.delete_all
|
||||
end
|
||||
# rubocop:enable Style/SymbolProc
|
||||
end
|
||||
|
||||
def down
|
||||
# This migration is irreversible
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def membership
|
||||
@membership ||= define_batchable_model('members')
|
||||
end
|
||||
|
||||
def query_condition
|
||||
'invite_token IS NULL and invite_accepted_at IS NOT NULL AND user_id IS NULL'
|
||||
end
|
||||
end
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoveTemporaryIndexForOrphanedInvitedMembers < Gitlab::Database::Migration[2.0]
|
||||
disable_ddl_transaction!
|
||||
|
||||
TMP_INDEX_NAME = 'tmp_idx_orphaned_invited_members'
|
||||
|
||||
def up
|
||||
remove_concurrent_index_by_name('members', TMP_INDEX_NAME) if index_exists_by_name?('members', TMP_INDEX_NAME)
|
||||
end
|
||||
|
||||
def down
|
||||
add_concurrent_index('members', :id, where: query_condition, name: TMP_INDEX_NAME)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def query_condition
|
||||
'invite_token IS NULL and invite_accepted_at IS NOT NULL AND user_id IS NULL'
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
aa0b767ad0e38500e0eef83d5c8306054952363166f8cc2076ce48feeac1b0e1
|
|
@ -0,0 +1 @@
|
|||
badc3556e1dea545bbf8b55fb33065f45598df9b3fda74bffd28e89d7485e0b4
|
|
@ -0,0 +1 @@
|
|||
85e401f0920c6eb13b6756f191ccdf70494ca40f8133f05bbd5f23ba295b115d
|
|
@ -117,7 +117,7 @@ Some feature flags can be enabled or disabled on a per project basis:
|
|||
Feature.enable(:<feature flag>, Project.find(<project id>))
|
||||
```
|
||||
|
||||
For example, to enable the [`:product_analytics`](../operations/product_analytics.md#enable-or-disable-product-analytics) feature flag for project `1234`:
|
||||
For example, to enable the [`:product_analytics`](../operations/product_analytics.md) feature flag for project `1234`:
|
||||
|
||||
```ruby
|
||||
Feature.enable(:product_analytics, Project.find(1234))
|
||||
|
|
|
@ -24,6 +24,7 @@ The following lists the currently supported OSs and their possible EOL dates.
|
|||
| OpenSUSE 15.3 | GitLab CE / GitLab EE 14.5.0 | x86_64, aarch64 | [OpenSUSE Install Documentation](https://about.gitlab.com/install/#opensuse-leap-15-3) | Nov 2022 | <https://en.opensuse.org/Lifetime> |
|
||||
| RHEL 8 | GitLab CE / GitLab EE 12.8.1 | x86_64, arm64 | [Use CentOS Install Documentation](https://about.gitlab.com/install/#centos-7) | May 2029 | [RHEL Details](https://access.redhat.com/support/policy/updates/errata/#Life_Cycle_Dates) |
|
||||
| SLES 12 | GitLab EE 9.0.0 | x86_64 | [Use OpenSUSE Install Documentation](https://about.gitlab.com/install/#opensuse-leap-15-3) | Oct 2027 | <https://www.suse.com/lifecycle/> |
|
||||
| SLES 15 | GitLab EE 14.8.0 | x86_64 | [Use OpenSUSE Install Documentation](https://about.gitlab.com/install/#opensuse-leap-15-3) | Dec 2024 | <https://www.suse.com/lifecycle/> |
|
||||
| Oracle Linux | GitLab CE / GitLab EE 8.14.0 | x86_64 | [Use CentOS Install Documentation](https://about.gitlab.com/install/#centos-7) | Jul 2024 | <https://www.oracle.com/a/ocom/docs/elsp-lifetime-069338.pdf> |
|
||||
| Scientific Linux | GitLab CE / GitLab EE 8.14.0 | x86_64 | [Use CentOS Install Documentation](https://about.gitlab.com/install/#centos-7) | June 2024 | <https://scientificlinux.org/downloads/sl-versions/sl7/> |
|
||||
| Ubuntu 18.04 | GitLab CE / GitLab EE 10.7.0 | amd64 | [Ubuntu Install Documentation](https://about.gitlab.com/install/#ubuntu) | April 2023 | <https://wiki.ubuntu.com/Releases> |
|
||||
|
|
|
@ -212,11 +212,11 @@ curl --header "Private-Token: <personal_access_token>" \
|
|||
|
||||
This writes the downloaded file using the remote filename in the current directory.
|
||||
|
||||
## Download a binary file's index
|
||||
## Download a packages index
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64923) in GitLab 14.2.
|
||||
|
||||
Download a distribution index.
|
||||
Download a packages index.
|
||||
|
||||
```plaintext
|
||||
GET <route-prefix>/dists/*distribution/:component/binary-:architecture/Packages
|
||||
|
@ -229,14 +229,73 @@ GET <route-prefix>/dists/*distribution/:component/binary-:architecture/Packages
|
|||
| `architecture` | string | yes | The distribution architecture type. |
|
||||
|
||||
```shell
|
||||
curl --header "Private-Token: <personal_access_token>" "https://gitlab.example.com/api/v4/projects/1/packages/debian/dists/my-distro/main/amd64/Packages"
|
||||
curl --header "Private-Token: <personal_access_token>" "https://gitlab.example.com/api/v4/projects/1/packages/debian/dists/my-distro/main/binary-amd64/Packages"
|
||||
```
|
||||
|
||||
Write the output to a file:
|
||||
|
||||
```shell
|
||||
curl --header "Private-Token: <personal_access_token>" \
|
||||
"https://gitlab.example.com/api/v4/projects/1/packages/debian/dists/my-distro/main/amd64/Packages" \
|
||||
"https://gitlab.example.com/api/v4/projects/1/packages/debian/dists/my-distro/main/binary-amd64/Packages" \
|
||||
--remote-name
|
||||
```
|
||||
|
||||
This writes the downloaded file using the remote filename in the current directory.
|
||||
|
||||
## Download a Debian Installer packages index
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71918) in GitLab 15.4.
|
||||
|
||||
Download a Debian Installer packages index.
|
||||
|
||||
```plaintext
|
||||
GET <route-prefix>/dists/*distribution/:component/debian-installer/binary-:architecture/Packages
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| ----------------- | ------ | -------- | ----------- |
|
||||
| `distribution` | string | yes | The codename or suite of the Debian distribution. |
|
||||
| `component` | string | yes | The distribution component name. |
|
||||
| `architecture` | string | yes | The distribution architecture type. |
|
||||
|
||||
```shell
|
||||
curl --header "Private-Token: <personal_access_token>" "https://gitlab.example.com/api/v4/projects/1/packages/debian/dists/my-distro/main/debian-installer/binary-amd64/Packages"
|
||||
```
|
||||
|
||||
Write the output to a file:
|
||||
|
||||
```shell
|
||||
curl --header "Private-Token: <personal_access_token>" \
|
||||
"https://gitlab.example.com/api/v4/projects/1/packages/debian/dists/my-distro/main/debian-installer/binary-amd64/Packages" \
|
||||
--remote-name
|
||||
```
|
||||
|
||||
This writes the downloaded file using the remote filename in the current directory.
|
||||
|
||||
## Download a source packages index
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71918) in GitLab 15.4.
|
||||
|
||||
Download a source packages index.
|
||||
|
||||
```plaintext
|
||||
GET <route-prefix>/dists/*distribution/:component/source/Sources
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| ----------------- | ------ | -------- | ----------- |
|
||||
| `distribution` | string | yes | The codename or suite of the Debian distribution. |
|
||||
| `component` | string | yes | The distribution component name. |
|
||||
|
||||
```shell
|
||||
curl --header "Private-Token: <personal_access_token>" "https://gitlab.example.com/api/v4/projects/1/packages/debian/dists/my-distro/main/source/Sources"
|
||||
```
|
||||
|
||||
Write the output to a file:
|
||||
|
||||
```shell
|
||||
curl --header "Private-Token: <personal_access_token>" \
|
||||
"https://gitlab.example.com/api/v4/projects/1/packages/debian/dists/my-distro/main/source/Sources" \
|
||||
--remote-name
|
||||
```
|
||||
|
||||
|
|
|
@ -1365,32 +1365,24 @@ It renders on the GitLab documentation site as:
|
|||
|
||||
On the docs site, you can format text so it's displayed as tabs.
|
||||
|
||||
NOTE:
|
||||
For now, tabs are for testing only. Do not use them on the production docs site.
|
||||
|
||||
To create a set of tabs, follow this example:
|
||||
|
||||
```markdown
|
||||
{::options parse_block_html="true" /}
|
||||
```plaintext
|
||||
::Tabs
|
||||
|
||||
<div class="js-tabs">
|
||||
:::TabTitle Tab One
|
||||
|
||||
## This is the first tab
|
||||
{: .no_toc}
|
||||
Here's some content in tab one.
|
||||
|
||||
Here's some content in tab panel one.
|
||||
:::TabTitle Tab Two
|
||||
|
||||
## Tab two
|
||||
{: .no_toc}
|
||||
Here's some other content in tab two.
|
||||
|
||||
Here's some content in tab panel two.
|
||||
|
||||
</div>
|
||||
::EndTabs
|
||||
```
|
||||
|
||||
The headings determine the tab titles. Each tab is populated with the content between the titles.
|
||||
|
||||
Use brief words for the titles, ensure they are parallel, and start each with a capital letter. For example:
|
||||
For tab titles, be brief and consistent. Ensure they are parallel, and start each with a capital letter.
|
||||
For example:
|
||||
|
||||
- `Omnibus package`, `Helm chart`, `Source`
|
||||
- `15.1 and earlier`, `15.2 and later`
|
||||
|
|
|
@ -8,8 +8,12 @@ description: 'Writing styles, markup, formatting, and other standards for GitLab
|
|||
# Recommended word list
|
||||
|
||||
To help ensure consistency in the documentation, the Technical Writing team
|
||||
recommends these wording choices. The GitLab handbook also maintains a list of
|
||||
[top misused terms](https://about.gitlab.com/handbook/communication/top-misused-terms/).
|
||||
recommends these word choices. In addition:
|
||||
|
||||
- The GitLab handbook contains a list of
|
||||
[top misused terms](https://about.gitlab.com/handbook/communication/top-misused-terms/).
|
||||
- The documentation [style guide](../styleguide#language) includes details
|
||||
about language and capitalization.
|
||||
|
||||
For guidance not on this page, we defer to these style guides:
|
||||
|
||||
|
|
|
@ -1,81 +1,45 @@
|
|||
---
|
||||
stage: Analytics
|
||||
group: Product Intelligence
|
||||
group: Product Analytics
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Product Analytics **(FREE)**
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/225167) in GitLab 13.3.
|
||||
> - It's deployed behind a feature flag, disabled by default.
|
||||
> - It's disabled on GitLab.com.
|
||||
> - It's able to be enabled or disabled per-project.
|
||||
> - It's not recommended for production use.
|
||||
> - To use it in GitLab self-managed instances, ask a GitLab administrator to enable it.
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/225167) in GitLab 13.3 [with a flag](../administration/feature_flags.md) named `product_analytics`. Disabled by default.
|
||||
|
||||
GitLab allows you to go from planning an application to getting feedback. Feedback
|
||||
is not just observability, but also knowing how people use your product.
|
||||
Product Analytics uses events sent from your application to know how they are using it.
|
||||
It's based on [Snowplow](https://github.com/snowplow/snowplow), the best open-source
|
||||
event tracker. With Product Analytics, you can receive and analyze the Snowplow data
|
||||
inside GitLab.
|
||||
FLAG:
|
||||
On self-managed GitLab, by default this feature is not available. To make it available per project or for your entire instance, ask an administrator to [enable the feature flag](../administration/feature_flags.md) named `product_analytics`. On GitLab.com, this feature is not available. The feature is not ready for production use.
|
||||
|
||||
## Enable or disable Product Analytics
|
||||
GitLab enables you to go from planning an application to getting feedback. You can use
|
||||
Product Analytics to receive and analyze events sent from your application. This analysis
|
||||
provides observability information and feedback on how people use your product.
|
||||
|
||||
Product Analytics is under development and not ready for production use. It's
|
||||
deployed behind a feature flag that's **disabled by default**.
|
||||
[GitLab administrators with access to the GitLab Rails console](../administration/feature_flags.md)
|
||||
can enable it for your instance. Product Analytics can be enabled or disabled per-project.
|
||||
Events are collected by a [Rails collector](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/36443) and
|
||||
then processed with [Snowplow](https://github.com/snowplow/snowplow). Events are stored in a GitLab database.
|
||||
|
||||
To enable it:
|
||||
## View Product Analytics
|
||||
|
||||
```ruby
|
||||
# Instance-wide
|
||||
Feature.enable(:product_analytics)
|
||||
# or by project
|
||||
Feature.enable(:product_analytics, Project.find(<project ID>))
|
||||
```
|
||||
You can view the event data collected about your applications.
|
||||
|
||||
To disable it:
|
||||
Prerequisite:
|
||||
|
||||
```ruby
|
||||
# Instance-wide
|
||||
Feature.disable(:product_analytics)
|
||||
# or by project
|
||||
Feature.disable(:product_analytics, Project.find(<project ID>))
|
||||
```
|
||||
- You must have at least the Reporter role.
|
||||
|
||||
## Access Product Analytics
|
||||
To access Product Analytics:
|
||||
|
||||
After enabling the feature flag for Product Analytics, you can access the
|
||||
user interface:
|
||||
1. On the top bar, select **Menu > Projects** and find your project.
|
||||
1. On the left sidebar, select **Monitor > Product Analytics**.
|
||||
|
||||
1. Sign in to GitLab as a user with at least the Reporter role.
|
||||
1. Navigate to **Monitor > Product Analytics**.
|
||||
The Product Analytics interface contains:
|
||||
|
||||
The user interface contains:
|
||||
- An Events tab that shows the recent events and a total count.
|
||||
- A Graph tab that shows graphs based on events of the last 30 days.
|
||||
- A Test tab that sends a sample event payload.
|
||||
- A Setup page containing the code to implement in your application.
|
||||
|
||||
- An Events page that shows the recent events and a total count.
|
||||
- A test page that sends a sample event.
|
||||
- A setup page containing the code to implement in your application.
|
||||
|
||||
## Rate limits for Product Analytics
|
||||
## Rate limits
|
||||
|
||||
While Product Analytics is under development, it's rate-limited to
|
||||
**100 events per minute** per project. This limit prevents the events table in the
|
||||
database from growing too quickly.
|
||||
|
||||
## Data storage for Product Analytics
|
||||
|
||||
Product Analytics stores events are stored in GitLab database.
|
||||
|
||||
WARNING:
|
||||
This data storage is experimental, and GitLab is likely to remove this data during
|
||||
future development.
|
||||
|
||||
## Event collection
|
||||
|
||||
Events are collected by [Rails collector](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/36443),
|
||||
allowing GitLab to ship the feature fast. Due to scalability issue, GitLab plans
|
||||
to switch to a separate application, such as
|
||||
[snowplow-go-collector](https://gitlab.com/gitlab-org/snowplow-go-collector), for event collection.
|
||||
|
|
|
@ -57,6 +57,7 @@ From the Vulnerability Report you can:
|
|||
- [View an issue raised for a vulnerability](#view-issues-raised-for-a-vulnerability).
|
||||
- [Change the status of vulnerabilities](#change-status-of-vulnerabilities).
|
||||
- [Export details of vulnerabilities](#export-vulnerability-details).
|
||||
- [Sort vulnerabilities by date](#sort-vulnerabilities-by-date-detected).
|
||||
- [Manually add a vulnerability finding](#manually-add-a-vulnerability-finding).
|
||||
|
||||
## Vulnerability Report filters
|
||||
|
@ -186,6 +187,12 @@ Vulnerability records cannot be deleted, so a permanent record always remains.
|
|||
|
||||
If a vulnerability is dismissed in error, reverse the dismissal by changing its status.
|
||||
|
||||
## Sort vulnerabilities by date detected
|
||||
|
||||
By default, vulnerabilities are sorted by severity level, with the highest-severity vulnerabilities listed at the top.
|
||||
|
||||
To sort vulnerabilities by the date each vulnerability was detected, click the "Detected" column header.
|
||||
|
||||
## Export vulnerability details
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/213014) in the Security Center (previously known as the Instance Security Dashboard) and project-level Vulnerability Report (previously known as the Project Security Dashboard) in GitLab 13.0.
|
||||
|
|
|
@ -49,7 +49,7 @@ For details, view the [architecture documentation](https://gitlab.com/gitlab-org
|
|||
|
||||
To update a Kubernetes cluster by using GitOps, complete the following steps.
|
||||
|
||||
1. Ensure you have a working Kubernetes cluster, and that the manifests are in a GitLab project.
|
||||
1. Ensure you have a working Kubernetes cluster, and that the manifests or [Helm charts](gitops/helm.md) are in a GitLab project.
|
||||
1. In the same project, [register and install the GitLab agent](install/index.md).
|
||||
1. Configure the agent configuration file so that the agent monitors the project for changes to the Kubernetes manifests.
|
||||
Use the [GitOps configuration reference](#gitops-configuration-reference) for guidance.
|
||||
|
@ -112,12 +112,12 @@ a Kubernetes SIG project. You can read more about the available annotations in t
|
|||
|
||||
## Automatic drift remediation
|
||||
|
||||
Drift happens when the current configuration of an infrastructure resource differs from its expected configuration.
|
||||
Typically, this is caused by manually editing resources directly through the service that created the resource. Minimizing the
|
||||
risk of drift helps to ensure configuration consistency and successful operations.
|
||||
Drift happens when the current configuration of an infrastructure resource differs from its desired configuration.
|
||||
Typically, this is caused by manually editing resources directly rather than via the used infrastructure-as-code
|
||||
mechanism. Minimizing the risk of drift helps to ensure configuration consistency and successful operations.
|
||||
|
||||
In GitLab, the agent for Kubernetes regularly compares the expected state from the `git` repository with
|
||||
the known state from the `cluster`. Deviations from the `git` state are fixed at every check. These checks
|
||||
In GitLab, the agent for Kubernetes regularly compares the desired state from the `git` repository with
|
||||
the actual state from the Kubernetes cluster. Deviations from the `git` state are fixed at every check. These checks
|
||||
happen automatically every 5 minutes. They are not configurable.
|
||||
|
||||
The agent uses [server-side applies](https://kubernetes.io/docs/reference/using-api/server-side-apply/).
|
||||
|
@ -127,6 +127,7 @@ are checked for drift. This facilitates the use of in-cluster controllers to mod
|
|||
|
||||
## Related topics
|
||||
|
||||
- [Deploying Helm charts with the GitOps workflow](gitops/helm.md)
|
||||
- [GitOps working examples for training and demos](https://gitlab.com/groups/guided-explorations/gl-k8s-agent/gitops/-/wikis/home)
|
||||
- [Self-paced classroom workshop](https://gitlab-for-eks.awsworkshop.io) (Uses AWS EKS, but you can use for other Kubernetes clusters)
|
||||
- [Managing Kubernetes secrets in a GitOps workflow](gitops/secrets_management.md)
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
---
|
||||
stage: Configure
|
||||
group: Configure
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Using Helm charts to update a Kubernetes cluster (Alpha) **(FREE)**
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/371019) in GitLab 15.4.
|
||||
|
||||
You can deploy Helm charts to your Kubernetes cluster and keep the resources in your cluster in sync
|
||||
with your charts and values. To do this, you use the pull-based GitOps features of the agent for
|
||||
Kubernetes.
|
||||
|
||||
This feature is in Alpha and [an epic exists](https://gitlab.com/groups/gitlab-org/-/epics/7938)
|
||||
to track future work. Please tell us about your use cases by leaving comments in the epic.
|
||||
|
||||
NOTE:
|
||||
This feature is Alpha. In future releases, to accommodate new features, the configuration format might change without notice.
|
||||
|
||||
## GitOps workflow steps
|
||||
|
||||
To update a Kubernetes cluster by using GitOps with charts, complete the following steps.
|
||||
|
||||
1. Ensure you have a working Kubernetes cluster, and that the chart is in a GitLab project.
|
||||
1. In the same project, [register and install the GitLab agent](../install/index.md).
|
||||
1. Configure the agent configuration file so that the agent monitors the project for changes to the chart.
|
||||
Use the [GitOps configuration reference](#helm-configuration-reference) for guidance.
|
||||
|
||||
## Helm chart with GitOps workflow
|
||||
|
||||
To update a Kubernetes cluster by using Helm charts:
|
||||
|
||||
1. Ensure you have a working Kubernetes cluster.
|
||||
1. In a GitLab project:
|
||||
- Store your Helm charts.
|
||||
- [Register and install the GitLab agent](../install/index.md).
|
||||
1. Update the agent configuration file so that the agent monitors the project for changes to the chart.
|
||||
Use the [configuration reference](#helm-configuration-reference) for guidance.
|
||||
|
||||
Any time you commit updates to your chart repository, the agent applies the chart in the cluster.
|
||||
|
||||
## Helm configuration reference
|
||||
|
||||
The following snippet shows an example of the possible keys and values for the GitOps section of an [agent configuration file](../install/index.md#create-an-agent-configuration-file) (`config.yaml`).
|
||||
|
||||
```yaml
|
||||
gitops:
|
||||
charts:
|
||||
- release_name: my-application-release
|
||||
source:
|
||||
project:
|
||||
id: my-group/my-project-with-chart
|
||||
path: dir-in-project/with/charts
|
||||
namespace: my-ns
|
||||
max_history: 1
|
||||
```
|
||||
|
||||
| Keyword | Description |
|
||||
|--|--|
|
||||
| `charts` | List of charts you want to be applied in your cluster. Charts are applied concurrently. All charts must be in the same directory. |
|
||||
| `release_name` | Required. Name of the release to use when applying the chart. |
|
||||
| `id` | Required. ID of the project where Helm chart is committed. No authentication mechanisms are currently supported. |
|
||||
| `path` | Optional. Path of the chart in the project repository. Root of the repository is used by default. This is the directory with the `Chart.yaml` file. |
|
||||
| `namespace` | Optional. Namespace to use when applying the chart. Defaults to `default`. |
|
||||
| `max_history` | Optional. Maximum number of release [revisions to store in the cluster](https://helm.sh/docs/helm/helm_history/). |
|
||||
|
||||
## Automatic drift remediation
|
||||
|
||||
Drift happens when the current configuration of an infrastructure resource differs from its desired configuration.
|
||||
Typically, drift is caused by manually editing resources directly, rather than by editing the code that describes the desired state. Minimizing the risk of drift helps to ensure configuration consistency and successful operations.
|
||||
mechanism. Minimizing the risk of drift helps to ensure configuration consistency and successful operations.
|
||||
|
||||
In GitLab, the agent for Kubernetes regularly compares the desired state from the chart source with
|
||||
the actual state from the Kubernetes cluster. Deviations from the desired state are fixed at every check. These checks
|
||||
happen automatically every 5 minutes. They are not configurable.
|
||||
|
||||
## Known issues
|
||||
|
||||
The following are known issues:
|
||||
|
||||
- Your chart must be in a GitLab project. The project must be an agent configuration project or a public
|
||||
project. This known issue also exists for manifest-based GitOps and is tracked in
|
||||
[this epic](https://gitlab.com/groups/gitlab-org/-/epics/7704).
|
||||
- Values for the chart must be in a `values.yaml` file. This file must be with the chart,
|
||||
in the same project and path.
|
||||
- Because of drift detection and remediation, release history, stored in the cluster, is not useful.
|
||||
A new release is created every five minutes and the oldest release is discarded.
|
||||
Eventually history consists only of the same information.
|
||||
View [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/372023) for details.
|
||||
|
||||
## Example repository layout
|
||||
|
||||
```plaintext
|
||||
/my-chart
|
||||
├── templates
|
||||
| └── ...
|
||||
├── charts
|
||||
| └── ...
|
||||
├── Chart.yaml
|
||||
├── Chart.lock
|
||||
├── values.yaml
|
||||
├── values.schema.json
|
||||
└── some-file-used-in-chart.txt
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Agent cannot find values for the chart
|
||||
|
||||
Make sure values are in `values.yaml` and in the same directory as the `Chart.yaml` file.
|
||||
The filename must be lowercase, with `.yaml` extension (not `.yml`).
|
|
@ -175,3 +175,43 @@ To install a package:
|
|||
```shell
|
||||
sudo apt-get -y install -t <codename> <package-name>
|
||||
```
|
||||
|
||||
## Download a source package
|
||||
|
||||
To download a source package:
|
||||
|
||||
1. Configure the repository:
|
||||
|
||||
If you are using a private project, add your [credentials](#authenticate-to-the-package-registry) to your apt configuration:
|
||||
|
||||
```shell
|
||||
echo 'machine gitlab.example.com login <username> password <your_access_token>' \
|
||||
| sudo tee /etc/apt/auth.conf.d/gitlab_project.conf
|
||||
```
|
||||
|
||||
Download your distribution key:
|
||||
|
||||
```shell
|
||||
sudo mkdir -p /usr/local/share/keyrings
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
"https://gitlab.example.com/api/v4/projects/<project_id>/debian_distributions/<codename>/key.asc" \
|
||||
| \
|
||||
gpg --dearmor \
|
||||
| \
|
||||
sudo tee /usr/local/share/keyrings/<codename>-archive-keyring.gpg \
|
||||
> /dev/null
|
||||
```
|
||||
|
||||
Add your project as a source:
|
||||
|
||||
```shell
|
||||
echo 'deb-src [ signed-by=/usr/local/share/keyrings/<codename>-archive-keyring.gpg ] https://gitlab.example.com/api/v4/projects/<project_id>/packages/debian <codename> <component1> <component2>' \
|
||||
| sudo tee /etc/apt/sources.list.d/gitlab_project-sources.list
|
||||
sudo apt-get update
|
||||
```
|
||||
|
||||
1. Download the source package:
|
||||
|
||||
```shell
|
||||
sudo apt-get source -t <codename> <package-name>
|
||||
```
|
||||
|
|
|
@ -42,6 +42,23 @@ module API
|
|||
|
||||
present_carrierwave_file!(package_file.file)
|
||||
end
|
||||
|
||||
def present_index_file!(file_type)
|
||||
relation = "::Packages::Debian::#{project_or_group.class.name}ComponentFile".constantize
|
||||
|
||||
component_file = relation
|
||||
.preload_distribution
|
||||
.with_container(project_or_group)
|
||||
.with_codename_or_suite(params[:distribution])
|
||||
.with_component_name(params[:component])
|
||||
.with_file_type(file_type)
|
||||
.with_architecture_name(params[:architecture])
|
||||
.with_compression_type(nil)
|
||||
.order_created_asc
|
||||
.last!
|
||||
|
||||
present_carrierwave_file!(component_file.file)
|
||||
end
|
||||
end
|
||||
|
||||
rescue_from ArgumentError do |e|
|
||||
|
@ -66,6 +83,7 @@ module API
|
|||
|
||||
namespace 'dists/*distribution', requirements: DISTRIBUTION_REQUIREMENTS do
|
||||
# GET {projects|groups}/:id/packages/debian/dists/*distribution/Release.gpg
|
||||
# https://wiki.debian.org/DebianRepository/Format#A.22Release.22_files
|
||||
desc 'The Release file signature' do
|
||||
detail 'This feature was introduced in GitLab 13.5'
|
||||
end
|
||||
|
@ -76,6 +94,7 @@ module API
|
|||
end
|
||||
|
||||
# GET {projects|groups}/:id/packages/debian/dists/*distribution/Release
|
||||
# https://wiki.debian.org/DebianRepository/Format#A.22Release.22_files
|
||||
desc 'The unsigned Release file' do
|
||||
detail 'This feature was introduced in GitLab 13.5'
|
||||
end
|
||||
|
@ -86,6 +105,7 @@ module API
|
|||
end
|
||||
|
||||
# GET {projects|groups}/:id/packages/debian/dists/*distribution/InRelease
|
||||
# https://wiki.debian.org/DebianRepository/Format#A.22Release.22_files
|
||||
desc 'The signed Release file' do
|
||||
detail 'This feature was introduced in GitLab 13.5'
|
||||
end
|
||||
|
@ -97,31 +117,54 @@ module API
|
|||
|
||||
params do
|
||||
requires :component, type: String, desc: 'The Debian Component', regexp: Gitlab::Regex.debian_component_regex
|
||||
requires :architecture, type: String, desc: 'The Debian Architecture', regexp: Gitlab::Regex.debian_architecture_regex
|
||||
end
|
||||
|
||||
namespace ':component/binary-:architecture', requirements: COMPONENT_ARCHITECTURE_REQUIREMENTS do
|
||||
# GET {projects|groups}/:id/packages/debian/dists/*distribution/:component/binary-:architecture/Packages
|
||||
desc 'The binary files index' do
|
||||
detail 'This feature was introduced in GitLab 13.5'
|
||||
namespace ':component', requirements: COMPONENT_ARCHITECTURE_REQUIREMENTS do
|
||||
params do
|
||||
requires :architecture, type: String, desc: 'The Debian Architecture', regexp: Gitlab::Regex.debian_architecture_regex
|
||||
end
|
||||
|
||||
route_setting :authentication, authenticate_non_public: true
|
||||
get 'Packages' do
|
||||
relation = "::Packages::Debian::#{project_or_group.class.name}ComponentFile".constantize
|
||||
namespace 'debian-installer/binary-:architecture' do
|
||||
# GET {projects|groups}/:id/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/Packages
|
||||
# https://wiki.debian.org/DebianRepository/Format#A.22Packages.22_Indices
|
||||
desc 'The installer (udeb) binary files index' do
|
||||
detail 'This feature was introduced in GitLab 15.4'
|
||||
end
|
||||
|
||||
component_file = relation
|
||||
.preload_distribution
|
||||
.with_container(project_or_group)
|
||||
.with_codename_or_suite(params[:distribution])
|
||||
.with_component_name(params[:component])
|
||||
.with_file_type(:packages)
|
||||
.with_architecture_name(params[:architecture])
|
||||
.with_compression_type(nil)
|
||||
.order_created_asc
|
||||
.last!
|
||||
route_setting :authentication, authenticate_non_public: true
|
||||
get 'Packages' do
|
||||
present_index_file!(:di_packages)
|
||||
end
|
||||
end
|
||||
|
||||
present_carrierwave_file!(component_file.file)
|
||||
namespace 'source', requirements: COMPONENT_ARCHITECTURE_REQUIREMENTS do
|
||||
# GET {projects|groups}/:id/packages/debian/dists/*distribution/:component/source/Sources
|
||||
# https://wiki.debian.org/DebianRepository/Format#A.22Sources.22_Indices
|
||||
desc 'The source files index' do
|
||||
detail 'This feature was introduced in GitLab 15.4'
|
||||
end
|
||||
|
||||
route_setting :authentication, authenticate_non_public: true
|
||||
get 'Sources' do
|
||||
present_index_file!(:sources)
|
||||
end
|
||||
end
|
||||
|
||||
params do
|
||||
requires :architecture, type: String, desc: 'The Debian Architecture', regexp: Gitlab::Regex.debian_architecture_regex
|
||||
end
|
||||
|
||||
namespace 'binary-:architecture', requirements: COMPONENT_ARCHITECTURE_REQUIREMENTS do
|
||||
# GET {projects|groups}/:id/packages/debian/dists/*distribution/:component/binary-:architecture/Packages
|
||||
# https://wiki.debian.org/DebianRepository/Format#A.22Packages.22_Indices
|
||||
desc 'The binary files index' do
|
||||
detail 'This feature was introduced in GitLab 13.5'
|
||||
end
|
||||
|
||||
route_setting :authentication, authenticate_non_public: true
|
||||
get 'Packages' do
|
||||
present_index_file!(:packages)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -33,7 +33,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def password
|
||||
@password ||= Gitlab::Utils.force_utf8(::User.random_password.downcase)
|
||||
@password ||= Gitlab::Utils.force_utf8(::User.random_password)
|
||||
end
|
||||
|
||||
def location
|
||||
|
|
|
@ -4,6 +4,11 @@ require_relative '../utils' # Gitlab::Utils
|
|||
|
||||
module Gitlab
|
||||
module Cluster
|
||||
# We take advantage of the fact that the application is pre-loaded in the primary
|
||||
# process. If it's a pre-fork server like Puma, this will be the Puma master process.
|
||||
# Otherwise it is the worker itself such as for Sidekiq.
|
||||
PRIMARY_PID = $$
|
||||
|
||||
#
|
||||
# LifecycleEvents lets Rails initializers register application startup hooks
|
||||
# that are sensitive to forking. For example, to defer the creation of
|
||||
|
|
|
@ -17,7 +17,7 @@ module Gitlab
|
|||
message: 'Id invalid'
|
||||
}, allow_blank: true
|
||||
validates :artifact_id, format: {
|
||||
with: /\A[a-zA-Z0-9\_\.\-$]+\z/,
|
||||
with: /\A[a-zA-Z0-9\_\.\-$:]+\z/,
|
||||
message: 'Id invalid'
|
||||
}, allow_blank: true
|
||||
validates :sort, format: {
|
||||
|
|
|
@ -16,8 +16,9 @@ module Gitlab
|
|||
# The duration for which a process may be above a given fragmentation
|
||||
# threshold is computed as `max_strikes * sleep_time_seconds`.
|
||||
class Watchdog
|
||||
DEFAULT_SLEEP_TIME_SECONDS = 60
|
||||
DEFAULT_HEAP_FRAG_THRESHOLD = 0.5
|
||||
DEFAULT_SLEEP_TIME_SECONDS = 60 * 5
|
||||
DEFAULT_MAX_HEAP_FRAG = 0.5
|
||||
DEFAULT_MAX_MEM_GROWTH = 3.0
|
||||
DEFAULT_MAX_STRIKES = 5
|
||||
|
||||
# This handler does nothing. It returns `false` to indicate to the
|
||||
|
@ -29,7 +30,7 @@ module Gitlab
|
|||
class NullHandler
|
||||
include Singleton
|
||||
|
||||
def on_high_heap_fragmentation(value)
|
||||
def call
|
||||
# NOP
|
||||
false
|
||||
end
|
||||
|
@ -41,7 +42,7 @@ module Gitlab
|
|||
@pid = pid
|
||||
end
|
||||
|
||||
def on_high_heap_fragmentation(value)
|
||||
def call
|
||||
Process.kill(:TERM, @pid)
|
||||
true
|
||||
end
|
||||
|
@ -55,7 +56,7 @@ module Gitlab
|
|||
@worker = ::Puma::Cluster::WorkerHandle.new(0, $$, 0, puma_options)
|
||||
end
|
||||
|
||||
def on_high_heap_fragmentation(value)
|
||||
def call
|
||||
@worker.term
|
||||
true
|
||||
end
|
||||
|
@ -63,6 +64,9 @@ module Gitlab
|
|||
|
||||
# max_heap_fragmentation:
|
||||
# The degree to which the Ruby heap is allowed to be fragmented. Range [0,1].
|
||||
# max_mem_growth:
|
||||
# A multiplier for how much excess private memory a worker can map compared to a reference process
|
||||
# (itself or the primary in a pre-fork server.)
|
||||
# max_strikes:
|
||||
# How many times the process is allowed to be above max_heap_fragmentation before
|
||||
# a handler is invoked.
|
||||
|
@ -71,7 +75,8 @@ module Gitlab
|
|||
def initialize(
|
||||
handler: NullHandler.instance,
|
||||
logger: Logger.new($stdout),
|
||||
max_heap_fragmentation: ENV['GITLAB_MEMWD_MAX_HEAP_FRAG']&.to_f || DEFAULT_HEAP_FRAG_THRESHOLD,
|
||||
max_heap_fragmentation: ENV['GITLAB_MEMWD_MAX_HEAP_FRAG']&.to_f || DEFAULT_MAX_HEAP_FRAG,
|
||||
max_mem_growth: ENV['GITLAB_MEMWD_MAX_MEM_GROWTH']&.to_f || DEFAULT_MAX_MEM_GROWTH,
|
||||
max_strikes: ENV['GITLAB_MEMWD_MAX_STRIKES']&.to_i || DEFAULT_MAX_STRIKES,
|
||||
sleep_time_seconds: ENV['GITLAB_MEMWD_SLEEP_TIME_SEC']&.to_i || DEFAULT_SLEEP_TIME_SECONDS,
|
||||
**options)
|
||||
|
@ -79,17 +84,37 @@ module Gitlab
|
|||
|
||||
@handler = handler
|
||||
@logger = logger
|
||||
@max_heap_fragmentation = max_heap_fragmentation
|
||||
@sleep_time_seconds = sleep_time_seconds
|
||||
@max_strikes = max_strikes
|
||||
@stats = {
|
||||
heap_frag: {
|
||||
max: max_heap_fragmentation,
|
||||
strikes: 0
|
||||
},
|
||||
mem_growth: {
|
||||
max: max_mem_growth,
|
||||
strikes: 0
|
||||
}
|
||||
}
|
||||
|
||||
@alive = true
|
||||
@strikes = 0
|
||||
|
||||
init_prometheus_metrics(max_heap_fragmentation)
|
||||
end
|
||||
|
||||
attr_reader :strikes, :max_heap_fragmentation, :max_strikes, :sleep_time_seconds
|
||||
attr_reader :max_strikes, :sleep_time_seconds
|
||||
|
||||
def max_heap_fragmentation
|
||||
@stats[:heap_frag][:max]
|
||||
end
|
||||
|
||||
def max_mem_growth
|
||||
@stats[:mem_growth][:max]
|
||||
end
|
||||
|
||||
def strikes(stat)
|
||||
@stats[stat][:strikes]
|
||||
end
|
||||
|
||||
def call
|
||||
@logger.info(log_labels.merge(message: 'started'))
|
||||
|
@ -97,7 +122,10 @@ module Gitlab
|
|||
while @alive
|
||||
sleep(@sleep_time_seconds)
|
||||
|
||||
monitor_heap_fragmentation if Feature.enabled?(:gitlab_memory_watchdog, type: :ops)
|
||||
next unless Feature.enabled?(:gitlab_memory_watchdog, type: :ops)
|
||||
|
||||
monitor_heap_fragmentation
|
||||
monitor_memory_growth
|
||||
end
|
||||
|
||||
@logger.info(log_labels.merge(message: 'stopped'))
|
||||
|
@ -109,32 +137,73 @@ module Gitlab
|
|||
|
||||
private
|
||||
|
||||
def monitor_heap_fragmentation
|
||||
heap_fragmentation = Gitlab::Metrics::Memory.gc_heap_fragmentation
|
||||
def monitor_memory_condition(stat_key)
|
||||
return unless @alive
|
||||
|
||||
if heap_fragmentation > @max_heap_fragmentation
|
||||
@strikes += 1
|
||||
@heap_frag_violations.increment
|
||||
stat = @stats[stat_key]
|
||||
|
||||
ok, labels = yield(stat)
|
||||
|
||||
if ok
|
||||
stat[:strikes] = 0
|
||||
else
|
||||
@strikes = 0
|
||||
stat[:strikes] += 1
|
||||
@counter_violations.increment(reason: stat_key.to_s)
|
||||
end
|
||||
|
||||
if @strikes > @max_strikes
|
||||
# If the handler returns true, it means the event is handled and we can shut down.
|
||||
@alive = !handle_heap_fragmentation_limit_exceeded(heap_fragmentation)
|
||||
@strikes = 0
|
||||
if stat[:strikes] > @max_strikes
|
||||
@alive = !memory_limit_exceeded_callback(stat_key, labels)
|
||||
stat[:strikes] = 0
|
||||
end
|
||||
end
|
||||
|
||||
def handle_heap_fragmentation_limit_exceeded(value)
|
||||
@logger.warn(
|
||||
log_labels.merge(
|
||||
message: 'heap fragmentation limit exceeded',
|
||||
memwd_cur_heap_frag: value
|
||||
))
|
||||
@heap_frag_violations_handled.increment
|
||||
def monitor_heap_fragmentation
|
||||
monitor_memory_condition(:heap_frag) do |stat|
|
||||
heap_fragmentation = Gitlab::Metrics::Memory.gc_heap_fragmentation
|
||||
[
|
||||
heap_fragmentation <= stat[:max],
|
||||
{
|
||||
message: 'heap fragmentation limit exceeded',
|
||||
memwd_cur_heap_frag: heap_fragmentation,
|
||||
memwd_max_heap_frag: stat[:max]
|
||||
}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
handler.on_high_heap_fragmentation(value)
|
||||
def monitor_memory_growth
|
||||
monitor_memory_condition(:mem_growth) do |stat|
|
||||
worker_uss = Gitlab::Metrics::System.memory_usage_uss_pss[:uss]
|
||||
reference_uss = reference_mem[:uss]
|
||||
memory_limit = stat[:max] * reference_uss
|
||||
[
|
||||
worker_uss <= memory_limit,
|
||||
{
|
||||
message: 'memory limit exceeded',
|
||||
memwd_uss_bytes: worker_uss,
|
||||
memwd_ref_uss_bytes: reference_uss,
|
||||
memwd_max_uss_bytes: memory_limit
|
||||
}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
# On pre-fork systems this would be the primary process memory from which workers fork.
|
||||
# Otherwise it is the current process' memory.
|
||||
#
|
||||
# We initialize this lazily because in the initializer the application may not have
|
||||
# finished booting yet, which would yield an incorrect baseline.
|
||||
def reference_mem
|
||||
@reference_mem ||= Gitlab::Metrics::System.memory_usage_uss_pss(pid: Gitlab::Cluster::PRIMARY_PID)
|
||||
end
|
||||
|
||||
def memory_limit_exceeded_callback(stat_key, handler_labels)
|
||||
all_labels = log_labels.merge(handler_labels)
|
||||
.merge(memwd_cur_strikes: strikes(stat_key))
|
||||
@logger.warn(all_labels)
|
||||
@counter_violations_handled.increment(reason: stat_key.to_s)
|
||||
|
||||
handler.call
|
||||
end
|
||||
|
||||
def handler
|
||||
|
@ -151,9 +220,7 @@ module Gitlab
|
|||
worker_id: worker_id,
|
||||
memwd_handler_class: handler.class.name,
|
||||
memwd_sleep_time_s: @sleep_time_seconds,
|
||||
memwd_max_heap_frag: @max_heap_fragmentation,
|
||||
memwd_max_strikes: @max_strikes,
|
||||
memwd_cur_strikes: @strikes,
|
||||
memwd_rss_bytes: process_rss_bytes
|
||||
}
|
||||
end
|
||||
|
@ -174,14 +241,14 @@ module Gitlab
|
|||
@heap_frag_limit.set({}, max_heap_fragmentation)
|
||||
|
||||
default_labels = { pid: worker_id }
|
||||
@heap_frag_violations = Gitlab::Metrics.counter(
|
||||
:gitlab_memwd_heap_frag_violations_total,
|
||||
'Total number of times heap fragmentation in a Ruby process exceeded its allowed maximum',
|
||||
@counter_violations = Gitlab::Metrics.counter(
|
||||
:gitlab_memwd_violations_total,
|
||||
'Total number of times a Ruby process violated a memory threshold',
|
||||
default_labels
|
||||
)
|
||||
@heap_frag_violations_handled = Gitlab::Metrics.counter(
|
||||
:gitlab_memwd_heap_frag_violations_handled_total,
|
||||
'Total number of times heap fragmentation violations in a Ruby process were handled',
|
||||
@counter_violations_handled = Gitlab::Metrics.counter(
|
||||
:gitlab_memwd_violations_handled_total,
|
||||
'Total number of times Ruby process memory violations were handled',
|
||||
default_labels
|
||||
)
|
||||
end
|
||||
|
|
|
@ -50,7 +50,9 @@ module Sidebars
|
|||
end
|
||||
|
||||
def harbor_registry__menu_item
|
||||
return nil_menu_item(:harbor_registry) if Feature.disabled?(:harbor_registry_integration)
|
||||
if Feature.disabled?(:harbor_registry_integration) || context.group.harbor_integration.nil?
|
||||
return nil_menu_item(:harbor_registry)
|
||||
end
|
||||
|
||||
::Sidebars::MenuItem.new(
|
||||
title: _('Harbor Registry'),
|
||||
|
|
|
@ -66,7 +66,9 @@ module Sidebars
|
|||
end
|
||||
|
||||
def harbor_registry__menu_item
|
||||
return ::Sidebars::NilMenuItem.new(item_id: :harbor_registry) if Feature.disabled?(:harbor_registry_integration)
|
||||
if Feature.disabled?(:harbor_registry_integration) || context.project.harbor_integration.nil?
|
||||
return ::Sidebars::NilMenuItem.new(item_id: :harbor_registry)
|
||||
end
|
||||
|
||||
::Sidebars::MenuItem.new(
|
||||
title: _('Harbor Registry'),
|
||||
|
|
|
@ -148,6 +148,11 @@ msgid_plural "%d approvers (you've approved)"
|
|||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "%d artifact"
|
||||
msgid_plural "%d artifacts"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "%d assigned issue"
|
||||
msgid_plural "%d assigned issues"
|
||||
msgstr[0] ""
|
||||
|
@ -19220,17 +19225,17 @@ msgstr ""
|
|||
msgid "HarborIntegration|Use Harbor as this project's container registry."
|
||||
msgstr ""
|
||||
|
||||
msgid "HarborRegistry|%d artifact"
|
||||
msgid_plural "HarborRegistry|%d artifacts"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "HarborRegistry|%{count} Image repository"
|
||||
msgid_plural "HarborRegistry|%{count} Image repositories"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "HarborRegistry|%{count} Tag"
|
||||
msgid_plural "HarborRegistry|%{count} Tags"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "HarborRegistry|Configuration digest: %{digest}"
|
||||
msgid "HarborRegistry|-- artifacts"
|
||||
msgstr ""
|
||||
|
||||
msgid "HarborRegistry|Digest: %{imageId}"
|
||||
|
@ -19242,43 +19247,37 @@ msgstr ""
|
|||
msgid "HarborRegistry|Harbor connection error"
|
||||
msgstr ""
|
||||
|
||||
msgid "HarborRegistry|Invalid tag: missing manifest digest"
|
||||
msgstr ""
|
||||
|
||||
msgid "HarborRegistry|Last updated %{time}"
|
||||
msgstr ""
|
||||
|
||||
msgid "HarborRegistry|Manifest digest: %{digest}"
|
||||
msgstr ""
|
||||
|
||||
msgid "HarborRegistry|Please try different search criteria"
|
||||
msgstr ""
|
||||
|
||||
msgid "HarborRegistry|Published %{timeInfo}"
|
||||
msgstr ""
|
||||
|
||||
msgid "HarborRegistry|Published to the %{repositoryPath} image repository at %{time} on %{date}"
|
||||
msgid "HarborRegistry|Root image"
|
||||
msgstr ""
|
||||
|
||||
msgid "HarborRegistry|Root image"
|
||||
msgid "HarborRegistry|Something went wrong while fetching the artifact list."
|
||||
msgstr ""
|
||||
|
||||
msgid "HarborRegistry|Something went wrong while fetching the repository list."
|
||||
msgstr ""
|
||||
|
||||
msgid "HarborRegistry|Something went wrong while fetching the tags."
|
||||
msgstr ""
|
||||
|
||||
msgid "HarborRegistry|Sorry, your filter produced no results."
|
||||
msgstr ""
|
||||
|
||||
msgid "HarborRegistry|Tag"
|
||||
msgstr ""
|
||||
|
||||
msgid "HarborRegistry|The filter returned no results"
|
||||
msgstr ""
|
||||
|
||||
msgid "HarborRegistry|The image repository could not be found."
|
||||
msgid "HarborRegistry|There are no harbor images stored for this project"
|
||||
msgstr ""
|
||||
|
||||
msgid "HarborRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
|
||||
msgstr ""
|
||||
|
||||
msgid "HarborRegistry|The requested image repository does not exist or has been deleted. If you think this is an error, try refreshing the page."
|
||||
msgstr ""
|
||||
|
||||
msgid "HarborRegistry|This image has no active tags"
|
||||
msgid "HarborRegistry|This image has no artifacts"
|
||||
msgstr ""
|
||||
|
||||
msgid "HarborRegistry|To widen your search, change or remove the filters above."
|
||||
|
@ -19287,6 +19286,9 @@ msgstr ""
|
|||
msgid "HarborRegistry|We are having trouble connecting to the Harbor Registry. Please try refreshing the page. If this error persists, please review %{docLinkStart}the troubleshooting documentation%{docLinkEnd}."
|
||||
msgstr ""
|
||||
|
||||
msgid "HarborRegistry|With the Harbor Registry, every project can connect to a harbor space to store its Docker images."
|
||||
msgstr ""
|
||||
|
||||
msgid "HarborRegistry|With the Harbor Registry, every project can have its own space to store images. %{docLinkStart}More information%{docLinkEnd}"
|
||||
msgstr ""
|
||||
|
||||
|
@ -26962,6 +26964,9 @@ msgstr ""
|
|||
msgid "Notify|You have been mentioned in an issue."
|
||||
msgstr ""
|
||||
|
||||
msgid "Notify|You have been mentioned in merge request %{mr_link}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Notify|Your request to join the %{target_to_join} %{target_type} has been %{denied_tag}."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -11,8 +11,8 @@ GEM
|
|||
adamantium (0.2.0)
|
||||
ice_nine (~> 0.11.0)
|
||||
memoizable (~> 0.4.0)
|
||||
addressable (2.8.0)
|
||||
public_suffix (>= 2.0.2, < 5.0)
|
||||
addressable (2.8.1)
|
||||
public_suffix (>= 2.0.2, < 6.0)
|
||||
airborne (0.3.4)
|
||||
activesupport
|
||||
rack
|
||||
|
@ -118,7 +118,7 @@ GEM
|
|||
gitlab (4.18.0)
|
||||
httparty (~> 0.18)
|
||||
terminal-table (>= 1.5.1)
|
||||
gitlab-qa (8.4.0)
|
||||
gitlab-qa (8.4.1)
|
||||
activesupport (~> 6.1)
|
||||
gitlab (~> 4.18.0)
|
||||
http (~> 5.0)
|
||||
|
@ -223,7 +223,7 @@ GEM
|
|||
pry-byebug (3.5.1)
|
||||
byebug (~> 9.1)
|
||||
pry (~> 0.10)
|
||||
public_suffix (4.0.7)
|
||||
public_suffix (5.0.0)
|
||||
racc (1.6.0)
|
||||
rack (2.2.3.1)
|
||||
rack-test (1.1.0)
|
||||
|
|
|
@ -30,10 +30,12 @@ FactoryBot.define do
|
|||
trait(:sources) do
|
||||
file_type { :sources }
|
||||
architecture { nil }
|
||||
file_fixture { 'spec/fixtures/packages/debian/distribution/Sources' }
|
||||
end
|
||||
|
||||
trait(:di_packages) do
|
||||
file_type { :di_packages }
|
||||
file_fixture { 'spec/fixtures/packages/debian/distribution/D-I-Packages' }
|
||||
end
|
||||
|
||||
trait(:object_storage) do
|
||||
|
|
|
@ -81,7 +81,11 @@ RSpec.describe 'Group navbar' do
|
|||
end
|
||||
|
||||
context 'when harbor registry is available' do
|
||||
let(:harbor_integration) { create(:harbor_integration, group: group, project: nil) }
|
||||
|
||||
before do
|
||||
group.update!(harbor_integration: harbor_integration)
|
||||
|
||||
stub_feature_flags(harbor_registry_integration: true)
|
||||
|
||||
insert_harbor_registry_nav(_('Package Registry'))
|
||||
|
|
|
@ -17,20 +17,20 @@ RSpec.describe 'Incident timeline events', :js do
|
|||
|
||||
visit project_issues_incident_path(project, incident)
|
||||
wait_for_requests
|
||||
click_link 'Timeline'
|
||||
click_link s_('Incident|Timeline')
|
||||
end
|
||||
|
||||
context 'when add event is clicked' do
|
||||
it 'submits event data when save is clicked' do
|
||||
click_button 'Add new timeline event'
|
||||
click_button s_('Incident|Add new timeline event')
|
||||
|
||||
expect(page).to have_selector('.common-note-form')
|
||||
|
||||
fill_in 'Description', with: 'Event note goes here'
|
||||
fill_in _('Description'), with: 'Event note goes here'
|
||||
fill_in 'timeline-input-hours', with: '07'
|
||||
fill_in 'timeline-input-minutes', with: '25'
|
||||
|
||||
click_button 'Save'
|
||||
click_button _('Save')
|
||||
|
||||
expect(page).to have_selector('.incident-timeline-events')
|
||||
|
||||
|
@ -45,24 +45,24 @@ RSpec.describe 'Incident timeline events', :js do
|
|||
before do
|
||||
click_button 'Add new timeline event'
|
||||
fill_in 'Description', with: 'Event note to edit'
|
||||
click_button 'Save'
|
||||
click_button _('Save')
|
||||
end
|
||||
|
||||
it 'shows the confirmation modal and edits the event' do
|
||||
click_button 'More actions'
|
||||
click_button _('More actions')
|
||||
|
||||
page.within '.gl-new-dropdown-contents' do
|
||||
expect(page).to have_content('Edit')
|
||||
page.find('.gl-new-dropdown-item-text-primary', text: 'Edit').click
|
||||
expect(page).to have_content(_('Edit'))
|
||||
page.find('.gl-new-dropdown-item-text-primary', text: _('Edit')).click
|
||||
end
|
||||
|
||||
expect(page).to have_selector('.common-note-form')
|
||||
|
||||
fill_in 'Description', with: 'Event note goes here'
|
||||
fill_in _('Description'), with: 'Event note goes here'
|
||||
fill_in 'timeline-input-hours', with: '07'
|
||||
fill_in 'timeline-input-minutes', with: '25'
|
||||
|
||||
click_button 'Save'
|
||||
click_button _('Save')
|
||||
|
||||
wait_for_requests
|
||||
|
||||
|
@ -75,28 +75,28 @@ RSpec.describe 'Incident timeline events', :js do
|
|||
|
||||
context 'when delete is clicked' do
|
||||
before do
|
||||
click_button 'Add new timeline event'
|
||||
fill_in 'Description', with: 'Event note to delete'
|
||||
click_button 'Save'
|
||||
click_button s_('Incident|Add new timeline event')
|
||||
fill_in _('Description'), with: 'Event note to delete'
|
||||
click_button _('Save')
|
||||
end
|
||||
|
||||
it 'shows the confirmation modal and deletes the event' do
|
||||
click_button 'More actions'
|
||||
click_button _('More actions')
|
||||
|
||||
page.within '.gl-new-dropdown-contents' do
|
||||
expect(page).to have_content('Delete')
|
||||
expect(page).to have_content(_('Delete'))
|
||||
page.find('.gl-new-dropdown-item-text-primary', text: 'Delete').click
|
||||
end
|
||||
|
||||
page.within '.modal' do
|
||||
expect(page).to have_content('Delete event')
|
||||
expect(page).to have_content(s_('Incident|Delete event'))
|
||||
end
|
||||
|
||||
click_button 'Delete event'
|
||||
click_button s_('Incident|Delete event')
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_content('No timeline items have been added yet.')
|
||||
expect(page).to have_content(s_('Incident|No timeline items have been added yet.'))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -83,6 +83,8 @@ RSpec.describe 'Project navbar' do
|
|||
end
|
||||
|
||||
context 'when harbor registry is available' do
|
||||
let_it_be(:harbor_integration) { create(:harbor_integration, project: project) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(harbor_registry_integration: true)
|
||||
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
Package: example-package
|
||||
Description: This is an incomplete D-I Packages file
|
|
@ -0,0 +1,2 @@
|
|||
Package: example-package
|
||||
Description: This is an incomplete Sources file
|
|
@ -0,0 +1,107 @@
|
|||
import MockAdapter from 'axios-mock-adapter';
|
||||
import * as harborRegistryApi from '~/api/harbor_registry';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import httpStatus from '~/lib/utils/http_status';
|
||||
|
||||
describe('~/api/harbor_registry', () => {
|
||||
let mock;
|
||||
|
||||
beforeEach(() => {
|
||||
mock = new MockAdapter(axios);
|
||||
jest.spyOn(axios, 'get');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
describe('getHarborRepositoriesList', () => {
|
||||
it('fetches the harbor repositories of the configured harbor project', () => {
|
||||
const requestPath = '/flightjs/Flight/-/harbor/repositories';
|
||||
const expectedUrl = `${requestPath}.json`;
|
||||
const expectedParams = {
|
||||
limit: 10,
|
||||
page: 1,
|
||||
sort: 'update_time desc',
|
||||
requestPath,
|
||||
};
|
||||
const expectResponse = [
|
||||
{
|
||||
harbor_id: 1,
|
||||
name: 'test-project/image-1',
|
||||
artifact_count: 1,
|
||||
creation_time: '2022-07-16T08:20:34.851Z',
|
||||
update_time: '2022-07-16T08:20:34.851Z',
|
||||
harbor_project_id: 2,
|
||||
pull_count: 0,
|
||||
location: 'http://demo.harbor.com/harbor/projects/2/repositories/image-1',
|
||||
},
|
||||
];
|
||||
mock.onGet(expectedUrl).reply(httpStatus.OK, expectResponse);
|
||||
|
||||
return harborRegistryApi.getHarborRepositoriesList(expectedParams).then(({ data }) => {
|
||||
expect(data).toEqual(expectResponse);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getHarborArtifacts', () => {
|
||||
it('fetches the artifacts of a particular harbor repository', () => {
|
||||
const requestPath = '/flightjs/Flight/-/harbor/repositories';
|
||||
const repoName = 'image-1';
|
||||
const expectedUrl = `${requestPath}/${repoName}/artifacts.json`;
|
||||
const expectedParams = {
|
||||
limit: 10,
|
||||
page: 1,
|
||||
sort: 'name asc',
|
||||
repoName,
|
||||
requestPath,
|
||||
};
|
||||
const expectResponse = [
|
||||
{
|
||||
harbor_id: 1,
|
||||
digest: 'sha256:dcdf379c574e1773d703f0c0d56d67594e7a91d6b84d11ff46799f60fb081c52',
|
||||
size: 775241,
|
||||
push_time: '2022-07-16T08:20:34.867Z',
|
||||
tags: ['v2', 'v1', 'latest'],
|
||||
},
|
||||
];
|
||||
mock.onGet(expectedUrl).reply(httpStatus.OK, expectResponse);
|
||||
|
||||
return harborRegistryApi.getHarborArtifacts(expectedParams).then(({ data }) => {
|
||||
expect(data).toEqual(expectResponse);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getHarborTags', () => {
|
||||
it('fetches the tags of a particular artifact', () => {
|
||||
const requestPath = '/flightjs/Flight/-/harbor/repositories';
|
||||
const repoName = 'image-1';
|
||||
const digest = 'sha256:5d98daa36cdc8d6c7ed6579ce17230f0f9fd893a9012fc069cb7d714c0e3df35';
|
||||
const expectedUrl = `${requestPath}/${repoName}/artifacts/${digest}/tags.json`;
|
||||
const expectedParams = {
|
||||
requestPath,
|
||||
digest,
|
||||
repoName,
|
||||
};
|
||||
const expectResponse = [
|
||||
{
|
||||
repositoryId: 4,
|
||||
artifactId: 5,
|
||||
id: 4,
|
||||
name: 'latest',
|
||||
pullTime: '0001-01-01T00:00:00.000Z',
|
||||
pushTime: '2022-05-27T18:21:27.903Z',
|
||||
signed: false,
|
||||
immutable: false,
|
||||
},
|
||||
];
|
||||
mock.onGet(expectedUrl).reply(httpStatus.OK, expectResponse);
|
||||
|
||||
return harborRegistryApi.getHarborTags(expectedParams).then(({ data }) => {
|
||||
expect(data).toEqual(expectResponse);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -36,7 +36,7 @@ describe('Flash', () => {
|
|||
hideFlash(el, false);
|
||||
|
||||
expect(el.style.opacity).toBe('');
|
||||
expect(el.style.transition).toBeFalsy();
|
||||
expect(el.style.transition).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('removes element after transitionend', () => {
|
||||
|
|
|
@ -76,7 +76,7 @@ describe('IDE clientside preview navigator', () => {
|
|||
listenHandler({ type: 'urlchange', url: `${TEST_HOST}/url1` });
|
||||
await nextTick();
|
||||
findBackButton().trigger('click');
|
||||
expect(findBackButton().attributes('disabled')).toBeFalsy();
|
||||
expect(findBackButton().attributes()).not.toHaveProperty('disabled');
|
||||
});
|
||||
|
||||
it('is disabled when there is no previous entry', async () => {
|
||||
|
@ -117,7 +117,7 @@ describe('IDE clientside preview navigator', () => {
|
|||
findBackButton().trigger('click');
|
||||
|
||||
await nextTick();
|
||||
expect(findForwardButton().attributes('disabled')).toBeFalsy();
|
||||
expect(findForwardButton().attributes()).not.toHaveProperty('disabled');
|
||||
});
|
||||
|
||||
it('is disabled when there is no next entry', async () => {
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import VueApollo from 'vue-apollo';
|
||||
import Vue from 'vue';
|
||||
import { GlDatepicker } from '@gitlab/ui';
|
||||
import { __, s__ } from '~/locale';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import CreateTimelineEvent from '~/issues/show/components/incidents/create_timeline_event.vue';
|
||||
import TimelineEventsForm from '~/issues/show/components/incidents/timeline_events_form.vue';
|
||||
import createTimelineEventMutation from '~/issues/show/components/incidents/graphql/queries/create_timeline_event.mutation.graphql';
|
||||
import getTimelineEvents from '~/issues/show/components/incidents/graphql/queries/get_timeline_events.query.graphql';
|
||||
import { timelineFormI18n } from '~/issues/show/components/incidents/constants';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import { createAlert } from '~/flash';
|
||||
import { useFakeDate } from 'helpers/fake_date';
|
||||
|
@ -35,10 +35,9 @@ describe('Create Timeline events', () => {
|
|||
let responseSpy;
|
||||
let apolloProvider;
|
||||
|
||||
const findSubmitButton = () => wrapper.findByText(__('Save'));
|
||||
const findSubmitAndAddButton = () =>
|
||||
wrapper.findByText(s__('Incident|Save and add another event'));
|
||||
const findCancelButton = () => wrapper.findByText(__('Cancel'));
|
||||
const findSubmitButton = () => wrapper.findByText(timelineFormI18n.save);
|
||||
const findSubmitAndAddButton = () => wrapper.findByText(timelineFormI18n.saveAndAdd);
|
||||
const findCancelButton = () => wrapper.findByText(timelineFormI18n.cancel);
|
||||
const findDatePicker = () => wrapper.findComponent(GlDatepicker);
|
||||
const findNoteInput = () => wrapper.findByTestId('input-note');
|
||||
const setNoteInput = () => {
|
||||
|
|
|
@ -4,6 +4,7 @@ import { GlDatepicker } from '@gitlab/ui';
|
|||
import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import TimelineEventsForm from '~/issues/show/components/incidents/timeline_events_form.vue';
|
||||
import { timelineFormI18n } from '~/issues/show/components/incidents/constants';
|
||||
import { createAlert } from '~/flash';
|
||||
import { useFakeDate } from 'helpers/fake_date';
|
||||
|
||||
|
@ -34,9 +35,9 @@ describe('Timeline events form', () => {
|
|||
wrapper.destroy();
|
||||
});
|
||||
|
||||
const findSubmitButton = () => wrapper.findByText('Save');
|
||||
const findSubmitAndAddButton = () => wrapper.findByText('Save and add another event');
|
||||
const findCancelButton = () => wrapper.findByText('Cancel');
|
||||
const findSubmitButton = () => wrapper.findByText(timelineFormI18n.save);
|
||||
const findSubmitAndAddButton = () => wrapper.findByText(timelineFormI18n.saveAndAdd);
|
||||
const findCancelButton = () => wrapper.findByText(timelineFormI18n.cancel);
|
||||
const findDatePicker = () => wrapper.findComponent(GlDatepicker);
|
||||
const findHourInput = () => wrapper.findByTestId('input-hours');
|
||||
const findMinuteInput = () => wrapper.findByTestId('input-minutes');
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import timezoneMock from 'timezone-mock';
|
||||
import { GlIcon, GlDropdown } from '@gitlab/ui';
|
||||
import { nextTick } from 'vue';
|
||||
import { timelineItemI18n } from '~/issues/show/components/incidents/constants';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import IncidentTimelineEventItem from '~/issues/show/components/incidents/timeline_events_item.vue';
|
||||
import { mockEvents } from './mock_data';
|
||||
|
@ -27,7 +28,7 @@ describe('IncidentTimelineEventList', () => {
|
|||
const findCommentIcon = () => wrapper.findComponent(GlIcon);
|
||||
const findEventTime = () => wrapper.findByTestId('event-time');
|
||||
const findDropdown = () => wrapper.findComponent(GlDropdown);
|
||||
const findDeleteButton = () => wrapper.findByText('Delete');
|
||||
const findDeleteButton = () => wrapper.findByText(timelineItemI18n.delete);
|
||||
|
||||
describe('template', () => {
|
||||
it('shows comment icon', () => {
|
||||
|
|
|
@ -0,0 +1,143 @@
|
|||
import { GlSprintf } from '@gitlab/ui';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import { n__ } from '~/locale';
|
||||
import ArtifactsListRow from '~/packages_and_registries/harbor_registry/components/details/artifacts_list_row.vue';
|
||||
import RealListItem from '~/vue_shared/components/registry/list_item.vue';
|
||||
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
import { numberToHumanSize } from '~/lib/utils/number_utils';
|
||||
import { harborArtifactsList, defaultConfig } from '../../mock_data';
|
||||
|
||||
describe('Harbor artifact list row', () => {
|
||||
let wrapper;
|
||||
|
||||
const ListItem = {
|
||||
...RealListItem,
|
||||
data() {
|
||||
return {
|
||||
detailsSlots: [],
|
||||
isDetailsShown: true,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const RouterLinkStub = {
|
||||
props: {
|
||||
to: {
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement('a', {}, this.$slots.default);
|
||||
},
|
||||
};
|
||||
|
||||
const findListItem = () => wrapper.findComponent(ListItem);
|
||||
const findClipboardButton = () => wrapper.findAllComponents(ClipboardButton);
|
||||
const findTimeAgoTooltip = () => wrapper.findComponent(TimeAgoTooltip);
|
||||
const findByTestId = (testId) => wrapper.findByTestId(testId);
|
||||
|
||||
const $route = {
|
||||
params: {
|
||||
project: defaultConfig.harborIntegrationProjectName,
|
||||
image: 'test-repository',
|
||||
},
|
||||
};
|
||||
|
||||
const mountComponent = ({ propsData, config = defaultConfig }) => {
|
||||
wrapper = shallowMountExtended(ArtifactsListRow, {
|
||||
stubs: {
|
||||
GlSprintf,
|
||||
ListItem,
|
||||
'router-link': RouterLinkStub,
|
||||
},
|
||||
mocks: {
|
||||
$route,
|
||||
},
|
||||
propsData,
|
||||
provide() {
|
||||
return {
|
||||
...config,
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('list item', () => {
|
||||
beforeEach(() => {
|
||||
mountComponent({
|
||||
propsData: {
|
||||
artifact: harborArtifactsList[0],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('exists', () => {
|
||||
expect(findListItem().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('has the correct artifact name', () => {
|
||||
expect(findByTestId('name').text()).toBe(harborArtifactsList[0].digest);
|
||||
});
|
||||
|
||||
it('has the correct tags count', () => {
|
||||
const tagsCount = harborArtifactsList[0].tags.length;
|
||||
expect(findByTestId('tags-count').text()).toBe(n__('%d tag', '%d tags', tagsCount));
|
||||
});
|
||||
|
||||
it('has correct digest', () => {
|
||||
expect(findByTestId('digest').text()).toBe('Digest: 5d98daa');
|
||||
});
|
||||
describe('time', () => {
|
||||
it('has the correct push time', () => {
|
||||
expect(findByTestId('time').text()).toBe('Published');
|
||||
expect(findTimeAgoTooltip().attributes()).toMatchObject({
|
||||
time: harborArtifactsList[0].pushTime,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('clipboard button', () => {
|
||||
it('exists', () => {
|
||||
expect(findClipboardButton()).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('has the correct props', () => {
|
||||
expect(findClipboardButton().at(0).attributes()).toMatchObject({
|
||||
text: `docker pull demo.harbor.com/test-project/test-repository@${harborArtifactsList[0].digest}`,
|
||||
title: `docker pull demo.harbor.com/test-project/test-repository@${harborArtifactsList[0].digest}`,
|
||||
});
|
||||
|
||||
expect(findClipboardButton().at(1).attributes()).toMatchObject({
|
||||
text: harborArtifactsList[0].digest,
|
||||
title: harborArtifactsList[0].digest,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('size', () => {
|
||||
it('calculated correctly', () => {
|
||||
expect(findByTestId('size').text()).toBe(
|
||||
numberToHumanSize(Number(harborArtifactsList[0].size)),
|
||||
);
|
||||
});
|
||||
|
||||
it('when size is missing', () => {
|
||||
const artifactInfo = harborArtifactsList[0];
|
||||
artifactInfo.size = null;
|
||||
|
||||
mountComponent({
|
||||
propsData: {
|
||||
artifact: artifactInfo,
|
||||
},
|
||||
});
|
||||
|
||||
expect(findByTestId('size').text()).toBe('0 bytes');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,75 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { GlEmptyState } from '@gitlab/ui';
|
||||
import TagsLoader from '~/packages_and_registries/shared/components/tags_loader.vue';
|
||||
import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
|
||||
import ArtifactsList from '~/packages_and_registries/harbor_registry/components/details/artifacts_list.vue';
|
||||
import ArtifactsListRow from '~/packages_and_registries/harbor_registry/components/details/artifacts_list_row.vue';
|
||||
import { defaultConfig, harborArtifactsList } from '../../mock_data';
|
||||
|
||||
describe('Harbor artifacts list', () => {
|
||||
let wrapper;
|
||||
|
||||
const findTagsLoader = () => wrapper.find(TagsLoader);
|
||||
const findGlEmptyState = () => wrapper.find(GlEmptyState);
|
||||
const findRegistryList = () => wrapper.find(RegistryList);
|
||||
const findArtifactsListRow = () => wrapper.findAllComponents(ArtifactsListRow);
|
||||
|
||||
const mountComponent = ({ propsData, config = defaultConfig }) => {
|
||||
wrapper = shallowMount(ArtifactsList, {
|
||||
propsData,
|
||||
stubs: { RegistryList },
|
||||
provide() {
|
||||
return {
|
||||
...config,
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('when isLoading is true', () => {
|
||||
beforeEach(() => {
|
||||
mountComponent({
|
||||
propsData: {
|
||||
isLoading: true,
|
||||
pageInfo: {},
|
||||
filter: '',
|
||||
artifacts: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('show the loader', () => {
|
||||
expect(findTagsLoader().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not show the list', () => {
|
||||
expect(findGlEmptyState().exists()).toBe(false);
|
||||
expect(findRegistryList().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('registry list', () => {
|
||||
beforeEach(() => {
|
||||
mountComponent({
|
||||
propsData: {
|
||||
isLoading: false,
|
||||
pageInfo: {},
|
||||
filter: '',
|
||||
artifacts: harborArtifactsList,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('exists', () => {
|
||||
expect(findRegistryList().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('one artifact row exist', () => {
|
||||
expect(findArtifactsListRow()).toHaveLength(harborArtifactsList.length);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,85 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { nextTick } from 'vue';
|
||||
import DetailsHeader from '~/packages_and_registries/harbor_registry/components/details/details_header.vue';
|
||||
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
|
||||
import { ROOT_IMAGE_TEXT } from '~/packages_and_registries/harbor_registry/constants/index';
|
||||
|
||||
describe('Harbor Details Header', () => {
|
||||
let wrapper;
|
||||
|
||||
const findByTestId = (testId) => wrapper.find(`[data-testid="${testId}"]`);
|
||||
const findTitle = () => findByTestId('title');
|
||||
const findArtifactsCount = () => findByTestId('artifacts-count');
|
||||
|
||||
const mountComponent = ({ propsData }) => {
|
||||
wrapper = shallowMount(DetailsHeader, {
|
||||
propsData,
|
||||
stubs: {
|
||||
TitleArea,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('artifact name', () => {
|
||||
describe('missing image name', () => {
|
||||
beforeEach(() => {
|
||||
mountComponent({ propsData: { imagesDetail: { name: '', artifactCount: 1 } } });
|
||||
});
|
||||
|
||||
it('root image ', () => {
|
||||
expect(findTitle().text()).toBe(ROOT_IMAGE_TEXT);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with artifact name present', () => {
|
||||
beforeEach(() => {
|
||||
mountComponent({ propsData: { imagesDetail: { name: 'shao/flinkx', artifactCount: 1 } } });
|
||||
});
|
||||
|
||||
it('shows artifact.name ', () => {
|
||||
expect(findTitle().text()).toContain('shao/flinkx');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('metadata items', () => {
|
||||
describe('artifacts count', () => {
|
||||
it('displays "-- artifacts" while loading', async () => {
|
||||
mountComponent({ propsData: { imagesDetail: {} } });
|
||||
await nextTick();
|
||||
|
||||
expect(findArtifactsCount().props('text')).toBe('-- artifacts');
|
||||
});
|
||||
|
||||
it('when there is more than one artifact has the correct text', async () => {
|
||||
mountComponent({ propsData: { imagesDetail: { name: 'shao/flinkx', artifactCount: 10 } } });
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(findArtifactsCount().props('text')).toBe('10 artifacts');
|
||||
});
|
||||
|
||||
it('when there is one artifact has the correct text', async () => {
|
||||
mountComponent({
|
||||
propsData: { imagesDetail: { name: 'shao/flinkx', artifactCount: 1 } },
|
||||
});
|
||||
await nextTick();
|
||||
|
||||
expect(findArtifactsCount().props('text')).toBe('1 artifact');
|
||||
});
|
||||
|
||||
it('has the correct icon', async () => {
|
||||
mountComponent({
|
||||
propsData: { imagesDetail: { name: 'shao/flinkx', artifactCount: 1 } },
|
||||
});
|
||||
await nextTick();
|
||||
|
||||
expect(findArtifactsCount().props('icon')).toBe('package');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,25 +1,24 @@
|
|||
import { shallowMount, RouterLinkStub as RouterLink } from '@vue/test-utils';
|
||||
import { GlIcon, GlSprintf, GlSkeletonLoader } from '@gitlab/ui';
|
||||
import { GlIcon, GlSkeletonLoader } from '@gitlab/ui';
|
||||
|
||||
import HarborListRow from '~/packages_and_registries/harbor_registry/components/list/harbor_list_row.vue';
|
||||
import ListItem from '~/vue_shared/components/registry/list_item.vue';
|
||||
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||
import { harborListResponse } from '../../mock_data';
|
||||
import { harborImagesList } from '../../mock_data';
|
||||
|
||||
describe('Harbor List Row', () => {
|
||||
let wrapper;
|
||||
const [item] = harborListResponse.repositories;
|
||||
const item = harborImagesList[0];
|
||||
|
||||
const findDetailsLink = () => wrapper.find(RouterLink);
|
||||
const findClipboardButton = () => wrapper.findComponent(ClipboardButton);
|
||||
const findTagsCount = () => wrapper.find('[data-testid="tags-count"]');
|
||||
const findArtifactsCount = () => wrapper.find('[data-testid="artifacts-count"]');
|
||||
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
|
||||
|
||||
const mountComponent = (props) => {
|
||||
wrapper = shallowMount(HarborListRow, {
|
||||
stubs: {
|
||||
RouterLink,
|
||||
GlSprintf,
|
||||
ListItem,
|
||||
},
|
||||
propsData: {
|
||||
|
@ -42,7 +41,8 @@ describe('Harbor List Row', () => {
|
|||
expect(findDetailsLink().props('to')).toMatchObject({
|
||||
name: 'details',
|
||||
params: {
|
||||
id: item.id,
|
||||
image: 'nginx',
|
||||
project: 'nginx',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -56,17 +56,17 @@ describe('Harbor List Row', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('tags count', () => {
|
||||
describe('artifacts count', () => {
|
||||
it('exists', () => {
|
||||
mountComponent();
|
||||
expect(findTagsCount().exists()).toBe(true);
|
||||
expect(findArtifactsCount().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('contains a tag icon', () => {
|
||||
it('contains a package icon', () => {
|
||||
mountComponent();
|
||||
const icon = findTagsCount().find(GlIcon);
|
||||
const icon = findArtifactsCount().find(GlIcon);
|
||||
expect(icon.exists()).toBe(true);
|
||||
expect(icon.props('name')).toBe('tag');
|
||||
expect(icon.props('name')).toBe('package');
|
||||
});
|
||||
|
||||
describe('loading state', () => {
|
||||
|
@ -76,23 +76,23 @@ describe('Harbor List Row', () => {
|
|||
expect(findSkeletonLoader().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('hides the tags count while loading', () => {
|
||||
it('hides the artifacts count while loading', () => {
|
||||
mountComponent({ metadataLoading: true });
|
||||
|
||||
expect(findTagsCount().exists()).toBe(false);
|
||||
expect(findArtifactsCount().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('tags count text', () => {
|
||||
it('with one tag in the image', () => {
|
||||
describe('artifacts count text', () => {
|
||||
it('with one artifact in the image', () => {
|
||||
mountComponent({ item: { ...item, artifactCount: 1 } });
|
||||
|
||||
expect(findTagsCount().text()).toMatchInterpolatedText('1 Tag');
|
||||
expect(findArtifactsCount().text()).toMatchInterpolatedText('1 artifact');
|
||||
});
|
||||
it('with more than one tag in the image', () => {
|
||||
it('with more than one artifact in the image', () => {
|
||||
mountComponent({ item: { ...item, artifactCount: 3 } });
|
||||
|
||||
expect(findTagsCount().text()).toMatchInterpolatedText('3 Tags');
|
||||
expect(findArtifactsCount().text()).toMatchInterpolatedText('3 artifacts');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,7 +2,7 @@ import { shallowMount } from '@vue/test-utils';
|
|||
import HarborList from '~/packages_and_registries/harbor_registry/components/list/harbor_list.vue';
|
||||
import HarborListRow from '~/packages_and_registries/harbor_registry/components/list/harbor_list_row.vue';
|
||||
import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
|
||||
import { harborListResponse } from '../../mock_data';
|
||||
import { harborImagesList } from '../../mock_data';
|
||||
|
||||
describe('Harbor List', () => {
|
||||
let wrapper;
|
||||
|
@ -13,8 +13,8 @@ describe('Harbor List', () => {
|
|||
wrapper = shallowMount(HarborList, {
|
||||
stubs: { RegistryList },
|
||||
propsData: {
|
||||
images: harborListResponse.repositories,
|
||||
pageInfo: harborListResponse.pageInfo,
|
||||
images: harborImagesList,
|
||||
pageInfo: {},
|
||||
...props,
|
||||
},
|
||||
});
|
||||
|
@ -28,7 +28,7 @@ describe('Harbor List', () => {
|
|||
it('contains one list element for each image', () => {
|
||||
mountComponent();
|
||||
|
||||
expect(findHarborListRow().length).toBe(harborListResponse.repositories.length);
|
||||
expect(findHarborListRow().length).toBe(harborImagesList.length);
|
||||
});
|
||||
|
||||
it('passes down the metadataLoading prop', () => {
|
||||
|
|
|
@ -1,173 +1,105 @@
|
|||
export const harborListResponse = {
|
||||
repositories: [
|
||||
{
|
||||
artifactCount: 1,
|
||||
creationTime: '2022-03-02T06:35:53.205Z',
|
||||
id: 25,
|
||||
name: 'shao/flinkx',
|
||||
projectId: 21,
|
||||
pullCount: 0,
|
||||
updateTime: '2022-03-02T06:35:53.205Z',
|
||||
location: 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas',
|
||||
},
|
||||
{
|
||||
artifactCount: 1,
|
||||
creationTime: '2022-03-02T06:35:53.205Z',
|
||||
id: 26,
|
||||
name: 'shao/flinkx1',
|
||||
projectId: 21,
|
||||
pullCount: 0,
|
||||
updateTime: '2022-03-02T06:35:53.205Z',
|
||||
location: 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas',
|
||||
},
|
||||
{
|
||||
artifactCount: 1,
|
||||
creationTime: '2022-03-02T06:35:53.205Z',
|
||||
id: 27,
|
||||
name: 'shao/flinkx2',
|
||||
projectId: 21,
|
||||
pullCount: 0,
|
||||
updateTime: '2022-03-02T06:35:53.205Z',
|
||||
location: 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas',
|
||||
},
|
||||
],
|
||||
totalCount: 3,
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
},
|
||||
export const harborImageDetailEmptyResponse = {
|
||||
data: null,
|
||||
};
|
||||
|
||||
export const harborTagsResponse = {
|
||||
tags: [
|
||||
{
|
||||
digest: 'sha256:7f386a1844faf341353e1c20f2f39f11f397604fedc475435d13f756eeb235d1',
|
||||
location:
|
||||
'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:02310e655103823920157bc4410ea361dc638bc2cda59667d2cb1f2a988e264c',
|
||||
path:
|
||||
'gitlab-org/gitlab/gitlab-ee-qa/cache:02310e655103823920157bc4410ea361dc638bc2cda59667d2cb1f2a988e264c',
|
||||
name: '02310e655103823920157bc4410ea361dc638bc2cda59667d2cb1f2a988e264c',
|
||||
revision: 'f53bde3d44699e04e11cf15fb415046a0913e2623d878d89bc21adb2cbda5255',
|
||||
shortRevision: 'f53bde3d4',
|
||||
createdAt: '2022-03-02T23:59:05+00:00',
|
||||
totalSize: '6623124',
|
||||
},
|
||||
{
|
||||
digest: 'sha256:4554416b84c4568fe93086620b637064ed029737aabe7308b96d50e3d9d92ed7',
|
||||
location:
|
||||
'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:02deb4dddf177212b50e883d5e4f6c03731fad1a18cd27261736cd9dbba79160',
|
||||
path:
|
||||
'gitlab-org/gitlab/gitlab-ee-qa/cache:02deb4dddf177212b50e883d5e4f6c03731fad1a18cd27261736cd9dbba79160',
|
||||
name: '02deb4dddf177212b50e883d5e4f6c03731fad1a18cd27261736cd9dbba79160',
|
||||
revision: 'e1fe52d8bab66d71bd54a6b8784d3b9edbc68adbd6ea87f5fa44d9974144ef9e',
|
||||
shortRevision: 'e1fe52d8b',
|
||||
createdAt: '2022-02-10T01:09:56+00:00',
|
||||
totalSize: '920760',
|
||||
},
|
||||
{
|
||||
digest: 'sha256:14f37b60e52b9ce0e9f8f7094b311d265384798592f783487c30aaa3d58e6345',
|
||||
location:
|
||||
'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:03bc5971bab1e849ba52a20a31e7273053f22b2ddb1d04bd6b77d53a2635727a',
|
||||
path:
|
||||
'gitlab-org/gitlab/gitlab-ee-qa/cache:03bc5971bab1e849ba52a20a31e7273053f22b2ddb1d04bd6b77d53a2635727a',
|
||||
name: '03bc5971bab1e849ba52a20a31e7273053f22b2ddb1d04bd6b77d53a2635727a',
|
||||
revision: 'c72770c6eb93c421bc496964b4bffc742b1ec2e642cdab876be7afda1856029f',
|
||||
shortRevision: 'c72770c6e',
|
||||
createdAt: '2021-12-22T04:48:48+00:00',
|
||||
totalSize: '48609053',
|
||||
},
|
||||
{
|
||||
digest: 'sha256:e925e3b8277ea23f387ed5fba5e78280cfac7cfb261a78cf046becf7b6a3faae',
|
||||
location:
|
||||
'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:03f495bc5714bff78bb14293320d336afdf47fd47ddff0c3c5f09f8da86d5d19',
|
||||
path:
|
||||
'gitlab-org/gitlab/gitlab-ee-qa/cache:03f495bc5714bff78bb14293320d336afdf47fd47ddff0c3c5f09f8da86d5d19',
|
||||
name: '03f495bc5714bff78bb14293320d336afdf47fd47ddff0c3c5f09f8da86d5d19',
|
||||
revision: '1ac2a43194f4e15166abdf3f26e6ec92215240490b9cac834d63de1a3d87494a',
|
||||
shortRevision: '1ac2a4319',
|
||||
createdAt: '2022-03-09T11:02:27+00:00',
|
||||
totalSize: '35141894',
|
||||
},
|
||||
{
|
||||
digest: 'sha256:7d8303fd5c077787a8c879f8f66b69e2b5605f48ccd3f286e236fb0749fcc1ca',
|
||||
location:
|
||||
'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:05a4e58231e54b70aab2d6f22ba4fbe10e48aa4ddcbfef11c5662241c2ae4fda',
|
||||
path:
|
||||
'gitlab-org/gitlab/gitlab-ee-qa/cache:05a4e58231e54b70aab2d6f22ba4fbe10e48aa4ddcbfef11c5662241c2ae4fda',
|
||||
name: '05a4e58231e54b70aab2d6f22ba4fbe10e48aa4ddcbfef11c5662241c2ae4fda',
|
||||
revision: 'cf8fee086701016e1a84e6824f0c896951fef4cce9d4745459558b87eec3232c',
|
||||
shortRevision: 'cf8fee086',
|
||||
createdAt: '2022-01-21T11:31:43+00:00',
|
||||
totalSize: '48716070',
|
||||
},
|
||||
{
|
||||
digest: 'sha256:b33611cefe20e4a41a6e0dce356a5d7ef3c177ea7536a58652f5b3a4f2f83549',
|
||||
location:
|
||||
'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:093d2746876997723541aec8b88687a4cdb3b5bbb0279c5089b7891317741a9a',
|
||||
path:
|
||||
'gitlab-org/gitlab/gitlab-ee-qa/cache:093d2746876997723541aec8b88687a4cdb3b5bbb0279c5089b7891317741a9a',
|
||||
name: '093d2746876997723541aec8b88687a4cdb3b5bbb0279c5089b7891317741a9a',
|
||||
revision: '1a4b48198b13d55242c5164e64d41c4e9f75b5d9506bc6e0efc1534dd0dd1f15',
|
||||
shortRevision: '1a4b48198',
|
||||
createdAt: '2022-01-21T11:31:51+00:00',
|
||||
totalSize: '6623127',
|
||||
},
|
||||
{
|
||||
digest: 'sha256:d25c3c020e2dbd4711a67b9fe308f4cbb7b0bb21815e722a02f91c570dc5d519',
|
||||
location:
|
||||
'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:09698b3fae81dfd6e02554dbc82930f304a6356c8f541c80e8598a42aed985f7',
|
||||
path:
|
||||
'gitlab-org/gitlab/gitlab-ee-qa/cache:09698b3fae81dfd6e02554dbc82930f304a6356c8f541c80e8598a42aed985f7',
|
||||
name: '09698b3fae81dfd6e02554dbc82930f304a6356c8f541c80e8598a42aed985f7',
|
||||
revision: '03e2e2777dde01c30469ee8c710973dd08a7a4f70494d7dc1583c24b525d7f61',
|
||||
shortRevision: '03e2e2777',
|
||||
createdAt: '2022-03-02T23:58:20+00:00',
|
||||
totalSize: '911377',
|
||||
},
|
||||
{
|
||||
digest: 'sha256:fb760e4d2184e9e8e39d6917534d4610fe01009734698a5653b2de1391ba28f4',
|
||||
location:
|
||||
'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:09b830c3eaf80d547f3b523d8e242a2c411085c349dab86c520f36c7b7644f95',
|
||||
path:
|
||||
'gitlab-org/gitlab/gitlab-ee-qa/cache:09b830c3eaf80d547f3b523d8e242a2c411085c349dab86c520f36c7b7644f95',
|
||||
name: '09b830c3eaf80d547f3b523d8e242a2c411085c349dab86c520f36c7b7644f95',
|
||||
revision: '350e78d60646bf6967244448c6aaa14d21ecb9a0c6cf87e9ff0361cbe59b9012',
|
||||
shortRevision: '350e78d60',
|
||||
createdAt: '2022-01-19T13:49:14+00:00',
|
||||
totalSize: '48710241',
|
||||
},
|
||||
{
|
||||
digest: 'sha256:407250f380cea92729cbc038c420e74900f53b852e11edc6404fe75a0fd2c402',
|
||||
location:
|
||||
'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:0d03504a17b467eafc8c96bde70af26c74bd459a32b7eb2dd189dd6b3c121557',
|
||||
path:
|
||||
'gitlab-org/gitlab/gitlab-ee-qa/cache:0d03504a17b467eafc8c96bde70af26c74bd459a32b7eb2dd189dd6b3c121557',
|
||||
name: '0d03504a17b467eafc8c96bde70af26c74bd459a32b7eb2dd189dd6b3c121557',
|
||||
revision: '76038370b7f3904364891457c4a6a234897255e6b9f45d0a852bf3a7e5257e18',
|
||||
shortRevision: '76038370b',
|
||||
createdAt: '2022-01-24T12:56:22+00:00',
|
||||
totalSize: '280065',
|
||||
},
|
||||
{
|
||||
digest: 'sha256:ada87f25218542951ce6720c27f3d0758e90c2540bd129f5cfb9e15b31e07b07',
|
||||
location:
|
||||
'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:0eb20a4a7cac2ebea821d420b3279654fe550fd8502f1785c1927aa84e5949eb',
|
||||
path:
|
||||
'gitlab-org/gitlab/gitlab-ee-qa/cache:0eb20a4a7cac2ebea821d420b3279654fe550fd8502f1785c1927aa84e5949eb',
|
||||
name: '0eb20a4a7cac2ebea821d420b3279654fe550fd8502f1785c1927aa84e5949eb',
|
||||
revision: '3d4b49a7bbb36c48bb721f4d0e76e7950bec3878ee29cdfdd6da39f575d6d37f',
|
||||
shortRevision: '3d4b49a7b',
|
||||
createdAt: '2022-02-17T17:37:52+00:00',
|
||||
totalSize: '48655767',
|
||||
},
|
||||
],
|
||||
totalCount: 100,
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
},
|
||||
export const harborImageDetailResponse = {
|
||||
artifactCount: 10,
|
||||
creationTime: '2022-03-02T06:35:53.205Z',
|
||||
id: 25,
|
||||
name: 'shao/flinkx',
|
||||
projectId: 21,
|
||||
pullCount: 0,
|
||||
updateTime: '2022-03-02T06:35:53.205Z',
|
||||
location: 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas',
|
||||
};
|
||||
|
||||
export const harborArtifactsResponse = [
|
||||
{
|
||||
id: 1,
|
||||
digest: 'sha256:5d98daa36cdc8d6c7ed6579ce17230f0f9fd893a9012fc069cb7d714c0e3df35',
|
||||
size: 773928,
|
||||
push_time: '2022-05-19T15:54:47.821Z',
|
||||
tags: ['latest'],
|
||||
},
|
||||
];
|
||||
|
||||
export const harborArtifactsList = [
|
||||
{
|
||||
id: 1,
|
||||
digest: 'sha256:5d98daa36cdc8d6c7ed6579ce17230f0f9fd893a9012fc069cb7d714c0e3df35',
|
||||
size: 773928,
|
||||
pushTime: '2022-05-19T15:54:47.821Z',
|
||||
tags: ['latest'],
|
||||
},
|
||||
];
|
||||
|
||||
export const harborTagsResponse = [
|
||||
{
|
||||
repository_id: 4,
|
||||
artifact_id: 5,
|
||||
id: 4,
|
||||
name: 'latest',
|
||||
pull_time: '0001-01-01T00:00:00.000Z',
|
||||
push_time: '2022-05-27T18:21:27.903Z',
|
||||
signed: false,
|
||||
immutable: false,
|
||||
},
|
||||
];
|
||||
|
||||
export const harborTagsList = [
|
||||
{
|
||||
repositoryId: 4,
|
||||
artifactId: 5,
|
||||
id: 4,
|
||||
name: 'latest',
|
||||
pullTime: '0001-01-01T00:00:00.000Z',
|
||||
pushTime: '2022-05-27T18:21:27.903Z',
|
||||
signed: false,
|
||||
immutable: false,
|
||||
},
|
||||
];
|
||||
|
||||
export const defaultConfig = {
|
||||
noContainersImage: 'noContainersImage',
|
||||
repositoryUrl: 'demo.harbor.com',
|
||||
harborIntegrationProjectName: 'test-project',
|
||||
projectName: 'Flight',
|
||||
endpoint: '/flightjs/Flight/-/harbor/repositories',
|
||||
connectionError: false,
|
||||
invalidPathError: false,
|
||||
isGroupPage: false,
|
||||
helpPagePath: '',
|
||||
containersErrorImage: 'containersErrorImage',
|
||||
};
|
||||
|
||||
export const defaultFullPath = 'flightjs/Flight';
|
||||
|
||||
export const harborImagesResponse = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'nginx/nginx',
|
||||
artifact_count: 1,
|
||||
creation_time: '2022-05-29T10:07:16.812Z',
|
||||
update_time: '2022-05-29T10:07:16.812Z',
|
||||
project_id: 4,
|
||||
pull_count: 0,
|
||||
location: 'https://demo.goharbor.io/harbor/projects/4/repositories/nginx',
|
||||
},
|
||||
];
|
||||
|
||||
export const harborImagesList = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'nginx/nginx',
|
||||
artifactCount: 1,
|
||||
creationTime: '2022-05-29T10:07:16.812Z',
|
||||
updateTime: '2022-05-29T10:07:16.812Z',
|
||||
projectId: 4,
|
||||
pullCount: 0,
|
||||
location: 'https://demo.goharbor.io/harbor/projects/4/repositories/nginx',
|
||||
},
|
||||
];
|
||||
|
||||
export const dockerCommands = {
|
||||
dockerBuildCommand: 'foofoo',
|
||||
dockerPushCommand: 'barbar',
|
||||
|
|
|
@ -0,0 +1,162 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { nextTick } from 'vue';
|
||||
import { GlFilteredSearchToken } from '@gitlab/ui';
|
||||
import { s__ } from '~/locale';
|
||||
import HarborDetailsPage from '~/packages_and_registries/harbor_registry/pages/details.vue';
|
||||
import TagsLoader from '~/packages_and_registries/shared/components/tags_loader.vue';
|
||||
import ArtifactsList from '~/packages_and_registries/harbor_registry/components/details/artifacts_list.vue';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import DetailsHeader from '~/packages_and_registries/harbor_registry/components/details/details_header.vue';
|
||||
import PersistedSearch from '~/packages_and_registries/shared/components/persisted_search.vue';
|
||||
import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
|
||||
import {
|
||||
NAME_SORT_FIELD,
|
||||
TOKEN_TYPE_TAG_NAME,
|
||||
} from '~/packages_and_registries/harbor_registry/constants/index';
|
||||
import { harborArtifactsResponse, harborArtifactsList, defaultConfig } from '../mock_data';
|
||||
|
||||
let mockHarborArtifactsResponse;
|
||||
|
||||
jest.mock('~/rest_api', () => ({
|
||||
getHarborArtifacts: () => mockHarborArtifactsResponse,
|
||||
}));
|
||||
|
||||
describe('Harbor Details Page', () => {
|
||||
let wrapper;
|
||||
|
||||
const findTagsLoader = () => wrapper.find(TagsLoader);
|
||||
const findArtifactsList = () => wrapper.find(ArtifactsList);
|
||||
const findDetailsHeader = () => wrapper.find(DetailsHeader);
|
||||
const findPersistedSearch = () => wrapper.find(PersistedSearch);
|
||||
|
||||
const waitForHarborDetailRequest = async () => {
|
||||
await waitForPromises();
|
||||
await nextTick();
|
||||
};
|
||||
|
||||
const $route = {
|
||||
params: {
|
||||
project: 'test-project',
|
||||
image: 'test-repository',
|
||||
},
|
||||
};
|
||||
|
||||
const breadCrumbState = {
|
||||
updateName: jest.fn(),
|
||||
updateHref: jest.fn(),
|
||||
};
|
||||
|
||||
const defaultHeaders = {
|
||||
'x-page': '1',
|
||||
'X-Per-Page': '20',
|
||||
'X-TOTAL': '1',
|
||||
'X-Total-Pages': '1',
|
||||
};
|
||||
|
||||
const mountComponent = ({ config = defaultConfig } = {}) => {
|
||||
wrapper = shallowMount(HarborDetailsPage, {
|
||||
mocks: {
|
||||
$route,
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
breadCrumbState,
|
||||
...config,
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockHarborArtifactsResponse = Promise.resolve({
|
||||
data: harborArtifactsResponse,
|
||||
headers: defaultHeaders,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('when isLoading is true', () => {
|
||||
it('shows the loader', () => {
|
||||
mountComponent();
|
||||
|
||||
expect(findTagsLoader().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not show the list', () => {
|
||||
mountComponent();
|
||||
|
||||
expect(findArtifactsList().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('artifacts list', () => {
|
||||
it('exists', async () => {
|
||||
mountComponent();
|
||||
|
||||
findPersistedSearch().vm.$emit('update', { sort: 'NAME_ASC', filters: [] });
|
||||
await waitForHarborDetailRequest();
|
||||
|
||||
expect(findArtifactsList().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('has the correct props bound', async () => {
|
||||
mountComponent();
|
||||
|
||||
findPersistedSearch().vm.$emit('update', { sort: 'NAME_ASC', filters: [] });
|
||||
await waitForHarborDetailRequest();
|
||||
|
||||
expect(findArtifactsList().props()).toMatchObject({
|
||||
isLoading: false,
|
||||
filter: '',
|
||||
artifacts: harborArtifactsList,
|
||||
pageInfo: {
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
total: 1,
|
||||
totalPages: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('persisted search', () => {
|
||||
it('has the correct props', () => {
|
||||
mountComponent();
|
||||
|
||||
expect(findPersistedSearch().props()).toMatchObject({
|
||||
sortableFields: [NAME_SORT_FIELD],
|
||||
defaultOrder: NAME_SORT_FIELD.orderBy,
|
||||
defaultSort: 'asc',
|
||||
tokens: [
|
||||
{
|
||||
type: TOKEN_TYPE_TAG_NAME,
|
||||
icon: 'tag',
|
||||
title: s__('HarborRegistry|Tag'),
|
||||
unique: true,
|
||||
token: GlFilteredSearchToken,
|
||||
operators: OPERATOR_IS_ONLY,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('header', () => {
|
||||
it('has the correct props', async () => {
|
||||
mountComponent();
|
||||
|
||||
findPersistedSearch().vm.$emit('update', { sort: 'NAME_ASC', filters: [] });
|
||||
await waitForHarborDetailRequest();
|
||||
|
||||
expect(findDetailsHeader().props()).toMatchObject({
|
||||
imagesDetail: {
|
||||
name: 'test-project/test-repository',
|
||||
artifactCount: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -5,15 +5,14 @@ import HarborListHeader from '~/packages_and_registries/harbor_registry/componen
|
|||
import HarborRegistryList from '~/packages_and_registries/harbor_registry/pages/list.vue';
|
||||
import PersistedSearch from '~/packages_and_registries/shared/components/persisted_search.vue';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
// import { harborListResponse } from '~/packages_and_registries/harbor_registry/mock_api.js';
|
||||
import HarborList from '~/packages_and_registries/harbor_registry/components/list/harbor_list.vue';
|
||||
import CliCommands from '~/packages_and_registries/shared/components/cli_commands.vue';
|
||||
import { SORT_FIELDS } from '~/packages_and_registries/harbor_registry/constants/index';
|
||||
import { harborListResponse, dockerCommands } from '../mock_data';
|
||||
import { harborImagesResponse, defaultConfig, harborImagesList } from '../mock_data';
|
||||
|
||||
let mockHarborListResponse;
|
||||
jest.mock('~/packages_and_registries/harbor_registry/mock_api.js', () => ({
|
||||
harborListResponse: () => mockHarborListResponse,
|
||||
jest.mock('~/rest_api', () => ({
|
||||
getHarborRepositoriesList: () => mockHarborListResponse,
|
||||
}));
|
||||
|
||||
describe('Harbor List Page', () => {
|
||||
|
@ -24,34 +23,43 @@ describe('Harbor List Page', () => {
|
|||
await nextTick();
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockHarborListResponse = Promise.resolve(harborListResponse);
|
||||
});
|
||||
|
||||
const findHarborListHeader = () => wrapper.findComponent(HarborListHeader);
|
||||
const findPersistedSearch = () => wrapper.findComponent(PersistedSearch);
|
||||
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
|
||||
const findHarborList = () => wrapper.findComponent(HarborList);
|
||||
const findCliCommands = () => wrapper.findComponent(CliCommands);
|
||||
|
||||
const defaultHeaders = {
|
||||
'x-page': '1',
|
||||
'X-Per-Page': '20',
|
||||
'X-TOTAL': '1',
|
||||
'X-Total-Pages': '1',
|
||||
};
|
||||
|
||||
const fireFirstSortUpdate = () => {
|
||||
findPersistedSearch().vm.$emit('update', { sort: 'UPDATED_DESC', filters: [] });
|
||||
};
|
||||
|
||||
const mountComponent = ({ config = { isGroupPage: false } } = {}) => {
|
||||
const mountComponent = ({ config = defaultConfig } = {}) => {
|
||||
wrapper = shallowMount(HarborRegistryList, {
|
||||
stubs: {
|
||||
HarborListHeader,
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
config,
|
||||
...dockerCommands,
|
||||
...config,
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockHarborListResponse = Promise.resolve({
|
||||
data: harborImagesResponse,
|
||||
headers: defaultHeaders,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
@ -64,7 +72,7 @@ describe('Harbor List Page', () => {
|
|||
|
||||
expect(findHarborListHeader().exists()).toBe(true);
|
||||
expect(findHarborListHeader().props()).toMatchObject({
|
||||
imagesCount: 3,
|
||||
imagesCount: 1,
|
||||
metadataLoading: false,
|
||||
});
|
||||
});
|
||||
|
@ -117,6 +125,16 @@ describe('Harbor List Page', () => {
|
|||
await nextTick();
|
||||
|
||||
expect(findHarborList().exists()).toBe(true);
|
||||
expect(findHarborList().props()).toMatchObject({
|
||||
images: harborImagesList,
|
||||
metadataLoading: false,
|
||||
pageInfo: {
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
total: 1,
|
||||
totalPages: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
import Tracking from '~/tracking';
|
||||
import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
|
||||
|
||||
import { dockerCommands } from '../../mock_data';
|
||||
import { dockerCommands } from 'jest/packages_and_registries/container_registry/explorer/mock_data';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_relative '../../../../lib/gitlab/cluster/lifecycle_events'
|
||||
|
||||
RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, :prometheus do
|
||||
context 'watchdog' do
|
||||
|
@ -8,23 +9,31 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, :prometheus do
|
|||
let(:handler) { instance_double(described_class::NullHandler) }
|
||||
|
||||
let(:heap_frag_limit_gauge) { instance_double(::Prometheus::Client::Gauge) }
|
||||
let(:heap_frag_violations_counter) { instance_double(::Prometheus::Client::Counter) }
|
||||
let(:heap_frag_violations_handled_counter) { instance_double(::Prometheus::Client::Counter) }
|
||||
let(:violations_counter) { instance_double(::Prometheus::Client::Counter) }
|
||||
let(:violations_handled_counter) { instance_double(::Prometheus::Client::Counter) }
|
||||
|
||||
let(:sleep_time) { 0.1 }
|
||||
let(:max_heap_fragmentation) { 0.2 }
|
||||
let(:max_mem_growth) { 2 }
|
||||
|
||||
# Defaults that will not trigger any events.
|
||||
let(:fragmentation) { 0 }
|
||||
let(:worker_memory) { 0 }
|
||||
let(:primary_memory) { 0 }
|
||||
let(:max_strikes) { 0 }
|
||||
|
||||
# Tests should set this to control the number of loop iterations in `call`.
|
||||
let(:watchdog_iterations) { 1 }
|
||||
|
||||
subject(:watchdog) do
|
||||
described_class.new(handler: handler, logger: logger, sleep_time_seconds: sleep_time,
|
||||
max_strikes: max_strikes, max_heap_fragmentation: max_heap_fragmentation).tap do |instance|
|
||||
max_strikes: max_strikes, max_mem_growth: max_mem_growth,
|
||||
max_heap_fragmentation: max_heap_fragmentation).tap do |instance|
|
||||
# We need to defuse `sleep` and stop the internal loop after N iterations.
|
||||
iterations = 0
|
||||
expect(instance).to receive(:sleep) do
|
||||
instance.stop if (iterations += 1) >= watchdog_iterations
|
||||
end.at_most(watchdog_iterations)
|
||||
allow(instance).to receive(:sleep) do
|
||||
instance.stop if (iterations += 1) > watchdog_iterations
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -33,34 +42,35 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, :prometheus do
|
|||
.with(:gitlab_memwd_heap_frag_limit, anything)
|
||||
.and_return(heap_frag_limit_gauge)
|
||||
allow(Gitlab::Metrics).to receive(:counter)
|
||||
.with(:gitlab_memwd_heap_frag_violations_total, anything, anything)
|
||||
.and_return(heap_frag_violations_counter)
|
||||
.with(:gitlab_memwd_violations_total, anything, anything)
|
||||
.and_return(violations_counter)
|
||||
allow(Gitlab::Metrics).to receive(:counter)
|
||||
.with(:gitlab_memwd_heap_frag_violations_handled_total, anything, anything)
|
||||
.and_return(heap_frag_violations_handled_counter)
|
||||
.with(:gitlab_memwd_violations_handled_total, anything, anything)
|
||||
.and_return(violations_handled_counter)
|
||||
|
||||
allow(heap_frag_limit_gauge).to receive(:set)
|
||||
allow(heap_frag_violations_counter).to receive(:increment)
|
||||
allow(heap_frag_violations_handled_counter).to receive(:increment)
|
||||
allow(violations_counter).to receive(:increment)
|
||||
allow(violations_handled_counter).to receive(:increment)
|
||||
end
|
||||
|
||||
before do
|
||||
stub_prometheus_metrics
|
||||
|
||||
allow(handler).to receive(:on_high_heap_fragmentation).and_return(true)
|
||||
allow(handler).to receive(:call).and_return(true)
|
||||
|
||||
allow(logger).to receive(:warn)
|
||||
allow(logger).to receive(:info)
|
||||
|
||||
allow(Gitlab::Metrics::Memory).to receive(:gc_heap_fragmentation).and_return(fragmentation)
|
||||
allow(Gitlab::Metrics::System).to receive(:memory_usage_uss_pss).and_return({ uss: worker_memory })
|
||||
allow(Gitlab::Metrics::System).to receive(:memory_usage_uss_pss).with(
|
||||
pid: Gitlab::Cluster::PRIMARY_PID
|
||||
).and_return({ uss: primary_memory })
|
||||
|
||||
allow(::Prometheus::PidProvider).to receive(:worker_id).and_return('worker_1')
|
||||
end
|
||||
|
||||
context 'when created' do
|
||||
let(:fragmentation) { 0 }
|
||||
let(:max_strikes) { 0 }
|
||||
|
||||
it 'sets the heap fragmentation limit gauge' do
|
||||
expect(heap_frag_limit_gauge).to receive(:set).with({}, max_heap_fragmentation)
|
||||
|
||||
|
@ -71,7 +81,8 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, :prometheus do
|
|||
it 'initializes with defaults' do
|
||||
watchdog = described_class.new(handler: handler, logger: logger)
|
||||
|
||||
expect(watchdog.max_heap_fragmentation).to eq(described_class::DEFAULT_HEAP_FRAG_THRESHOLD)
|
||||
expect(watchdog.max_heap_fragmentation).to eq(described_class::DEFAULT_MAX_HEAP_FRAG)
|
||||
expect(watchdog.max_mem_growth).to eq(described_class::DEFAULT_MAX_MEM_GROWTH)
|
||||
expect(watchdog.max_strikes).to eq(described_class::DEFAULT_MAX_STRIKES)
|
||||
expect(watchdog.sleep_time_seconds).to eq(described_class::DEFAULT_SLEEP_TIME_SECONDS)
|
||||
end
|
||||
|
@ -82,6 +93,7 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, :prometheus do
|
|||
stub_env('GITLAB_MEMWD_MAX_HEAP_FRAG', 1)
|
||||
stub_env('GITLAB_MEMWD_MAX_STRIKES', 2)
|
||||
stub_env('GITLAB_MEMWD_SLEEP_TIME_SEC', 3)
|
||||
stub_env('GITLAB_MEMWD_MAX_MEM_GROWTH', 4)
|
||||
end
|
||||
|
||||
it 'initializes with these settings' do
|
||||
|
@ -90,30 +102,17 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, :prometheus do
|
|||
expect(watchdog.max_heap_fragmentation).to eq(1)
|
||||
expect(watchdog.max_strikes).to eq(2)
|
||||
expect(watchdog.sleep_time_seconds).to eq(3)
|
||||
expect(watchdog.max_mem_growth).to eq(4)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when process does not exceed heap fragmentation threshold' do
|
||||
let(:fragmentation) { max_heap_fragmentation - 0.1 }
|
||||
let(:max_strikes) { 0 } # To rule out that we were granting too many strikes.
|
||||
|
||||
it 'does not signal the handler' do
|
||||
expect(handler).not_to receive(:on_high_heap_fragmentation)
|
||||
|
||||
watchdog.call
|
||||
end
|
||||
end
|
||||
|
||||
context 'when process exceeds heap fragmentation threshold permanently' do
|
||||
let(:fragmentation) { max_heap_fragmentation + 0.1 }
|
||||
let(:max_strikes) { 3 }
|
||||
|
||||
shared_examples 'has strikes left' do |stat|
|
||||
context 'when process has not exceeded allowed number of strikes' do
|
||||
let(:watchdog_iterations) { max_strikes }
|
||||
|
||||
it 'does not signal the handler' do
|
||||
expect(handler).not_to receive(:on_high_heap_fragmentation)
|
||||
expect(handler).not_to receive(:call)
|
||||
|
||||
watchdog.call
|
||||
end
|
||||
|
@ -125,119 +124,228 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, :prometheus do
|
|||
end
|
||||
|
||||
it 'increments the violations counter' do
|
||||
expect(heap_frag_violations_counter).to receive(:increment).exactly(watchdog_iterations)
|
||||
expect(violations_counter).to receive(:increment).with(reason: stat).exactly(watchdog_iterations)
|
||||
|
||||
watchdog.call
|
||||
end
|
||||
|
||||
it 'does not increment violations handled counter' do
|
||||
expect(heap_frag_violations_handled_counter).not_to receive(:increment)
|
||||
expect(violations_handled_counter).not_to receive(:increment)
|
||||
|
||||
watchdog.call
|
||||
end
|
||||
end
|
||||
|
||||
context 'when process exceeds the allowed number of strikes' do
|
||||
let(:watchdog_iterations) { max_strikes + 1 }
|
||||
|
||||
it 'signals the handler and resets strike counter' do
|
||||
expect(handler).to receive(:on_high_heap_fragmentation).and_return(true)
|
||||
|
||||
watchdog.call
|
||||
|
||||
expect(watchdog.strikes).to eq(0)
|
||||
end
|
||||
|
||||
it 'logs the event' do
|
||||
expect(Gitlab::Metrics::System).to receive(:memory_usage_rss).at_least(:once).and_return(1024)
|
||||
expect(logger).to receive(:warn).with({
|
||||
message: 'heap fragmentation limit exceeded',
|
||||
pid: Process.pid,
|
||||
worker_id: 'worker_1',
|
||||
memwd_handler_class: 'RSpec::Mocks::InstanceVerifyingDouble',
|
||||
memwd_sleep_time_s: sleep_time,
|
||||
memwd_max_heap_frag: max_heap_fragmentation,
|
||||
memwd_cur_heap_frag: fragmentation,
|
||||
memwd_max_strikes: max_strikes,
|
||||
memwd_cur_strikes: max_strikes + 1,
|
||||
memwd_rss_bytes: 1024
|
||||
})
|
||||
|
||||
watchdog.call
|
||||
end
|
||||
|
||||
it 'increments both the violations and violations handled counters' do
|
||||
expect(heap_frag_violations_counter).to receive(:increment).exactly(watchdog_iterations)
|
||||
expect(heap_frag_violations_handled_counter).to receive(:increment)
|
||||
|
||||
watchdog.call
|
||||
end
|
||||
|
||||
context 'when enforce_memory_watchdog ops toggle is off' do
|
||||
before do
|
||||
stub_feature_flags(enforce_memory_watchdog: false)
|
||||
end
|
||||
|
||||
it 'always uses the NullHandler' do
|
||||
expect(handler).not_to receive(:on_high_heap_fragmentation)
|
||||
expect(described_class::NullHandler.instance).to(
|
||||
receive(:on_high_heap_fragmentation).with(fragmentation).and_return(true)
|
||||
)
|
||||
|
||||
watchdog.call
|
||||
end
|
||||
end
|
||||
|
||||
context 'when handler result is true' do
|
||||
it 'considers the event handled and stops itself' do
|
||||
expect(handler).to receive(:on_high_heap_fragmentation).once.and_return(true)
|
||||
expect(logger).to receive(:info).with(hash_including(message: 'stopped'))
|
||||
|
||||
watchdog.call
|
||||
end
|
||||
end
|
||||
|
||||
context 'when handler result is false' do
|
||||
let(:max_strikes) { 0 } # to make sure the handler fires each iteration
|
||||
let(:watchdog_iterations) { 3 }
|
||||
|
||||
it 'keeps running' do
|
||||
expect(heap_frag_violations_counter).to receive(:increment).exactly(watchdog_iterations)
|
||||
expect(heap_frag_violations_handled_counter).to receive(:increment).exactly(watchdog_iterations)
|
||||
# Return true the third time to terminate the daemon.
|
||||
expect(handler).to receive(:on_high_heap_fragmentation).and_return(false, false, true)
|
||||
|
||||
watchdog.call
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when process exceeds heap fragmentation threshold temporarily' do
|
||||
let(:fragmentation) { max_heap_fragmentation }
|
||||
let(:max_strikes) { 1 }
|
||||
let(:watchdog_iterations) { 4 }
|
||||
shared_examples 'no strikes left' do |stat|
|
||||
it 'signals the handler and resets strike counter' do
|
||||
expect(handler).to receive(:call).and_return(true)
|
||||
|
||||
before do
|
||||
allow(Gitlab::Metrics::Memory).to receive(:gc_heap_fragmentation).and_return(
|
||||
fragmentation - 0.1,
|
||||
fragmentation + 0.2,
|
||||
fragmentation - 0.1,
|
||||
fragmentation + 0.1
|
||||
)
|
||||
watchdog.call
|
||||
|
||||
expect(watchdog.strikes(stat.to_sym)).to eq(0)
|
||||
end
|
||||
|
||||
it 'does not signal the handler' do
|
||||
expect(handler).not_to receive(:on_high_heap_fragmentation)
|
||||
it 'increments both the violations and violations handled counters' do
|
||||
expect(violations_counter).to receive(:increment).with(reason: stat).exactly(watchdog_iterations)
|
||||
expect(violations_handled_counter).to receive(:increment).with(reason: stat)
|
||||
|
||||
watchdog.call
|
||||
end
|
||||
|
||||
context 'when enforce_memory_watchdog ops toggle is off' do
|
||||
before do
|
||||
stub_feature_flags(enforce_memory_watchdog: false)
|
||||
end
|
||||
|
||||
it 'always uses the NullHandler' do
|
||||
expect(handler).not_to receive(:call)
|
||||
expect(described_class::NullHandler.instance).to receive(:call).and_return(true)
|
||||
|
||||
watchdog.call
|
||||
end
|
||||
end
|
||||
|
||||
context 'when handler result is true' do
|
||||
it 'considers the event handled and stops itself' do
|
||||
expect(handler).to receive(:call).once.and_return(true)
|
||||
expect(logger).to receive(:info).with(hash_including(message: 'stopped'))
|
||||
|
||||
watchdog.call
|
||||
end
|
||||
end
|
||||
|
||||
context 'when handler result is false' do
|
||||
let(:max_strikes) { 0 } # to make sure the handler fires each iteration
|
||||
let(:watchdog_iterations) { 3 }
|
||||
|
||||
it 'keeps running' do
|
||||
expect(violations_counter).to receive(:increment).exactly(watchdog_iterations)
|
||||
expect(violations_handled_counter).to receive(:increment).exactly(watchdog_iterations)
|
||||
# Return true the third time to terminate the daemon.
|
||||
expect(handler).to receive(:call).and_return(false, false, true)
|
||||
|
||||
watchdog.call
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when monitoring memory growth' do
|
||||
let(:primary_memory) { 2048 }
|
||||
|
||||
context 'when process does not exceed threshold' do
|
||||
let(:worker_memory) { max_mem_growth * primary_memory - 1 }
|
||||
|
||||
it 'does not signal the handler' do
|
||||
expect(handler).not_to receive(:call)
|
||||
|
||||
watchdog.call
|
||||
end
|
||||
end
|
||||
|
||||
context 'when process exceeds threshold permanently' do
|
||||
let(:worker_memory) { max_mem_growth * primary_memory + 1 }
|
||||
let(:max_strikes) { 3 }
|
||||
|
||||
it_behaves_like 'has strikes left', 'mem_growth'
|
||||
|
||||
context 'when process exceeds the allowed number of strikes' do
|
||||
let(:watchdog_iterations) { max_strikes + 1 }
|
||||
|
||||
it_behaves_like 'no strikes left', 'mem_growth'
|
||||
|
||||
it 'only reads reference memory once' do
|
||||
expect(Gitlab::Metrics::System).to receive(:memory_usage_uss_pss)
|
||||
.with(pid: Gitlab::Cluster::PRIMARY_PID)
|
||||
.once
|
||||
|
||||
watchdog.call
|
||||
end
|
||||
|
||||
it 'logs the event' do
|
||||
expect(Gitlab::Metrics::System).to receive(:memory_usage_rss).at_least(:once).and_return(1024)
|
||||
expect(logger).to receive(:warn).with({
|
||||
message: 'memory limit exceeded',
|
||||
pid: Process.pid,
|
||||
worker_id: 'worker_1',
|
||||
memwd_handler_class: 'RSpec::Mocks::InstanceVerifyingDouble',
|
||||
memwd_sleep_time_s: sleep_time,
|
||||
memwd_max_uss_bytes: max_mem_growth * primary_memory,
|
||||
memwd_ref_uss_bytes: primary_memory,
|
||||
memwd_uss_bytes: worker_memory,
|
||||
memwd_rss_bytes: 1024,
|
||||
memwd_max_strikes: max_strikes,
|
||||
memwd_cur_strikes: max_strikes + 1
|
||||
})
|
||||
|
||||
watchdog.call
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when process exceeds threshold temporarily' do
|
||||
let(:worker_memory) { max_mem_growth * primary_memory }
|
||||
let(:max_strikes) { 1 }
|
||||
let(:watchdog_iterations) { 4 }
|
||||
|
||||
before do
|
||||
allow(Gitlab::Metrics::System).to receive(:memory_usage_uss_pss).and_return(
|
||||
{ uss: worker_memory - 0.1 },
|
||||
{ uss: worker_memory + 0.2 },
|
||||
{ uss: worker_memory - 0.1 },
|
||||
{ uss: worker_memory + 0.1 }
|
||||
)
|
||||
allow(Gitlab::Metrics::System).to receive(:memory_usage_uss_pss).with(
|
||||
pid: Gitlab::Cluster::PRIMARY_PID
|
||||
).and_return({ uss: primary_memory })
|
||||
end
|
||||
|
||||
it 'does not signal the handler' do
|
||||
expect(handler).not_to receive(:call)
|
||||
|
||||
watchdog.call
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when monitoring heap fragmentation' do
|
||||
context 'when process does not exceed threshold' do
|
||||
let(:fragmentation) { max_heap_fragmentation - 0.1 }
|
||||
|
||||
it 'does not signal the handler' do
|
||||
expect(handler).not_to receive(:call)
|
||||
|
||||
watchdog.call
|
||||
end
|
||||
end
|
||||
|
||||
context 'when process exceeds threshold permanently' do
|
||||
let(:fragmentation) { max_heap_fragmentation + 0.1 }
|
||||
let(:max_strikes) { 3 }
|
||||
|
||||
it_behaves_like 'has strikes left', 'heap_frag'
|
||||
|
||||
context 'when process exceeds the allowed number of strikes' do
|
||||
let(:watchdog_iterations) { max_strikes + 1 }
|
||||
|
||||
it_behaves_like 'no strikes left', 'heap_frag'
|
||||
|
||||
it 'logs the event' do
|
||||
expect(Gitlab::Metrics::System).to receive(:memory_usage_rss).at_least(:once).and_return(1024)
|
||||
expect(logger).to receive(:warn).with({
|
||||
message: 'heap fragmentation limit exceeded',
|
||||
pid: Process.pid,
|
||||
worker_id: 'worker_1',
|
||||
memwd_handler_class: 'RSpec::Mocks::InstanceVerifyingDouble',
|
||||
memwd_sleep_time_s: sleep_time,
|
||||
memwd_max_heap_frag: max_heap_fragmentation,
|
||||
memwd_cur_heap_frag: fragmentation,
|
||||
memwd_max_strikes: max_strikes,
|
||||
memwd_cur_strikes: max_strikes + 1,
|
||||
memwd_rss_bytes: 1024
|
||||
})
|
||||
|
||||
watchdog.call
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when process exceeds threshold temporarily' do
|
||||
let(:fragmentation) { max_heap_fragmentation }
|
||||
let(:max_strikes) { 1 }
|
||||
let(:watchdog_iterations) { 4 }
|
||||
|
||||
before do
|
||||
allow(Gitlab::Metrics::Memory).to receive(:gc_heap_fragmentation).and_return(
|
||||
fragmentation - 0.1,
|
||||
fragmentation + 0.2,
|
||||
fragmentation - 0.1,
|
||||
fragmentation + 0.1
|
||||
)
|
||||
end
|
||||
|
||||
it 'does not signal the handler' do
|
||||
expect(handler).not_to receive(:call)
|
||||
|
||||
watchdog.call
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when both memory fragmentation and growth exceed thresholds' do
|
||||
let(:fragmentation) { max_heap_fragmentation + 0.1 }
|
||||
let(:primary_memory) { 2048 }
|
||||
let(:worker_memory) { max_mem_growth * primary_memory + 1 }
|
||||
let(:watchdog_iterations) { max_strikes + 1 }
|
||||
|
||||
it 'only calls the handler once' do
|
||||
expect(handler).to receive(:call).once.and_return(true)
|
||||
|
||||
watchdog.call
|
||||
end
|
||||
end
|
||||
|
||||
context 'when gitlab_memory_watchdog ops toggle is off' do
|
||||
let(:fragmentation) { 0 }
|
||||
let(:max_strikes) { 0 }
|
||||
|
||||
before do
|
||||
stub_feature_flags(gitlab_memory_watchdog: false)
|
||||
end
|
||||
|
@ -247,6 +355,12 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, :prometheus do
|
|||
|
||||
watchdog.call
|
||||
end
|
||||
|
||||
it 'does not monitor memory growth' do
|
||||
expect(Gitlab::Metrics::System).not_to receive(:memory_usage_uss_pss)
|
||||
|
||||
watchdog.call
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -254,9 +368,9 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, :prometheus do
|
|||
context 'NullHandler' do
|
||||
subject(:handler) { described_class::NullHandler.instance }
|
||||
|
||||
describe '#on_high_heap_fragmentation' do
|
||||
describe '#call' do
|
||||
it 'does nothing' do
|
||||
expect(handler.on_high_heap_fragmentation(1.0)).to be(false)
|
||||
expect(handler.call).to be(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -264,11 +378,11 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, :prometheus do
|
|||
context 'TermProcessHandler' do
|
||||
subject(:handler) { described_class::TermProcessHandler.new(42) }
|
||||
|
||||
describe '#on_high_heap_fragmentation' do
|
||||
describe '#call' do
|
||||
it 'sends SIGTERM to the current process' do
|
||||
expect(Process).to receive(:kill).with(:TERM, 42)
|
||||
|
||||
expect(handler.on_high_heap_fragmentation(1.0)).to be(true)
|
||||
expect(handler.call).to be(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -286,12 +400,12 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, :prometheus do
|
|||
stub_const('::Puma::Cluster::WorkerHandle', puma_worker_handle_class)
|
||||
end
|
||||
|
||||
describe '#on_high_heap_fragmentation' do
|
||||
describe '#call' do
|
||||
it 'invokes orderly termination via Puma API' do
|
||||
expect(puma_worker_handle_class).to receive(:new).and_return(puma_worker_handle)
|
||||
expect(puma_worker_handle).to receive(:term)
|
||||
|
||||
expect(handler.on_high_heap_fragmentation(1.0)).to be(true)
|
||||
expect(handler.call).to be(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,6 +10,8 @@ RSpec.describe Sidebars::Groups::Menus::PackagesRegistriesMenu do
|
|||
end
|
||||
end
|
||||
|
||||
let_it_be(:harbor_integration) { create(:harbor_integration, group: group, project: nil) }
|
||||
|
||||
let(:user) { owner }
|
||||
let(:context) { Sidebars::Groups::Context.new(current_user: user, container: group) }
|
||||
let(:menu) { described_class.new(context) }
|
||||
|
|
|
@ -5,6 +5,8 @@ require 'spec_helper'
|
|||
RSpec.describe Sidebars::Projects::Menus::PackagesRegistriesMenu do
|
||||
let_it_be(:project) { create(:project) }
|
||||
|
||||
let_it_be(:harbor_integration) { create(:harbor_integration, project: project) }
|
||||
|
||||
let(:user) { project.first_owner }
|
||||
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
|
||||
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration!
|
||||
|
||||
RSpec.describe OrphanedInvitedMembersCleanup, :migration do
|
||||
describe '#up', :aggregate_failures do
|
||||
it 'removes accepted members with no associated user' do
|
||||
user = create_user!('testuser1')
|
||||
|
||||
create_member(invite_token: nil, invite_accepted_at: 1.day.ago)
|
||||
record2 = create_member(invite_token: nil, invite_accepted_at: 1.day.ago, user_id: user.id)
|
||||
record3 = create_member(invite_token: 'foo2', invite_accepted_at: nil)
|
||||
record4 = create_member(invite_token: 'foo3', invite_accepted_at: 1.day.ago)
|
||||
|
||||
migrate!
|
||||
|
||||
expect(table(:members).all.pluck(:id)).to match_array([record2.id, record3.id, record4.id])
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_user!(name)
|
||||
email = "#{name}@example.com"
|
||||
|
||||
table(:users).create!(
|
||||
name: name,
|
||||
email: email,
|
||||
username: name,
|
||||
projects_limit: 0
|
||||
)
|
||||
end
|
||||
|
||||
def create_member(**extra_attributes)
|
||||
defaults = {
|
||||
access_level: 10,
|
||||
source_id: 1,
|
||||
source_type: "Project",
|
||||
notification_level: 0,
|
||||
type: 'ProjectMember'
|
||||
}
|
||||
|
||||
table(:members).create!(defaults.merge(extra_attributes))
|
||||
end
|
||||
end
|
|
@ -27,6 +27,12 @@ RSpec.describe Integrations::Harbor do
|
|||
it { is_expected.to allow_value('https://demo.goharbor.io').for(:url) }
|
||||
end
|
||||
|
||||
describe 'hostname' do
|
||||
it 'returns the host of the integration url' do
|
||||
expect(harbor_integration.hostname).to eq('demo.goharbor.io')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#fields' do
|
||||
it 'returns custom fields' do
|
||||
expect(harbor_integration.fields.pluck(:name)).to eq(%w[url project_name username password])
|
||||
|
|
|
@ -36,6 +36,18 @@ RSpec.describe API::DebianGroupPackages do
|
|||
it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete Packages file/
|
||||
end
|
||||
|
||||
describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/source/Sources' do
|
||||
let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/source/Sources" }
|
||||
|
||||
it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete Sources file/
|
||||
end
|
||||
|
||||
describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/Packages' do
|
||||
let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/Packages" }
|
||||
|
||||
it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete D-I Packages file/
|
||||
end
|
||||
|
||||
describe 'GET groups/:id/-/packages/debian/pool/:codename/:project_id/:letter/:package_name/:package_version/:file_name' do
|
||||
let(:url) { "/groups/#{container.id}/-/packages/debian/pool/#{package.debian_distribution.codename}/#{project.id}/#{letter}/#{package.name}/#{package.version}/#{file_name}" }
|
||||
let(:file_name) { params[:file_name] }
|
||||
|
|
|
@ -36,6 +36,18 @@ RSpec.describe API::DebianProjectPackages do
|
|||
it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete Packages file/
|
||||
end
|
||||
|
||||
describe 'GET projects/:id/packages/debian/dists/*distribution/source/Sources' do
|
||||
let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/source/Sources" }
|
||||
|
||||
it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete Sources file/
|
||||
end
|
||||
|
||||
describe 'GET projects/:id/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/Packages' do
|
||||
let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/Packages" }
|
||||
|
||||
it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete D-I Packages file/
|
||||
end
|
||||
|
||||
describe 'GET projects/:id/packages/debian/pool/:codename/:letter/:package_name/:package_version/:file_name' do
|
||||
let(:url) { "/projects/#{container.id}/packages/debian/pool/#{package.debian_distribution.codename}/#{letter}/#{package.name}/#{package.version}/#{file_name}" }
|
||||
let(:file_name) { params[:file_name] }
|
||||
|
|
|
@ -18,6 +18,8 @@ RSpec.shared_context 'Debian repository shared context' do |container_type, can_
|
|||
let_it_be(:private_architecture_all, freeze: true) { create("debian_#{container_type}_architecture", distribution: private_distribution, name: 'all') }
|
||||
let_it_be(:private_architecture, freeze: true) { create("debian_#{container_type}_architecture", distribution: private_distribution, name: 'existing-arch') }
|
||||
let_it_be(:private_component_file) { create("debian_#{container_type}_component_file", component: private_component, architecture: private_architecture) }
|
||||
let_it_be(:private_component_sources) { create("debian_#{container_type}_component_file", :sources, component: private_component) }
|
||||
let_it_be(:private_component_file_di) { create("debian_#{container_type}_component_file", :di_packages, component: private_component, architecture: private_architecture) }
|
||||
|
||||
let_it_be(:public_distribution, freeze: true) { create("debian_#{container_type}_distribution", :with_file, container: public_container, codename: 'existing-codename') }
|
||||
let_it_be(:public_distribution_key, freeze: true) { create("debian_#{container_type}_distribution_key", distribution: public_distribution) }
|
||||
|
@ -25,6 +27,8 @@ RSpec.shared_context 'Debian repository shared context' do |container_type, can_
|
|||
let_it_be(:public_architecture_all, freeze: true) { create("debian_#{container_type}_architecture", distribution: public_distribution, name: 'all') }
|
||||
let_it_be(:public_architecture, freeze: true) { create("debian_#{container_type}_architecture", distribution: public_distribution, name: 'existing-arch') }
|
||||
let_it_be(:public_component_file) { create("debian_#{container_type}_component_file", component: public_component, architecture: public_architecture) }
|
||||
let_it_be(:public_component_file_sources) { create("debian_#{container_type}_component_file", :sources, component: public_component) }
|
||||
let_it_be(:public_component_file_di) { create("debian_#{container_type}_component_file", :di_packages, component: public_component, architecture: public_architecture) }
|
||||
|
||||
if container_type == :group
|
||||
let_it_be(:private_project) { create(:project, :private, group: private_container) }
|
||||
|
@ -48,7 +52,6 @@ RSpec.shared_context 'Debian repository shared context' do |container_type, can_
|
|||
let(:distribution) { { private: private_distribution, public: public_distribution }[visibility_level] }
|
||||
let(:architecture) { { private: private_architecture, public: public_architecture }[visibility_level] }
|
||||
let(:component) { { private: private_component, public: public_component }[visibility_level] }
|
||||
let(:component_file) { { private: private_component_file, public: public_component_file }[visibility_level] }
|
||||
let(:package) { { private: private_package, public: public_package }[visibility_level] }
|
||||
let(:letter) { package.name[0..2] == 'lib' ? package.name[0..3] : package.name[0] }
|
||||
|
||||
|
|
Loading…
Reference in New Issue