Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show secrets from org and global level #2873

Merged
merged 30 commits into from
Dec 16, 2023
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
2ff1ba3
Show secrets from org and global level
anbraten Nov 26, 2023
32c5165
use badge
anbraten Nov 27, 2023
b366e9c
update swagger
anbraten Nov 27, 2023
151afe0
fix secret listing
anbraten Nov 28, 2023
fead723
Merge branch 'main' into show-upper-lvl-secrets
anbraten Nov 28, 2023
655d582
Merge branch 'main' into show-upper-lvl-secrets
anbraten Dec 2, 2023
e2aa42d
Merge remote-tracking branch 'upstream/main' into show-upper-lvl-secrets
anbraten Dec 2, 2023
c6cc033
Merge branch 'show-upper-lvl-secrets' of github.com:anbraten/woodpeck…
anbraten Dec 2, 2023
0623ba3
Merge branch 'main' into show-upper-lvl-secrets
anbraten Dec 7, 2023
4440177
Merge branch 'main' into show-upper-lvl-secrets
qwerty287 Dec 8, 2023
d79605c
fix secret loading
anbraten Dec 8, 2023
86a330e
Merge branch 'show-upper-lvl-secrets' of github.com:anbraten/woodpeck…
anbraten Dec 8, 2023
233b718
support each for usePagination
anbraten Dec 8, 2023
5fa7ff8
sort repo,org, global
anbraten Dec 8, 2023
394fcc1
some testing
anbraten Dec 8, 2023
ddef97e
Merge remote-tracking branch 'upstream/main' into show-upper-lvl-secrets
anbraten Dec 13, 2023
1f2787d
Merge branch 'main' into show-upper-lvl-secrets
anbraten Dec 14, 2023
867b693
cleanup
anbraten Dec 14, 2023
e3a5a7d
fixes
anbraten Dec 14, 2023
3b667a9
improve pagination
anbraten Dec 14, 2023
95c07df
cleanup
anbraten Dec 14, 2023
1ec7699
fix lint
anbraten Dec 14, 2023
6e3f233
fix type
anbraten Dec 14, 2023
696b5bd
update woodpecker-go
anbraten Dec 16, 2023
a472c54
update checks
anbraten Dec 16, 2023
fda8c75
Merge branch 'main' into show-upper-lvl-secrets
anbraten Dec 16, 2023
69d0f2a
Merge branch 'main' into show-upper-lvl-secrets
anbraten Dec 16, 2023
f15ec58
fix test
anbraten Dec 16, 2023
69ae730
Merge branch 'show-upper-lvl-secrets' of github.com:anbraten/woodpeck…
anbraten Dec 16, 2023
ecbfaf2
fix test
anbraten Dec 16, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions cmd/server/docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -4233,6 +4233,12 @@ const docTemplate = `{
"name": {
"type": "string"
},
"org_id": {
"type": "integer"
},
"repo_id": {
"type": "integer"
},
"value": {
"type": "string"
}
Expand Down
9 changes: 7 additions & 2 deletions server/model/secret.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ type SecretStore interface {
// Secret represents a secret variable, such as a password or token.
type Secret struct {
ID int64 `json:"id" xorm:"pk autoincr 'secret_id'"`
OrgID int64 `json:"-" xorm:"NOT NULL DEFAULT 0 UNIQUE(s) INDEX 'secret_org_id'"`
RepoID int64 `json:"-" xorm:"NOT NULL DEFAULT 0 UNIQUE(s) INDEX 'secret_repo_id'"`
OrgID int64 `json:"org_id" xorm:"NOT NULL DEFAULT 0 UNIQUE(s) INDEX 'secret_org_id'"`
anbraten marked this conversation as resolved.
Show resolved Hide resolved
RepoID int64 `json:"repo_id" xorm:"NOT NULL DEFAULT 0 UNIQUE(s) INDEX 'secret_repo_id'"`
Name string `json:"name" xorm:"NOT NULL UNIQUE(s) INDEX 'secret_name'"`
Value string `json:"value,omitempty" xorm:"TEXT 'secret_value'"`
Images []string `json:"images" xorm:"json 'secret_images'"`
Expand All @@ -98,6 +98,11 @@ func (s Secret) Organization() bool {
return s.RepoID == 0 && s.OrgID != 0
}

// Repository secret.
func (s Secret) Repository() bool {
return s.RepoID != 0 && s.OrgID == 0
}
anbraten marked this conversation as resolved.
Show resolved Hide resolved

// Match returns true if an image and event match the restricted list.
func (s *Secret) Match(event WebhookEvent) bool {
if len(s.Events) == 0 {
Expand Down
12 changes: 7 additions & 5 deletions web/src/assets/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@
"saved": "Secret saved",
"images": {
"images": "Available for following images",
"desc": "Comma separated list of images where this secret is available, leave empty to allow all images"
"desc": "List of images where this secret is available, leave empty to allow all images"
},
"events": {
"events": "Available at following events",
Expand Down Expand Up @@ -305,7 +305,7 @@
"saved": "Organization secret saved",
"images": {
"images": "Available for following images",
"desc": "Comma separated list of images where this secret is available, leave empty to allow all images"
"desc": "List of images where this secret is available, leave empty to allow all images"
},
"plugins_only": "Only available for plugins",
"events": {
Expand Down Expand Up @@ -334,7 +334,7 @@
"saved": "Global secret saved",
"images": {
"images": "Available for following images",
"desc": "Comma separated list of images where this secret is available, leave empty to allow all images"
"desc": "List of images where this secret is available, leave empty to allow all images"
},
"plugins_only": "Only available for plugins",
"events": {
Expand Down Expand Up @@ -476,7 +476,7 @@
"saved": "User secret saved",
"images": {
"images": "Available for following images",
"desc": "Comma separated list of images where this secret is available, leave empty to allow all images"
"desc": "List of images where this secret is available, leave empty to allow all images"
},
"plugins_only": "Only available for plugins",
"events": {
Expand Down Expand Up @@ -504,5 +504,7 @@
"default": "default",
"info": "Info",
"running_version": "You are running Woodpecker {0}",
"update_woodpecker": "Please update your Woodpecker instance to {0}"
"update_woodpecker": "Please update your Woodpecker instance to {0}",
"global_level_secret": "global secret",
"org_level_secret": "organization secret"
}
2 changes: 1 addition & 1 deletion web/src/components/atomic/Button.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
>
<slot>
<Icon v-if="startIcon" :name="startIcon" class="!w-6 !h-6" :class="{ invisible: isLoading, 'mr-1': text }" />
<span :class="{ invisible: isLoading }">{{ text }}</span>
<span :class="{ invisible: isLoading }" class="flex-shrink-0">{{ text }}</span>
<Icon v-if="endIcon" :name="endIcon" class="ml-2 w-6 h-6" :class="{ invisible: isLoading }" />
<div
v-if="isLoading"
Expand Down
44 changes: 41 additions & 3 deletions web/src/components/repo/settings/SecretsTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,53 @@ const repo = inject<Ref<Repo>>('repo');
const selectedSecret = ref<Partial<Secret>>();
const isEditingSecret = computed(() => !!selectedSecret.value?.id);

async function loadSecrets(page: number): Promise<Secret[] | null> {
async function loadSecrets(page: number, level: 'repo' | 'org' | 'global'): Promise<Secret[] | null> {
if (!repo?.value) {
throw new Error("Unexpected: Can't load repo");
}

return apiClient.getSecretList(repo.value.id, page);
switch (level) {
case 'org':
return apiClient.getOrgSecretList(repo.value.org_id, page);
case 'global':
return apiClient.getGlobalSecretList(page);
default:
return apiClient.getSecretList(repo.value.id, page);
}
}

const { resetPage, data: secrets } = usePagination(loadSecrets, () => !selectedSecret.value);
const { resetPage, data: _secrets } = usePagination(loadSecrets, () => !selectedSecret.value, {
each: ['repo', 'org', 'global'],
name: 'secrets',
});
const secrets = computed(() => {
const secretsList: Record<string, Secret & { edit?: boolean; level: 'repo' | 'org' | 'global' }> = {};

// eslint-disable-next-line no-restricted-syntax
for (const level of ['repo', 'org', 'global']) {
anbraten marked this conversation as resolved.
Show resolved Hide resolved
// eslint-disable-next-line no-restricted-syntax
for (const secret of _secrets.value) {
if (
((level === 'repo' && secret.repo_id !== 0 && secret.org_id === 0) ||
(level === 'org' && secret.repo_id === 0 && secret.org_id !== 0) ||
(level === 'global' && secret.repo_id === 0 && secret.org_id === 0)) &&
!secretsList[secret.name]
) {
secretsList[secret.name] = { ...secret, edit: secret.repo_id !== 0, level };
}
}
}

const levelsOrder = {
global: 0,
org: 1,
repo: 2,
};

return Object.values(secretsList)
.toSorted((a, b) => a.name.localeCompare(b.name))
.toSorted((a, b) => levelsOrder[b.level] - levelsOrder[a.level]);
});

const { doSubmit: createSecret, isLoading: isSaving } = useAsyncAction(async () => {
if (!repo?.value) {
Expand Down
52 changes: 36 additions & 16 deletions web/src/components/secrets/SecretEdit.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,27 @@
</InputField>

<InputField :label="$t(i18nPrefix + 'value')">
<TextField v-model="innerValue.value" :placeholder="$t(i18nPrefix + 'value')" :lines="5" />
<TextField
v-model="innerValue.value"
:placeholder="$t(i18nPrefix + 'value')"
:lines="5"
:required="!isEditingSecret"
/>
</InputField>

<InputField :label="$t(i18nPrefix + 'images.images')">
<TextField v-model="images" :placeholder="$t(i18nPrefix + 'images.desc')" />
<span class="ml-1 mb-2 text-wp-text-alt-100">{{ $t(i18nPrefix + 'images.desc') }}</span>

<div class="flex flex-col gap-2">
<div v-for="image in innerValue.images" :key="image" class="flex gap-2">
<TextField :model-value="image" disabled />
<Button type="button" color="gray" start-icon="trash" @click="removeImage(image)" />
</div>
<div class="flex gap-2">
<TextField v-model="newImage" @keydown.enter.prevent="addNewImage" />
<Button type="button" color="gray" start-icon="plus" @click="addNewImage" />
</div>
</div>
</InputField>

<InputField :label="$t(i18nPrefix + 'events.events')">
Expand All @@ -36,7 +52,7 @@
</template>

<script lang="ts" setup>
import { computed, toRef } from 'vue';
import { computed, ref, toRef } from 'vue';
import { useI18n } from 'vue-i18n';

import Button from '~/components/atomic/Button.vue';
Expand Down Expand Up @@ -67,21 +83,20 @@ const innerValue = computed({
emit('update:modelValue', value);
},
});
const images = computed<string>({
get() {
return innerValue.value?.images?.join(',') || '';
},
set(value) {
if (innerValue.value) {
innerValue.value.images = value
.split(',')
.map((s) => s.trim())
.filter((s) => s !== '');
}
},
});
const isEditingSecret = computed(() => !!innerValue.value?.id);

const newImage = ref('');
function addNewImage() {
if (!newImage.value) {
return;
}
innerValue.value.images?.push(newImage.value);
newImage.value = '';
}
function removeImage(image: string) {
innerValue.value.images = innerValue.value.images?.filter((i) => i !== image);
}

const secretEventsOptions: CheckboxOption[] = [
{ value: WebhookEvents.Push, text: i18n.t('repo.pipeline.event.push') },
{ value: WebhookEvents.Tag, text: i18n.t('repo.pipeline.event.tag') },
Expand All @@ -99,6 +114,11 @@ function save() {
if (!innerValue.value) {
return;
}

if (newImage.value) {
innerValue.value.images?.push(newImage.value);
}

emit('save', innerValue.value);
}
</script>
36 changes: 22 additions & 14 deletions web/src/components/secrets/SecretList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,29 @@
class="items-center !bg-wp-background-200 !dark:bg-wp-background-100"
>
<span>{{ secret.name }}</span>
<Badge
v-if="secret.edit === false"
class="ml-2"
:label="secret.org_id === 0 ? $t('global_level_secret') : $t('org_level_secret')"
/>
<div class="ml-auto space-x-2 <md:hidden">
<Badge v-for="event in secret.events" :key="event" :label="event" />
</div>
<IconButton
icon="edit"
class="ml-2 <md:ml-auto w-8 h-8"
:title="$t('repo.settings.secrets.edit')"
@click="editSecret(secret)"
/>
<IconButton
icon="trash"
class="ml-2 w-8 h-8 hover:text-wp-control-error-100"
:is-loading="isDeleting"
:title="$t('repo.settings.secrets.delete')"
@click="deleteSecret(secret)"
/>
<template v-if="secret.edit !== false">
<IconButton
icon="edit"
class="ml-2 <md:ml-auto w-8 h-8"
:title="$t('repo.settings.secrets.edit')"
@click="editSecret(secret)"
/>
<IconButton
icon="trash"
class="ml-2 w-8 h-8 hover:text-wp-control-error-100"
:is-loading="isDeleting"
:title="$t('repo.settings.secrets.delete')"
@click="deleteSecret(secret)"
/>
</template>
</ListItem>

<div v-if="secrets?.length === 0" class="ml-2">{{ $t(i18nPrefix + 'none') }}</div>
Expand All @@ -32,12 +39,13 @@
import { toRef } from 'vue';
import { useI18n } from 'vue-i18n';

import Badge from '~/components/atomic/Badge.vue';
import IconButton from '~/components/atomic/IconButton.vue';
import ListItem from '~/components/atomic/ListItem.vue';
import { Secret } from '~/lib/api/types';

const props = defineProps<{
modelValue: Secret[];
modelValue: (Secret & { edit?: boolean })[];
isDeleting: boolean;
i18nPrefix: string;
}>();
Expand Down
47 changes: 29 additions & 18 deletions web/src/compositions/usePaginate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,32 +15,42 @@ export async function usePaginate<T>(getSingle: (page: number) => Promise<T[]>):
return result;
}

export function usePagination<T>(
_loadData: (page: number) => Promise<T[] | null>,
isActive: () => boolean = () => true,
scrollElement = ref(document.getElementById('scroll-component')),
export function usePagination<T, S = unknown>(
_loadData: (page: number, args: S) => Promise<T[] | null>,
isActive: () => boolean,
{
scrollElement: _scrollElement,
each: _each,
name,
}: { scrollElement?: Ref<HTMLElement | null>; each?: S[]; name?: string } = {},
) {
const scrollElement = _scrollElement ?? ref(document.getElementById('scroll-component'));
const page = ref(1);
const pageSize = ref(0);
const hasMore = ref(true);
const data = ref<T[]>([]) as Ref<T[]>;
const loading = ref(false);
const each = ref<S[]>(_each || []);

async function loadData() {
if (hasMore.value === false || loading.value === true) {
return;
}

loading.value = true;
const newData = await _loadData(page.value);
hasMore.value = newData !== null && newData.length >= pageSize.value;
if (newData !== null && newData.length !== 0) {
if (page.value === 1) {
pageSize.value = newData.length;
data.value = newData;
} else {
data.value.push(...newData);
}
} else if (page.value === 1) {
data.value = [];
} else {
hasMore.value = false;
name === 'secrets' && console.log('loadData', page.value, each.value?.[0] as S);
const newData = await _loadData(page.value, each.value?.[0] as S);
hasMore.value = (newData !== null && newData.length >= pageSize.value) || each.value.length > 0;
if (newData && newData.length > 0) {
data.value.push(...newData);
pageSize.value = newData.length;
} else if (each.value.length > 0) {
anbraten marked this conversation as resolved.
Show resolved Hide resolved
// use next each element
each.value.shift();
page.value = 1;
pageSize.value = 0;
hasMore.value = each.value.length > 0;
name === 'secrets' && console.log('loadData', 'next', each.value?.[0] as S, hasMore.value);
}
loading.value = false;
}
Expand All @@ -51,7 +61,7 @@ export function usePagination<T>(
useInfiniteScroll(
scrollElement,
() => {
if (isActive() && !loading.value && hasMore.value) {
if (isActive() && hasMore.value && !loading.value) {
// load more
page.value += 1;
}
Expand All @@ -60,6 +70,7 @@ export function usePagination<T>(
);

const resetPage = () => {
hasMore.value = true;
if (page.value !== 1) {
// just set page = 1, will be handled by watcher
page.value = 1;
Expand Down
2 changes: 2 additions & 0 deletions web/src/lib/api/types/secret.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { WebhookEvents } from './webhook';

export type Secret = {
id: string;
repo_id: number;
org_id: number;
name: string;
value: string;
events: WebhookEvents[];
Expand Down