From b76606308ccc4b7858232ba03fc24487cd1cc394 Mon Sep 17 00:00:00 2001 From: ktprograms <71804605+ktprograms@users.noreply.github.com> Date: Thu, 30 Dec 2021 14:26:36 +0800 Subject: [PATCH] Add field for image list in Secrets Repo Settings (Web UI) (#638) - Add field for image list in Secrets Repo Settings (Web UI) Simple comma separated input field, split into images array - validate secret images in backend - trim spaces and filter empty list items Signed-off-by: 6543 <6543@obermui.de> Co-authored-by: 6543 <6543@obermui.de> --- server/model/secret.go | 20 ++++++++ server/model/secret_test.go | 50 ++++++++++++++----- .../components/repo/settings/SecretsTab.vue | 11 ++++ 3 files changed, 69 insertions(+), 12 deletions(-) diff --git a/server/model/secret.go b/server/model/secret.go index dd19f3be6..c48344488 100644 --- a/server/model/secret.go +++ b/server/model/secret.go @@ -19,6 +19,7 @@ import ( "errors" "fmt" "path/filepath" + "regexp" ) var ( @@ -77,6 +78,13 @@ func (s *Secret) Match(event WebhookEvent) bool { return false } +var validDockerImageString = regexp.MustCompile( + `^([\w\d\-_\.\/]*` + // optional url prefix + `[\w\d\-_]+` + // image name + `)+` + + `(:[\w\d\-_]+)?$`, // optional image tag +) + // Validate validates the required fields and formats. func (s *Secret) Validate() error { for _, event := range s.Events { @@ -84,6 +92,18 @@ func (s *Secret) Validate() error { return fmt.Errorf("%s: '%s'", errSecretEventInvalid, event) } } + if len(s.Events) == 0 { + return fmt.Errorf("%s: no event specified", errSecretEventInvalid) + } + + for _, image := range s.Images { + if len(image) == 0 { + return fmt.Errorf("empty image in images") + } + if !validDockerImageString.MatchString(image) { + return fmt.Errorf("image '%s' do not match regexp '%s'", image, validDockerImageString.String()) + } + } switch { case len(s.Name) == 0: diff --git a/server/model/secret_test.go b/server/model/secret_test.go index 58dc8fea4..9b72a36ff 100644 --- a/server/model/secret_test.go +++ b/server/model/secret_test.go @@ -24,13 +24,11 @@ func TestSecret(t *testing.T) { g := goblin.Goblin(t) g.Describe("Secret", func() { g.It("should match event", func() { - secret := Secret{} - secret.Events = []WebhookEvent{"pull_request"} + secret := Secret{Events: []WebhookEvent{"pull_request"}} g.Assert(secret.Match("pull_request")).IsTrue() }) g.It("should not match event", func() { - secret := Secret{} - secret.Events = []WebhookEvent{"pull_request"} + secret := Secret{Events: []WebhookEvent{"pull_request"}} g.Assert(secret.Match("push")).IsFalse() }) g.It("should match when no event filters defined", func() { @@ -38,22 +36,50 @@ func TestSecret(t *testing.T) { g.Assert(secret.Match("pull_request")).IsTrue() }) g.It("should pass validation", func() { - secret := Secret{} - secret.Name = "secretname" - secret.Value = "secretvalue" + secret := Secret{ + Name: "secretname", + Value: "secretvalue", + Events: []WebhookEvent{EventPush}, + Images: []string{"docker.io/library/mysql:latest", "alpine"}, + } err := secret.Validate() - g.Assert(err).Equal(nil) + g.Assert(err).IsNil() }) g.Describe("should fail validation", func() { g.It("when no name", func() { - secret := Secret{} - secret.Value = "secretvalue" + secret := Secret{ + Value: "secretvalue", + Events: []WebhookEvent{EventPush}, + Images: []string{"docker.io/library/mysql:latest", "alpine"}, + } err := secret.Validate() g.Assert(err).IsNotNil() }) g.It("when no value", func() { - secret := Secret{} - secret.Name = "secretname" + secret := Secret{ + Name: "secretname", + Events: []WebhookEvent{EventPush}, + Images: []string{"docker.io/library/mysql:latest", "alpine"}, + } + err := secret.Validate() + g.Assert(err).IsNotNil() + }) + g.It("when no events", func() { + secret := Secret{ + Name: "secretname", + Value: "secretvalue", + Images: []string{"docker.io/library/mysql-alpine:latest", "alpine"}, + } + err := secret.Validate() + g.Assert(err).IsNotNil() + }) + g.It("wrong image no value", func() { + secret := Secret{ + Name: "secretname", + Value: "secretvalue", + Events: []WebhookEvent{EventPush}, + Images: []string{"wrong image:no"}, + } err := secret.Validate() g.Assert(err).IsNotNil() }) diff --git a/web/src/components/repo/settings/SecretsTab.vue b/web/src/components/repo/settings/SecretsTab.vue index cd4c1a895..c901209b4 100644 --- a/web/src/components/repo/settings/SecretsTab.vue +++ b/web/src/components/repo/settings/SecretsTab.vue @@ -50,6 +50,13 @@ + + + + @@ -118,6 +125,7 @@ export default defineComponent({ const secrets = ref(); const showAddSecret = ref(false); const selectedSecret = ref>({ ...emptySecret }); + const images = ref(''); async function loadSecrets() { if (!repo?.value) { @@ -132,6 +140,8 @@ export default defineComponent({ throw new Error("Unexpected: Can't load repo"); } + const imageList = images.value.split(',').map((s) => s.trim()); + selectedSecret.value.image = imageList.filter((s) => s !== ''); await apiClient.createSecret(repo.value.owner, repo.value.name, selectedSecret.value); notifications.notify({ title: 'Secret created', type: 'success' }); showAddSecret.value = false; @@ -157,6 +167,7 @@ export default defineComponent({ secretEventsOptions, selectedSecret, secrets, + images, showAddSecret, isSaving, isDeleting,