Global and organization registries (#1672)

Co-authored-by: Anbraten <6918444+anbraten@users.noreply.github.com>
This commit is contained in:
Lauris BH 2024-07-03 16:33:11 +03:00 committed by GitHub
parent e5f3e67bf2
commit 28e982fffb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
65 changed files with 3260 additions and 269 deletions

30
cli/admin/admin.go Normal file
View file

@ -0,0 +1,30 @@
// Copyright 2024 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package admin
import (
"github.com/urfave/cli/v2"
"go.woodpecker-ci.org/woodpecker/v2/cli/admin/registry"
)
// Command exports the admin command set.
var Command = &cli.Command{
Name: "admin",
Usage: "administer server settings",
Subcommands: []*cli.Command{
registry.Command,
},
}

View file

@ -1,4 +1,4 @@
// Copyright 2023 Woodpecker Authors
// Copyright 2024 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -21,7 +21,7 @@ import (
// Command exports the registry command set.
var Command = &cli.Command{
Name: "registry",
Usage: "manage registries",
Usage: "manage global registries",
Subcommands: []*cli.Command{
registryCreateCmd,
registryDeleteCmd,

View file

@ -0,0 +1,75 @@
// Copyright 2024 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package registry
import (
"os"
"strings"
"github.com/urfave/cli/v2"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal"
"go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker"
)
var registryCreateCmd = &cli.Command{
Name: "add",
Usage: "adds a registry",
Action: registryCreate,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "hostname",
Usage: "registry hostname",
Value: "docker.io",
},
&cli.StringFlag{
Name: "username",
Usage: "registry username",
},
&cli.StringFlag{
Name: "password",
Usage: "registry password",
},
},
}
func registryCreate(c *cli.Context) error {
var (
hostname = c.String("hostname")
username = c.String("username")
password = c.String("password")
)
client, err := internal.NewClient(c)
if err != nil {
return err
}
registry := &woodpecker.Registry{
Address: hostname,
Username: username,
Password: password,
}
if strings.HasPrefix(registry.Password, "@") {
path := strings.TrimPrefix(registry.Password, "@")
out, err := os.ReadFile(path)
if err != nil {
return err
}
registry.Password = string(out)
}
_, err = client.GlobalRegistryCreate(registry)
return err
}

View file

@ -0,0 +1,62 @@
// Copyright 2024 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package registry
import (
"html/template"
"os"
"github.com/urfave/cli/v2"
"go.woodpecker-ci.org/woodpecker/v2/cli/common"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal"
)
var registryInfoCmd = &cli.Command{
Name: "info",
Usage: "display registry info",
Action: registryInfo,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "hostname",
Usage: "registry hostname",
Value: "docker.io",
},
common.FormatFlag(tmplRegistryList, true),
},
}
func registryInfo(c *cli.Context) error {
var (
hostname = c.String("hostname")
format = c.String("format") + "\n"
)
client, err := internal.NewClient(c)
if err != nil {
return err
}
registry, err := client.GlobalRegistry(hostname)
if err != nil {
return err
}
tmpl, err := template.New("_").Parse(format)
if err != nil {
return err
}
return tmpl.Execute(os.Stdout, registry)
}

View file

@ -0,0 +1,65 @@
// Copyright 2024 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package registry
import (
"html/template"
"os"
"github.com/urfave/cli/v2"
"go.woodpecker-ci.org/woodpecker/v2/cli/common"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal"
)
var registryListCmd = &cli.Command{
Name: "ls",
Usage: "list registries",
Action: registryList,
Flags: []cli.Flag{
common.FormatFlag(tmplRegistryList, true),
},
}
func registryList(c *cli.Context) error {
format := c.String("format") + "\n"
client, err := internal.NewClient(c)
if err != nil {
return err
}
list, err := client.GlobalRegistryList()
if err != nil {
return err
}
tmpl, err := template.New("_").Parse(format)
if err != nil {
return err
}
for _, registry := range list {
if err := tmpl.Execute(os.Stdout, registry); err != nil {
return err
}
}
return nil
}
// Template for registry list information.
var tmplRegistryList = "\x1b[33m{{ .Address }} \x1b[0m" + `
Username: {{ .Username }}
Email: {{ .Email }}
`

View file

@ -0,0 +1,45 @@
// Copyright 2024 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package registry
import (
"github.com/urfave/cli/v2"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal"
)
var registryDeleteCmd = &cli.Command{
Name: "rm",
Usage: "remove a registry",
Action: registryDelete,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "hostname",
Usage: "registry hostname",
Value: "docker.io",
},
},
}
func registryDelete(c *cli.Context) error {
hostname := c.String("hostname")
client, err := internal.NewClient(c)
if err != nil {
return err
}
return client.GlobalRegistryDelete(hostname)
}

View file

@ -0,0 +1,78 @@
// Copyright 2024 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package registry
import (
"os"
"strings"
"github.com/urfave/cli/v2"
"go.woodpecker-ci.org/woodpecker/v2/cli/common"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal"
"go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker"
)
var registryUpdateCmd = &cli.Command{
Name: "update",
Usage: "update a registry",
Action: registryUpdate,
Flags: []cli.Flag{
common.OrgFlag,
&cli.StringFlag{
Name: "hostname",
Usage: "registry hostname",
Value: "docker.io",
},
&cli.StringFlag{
Name: "username",
Usage: "registry username",
},
&cli.StringFlag{
Name: "password",
Usage: "registry password",
},
},
}
func registryUpdate(c *cli.Context) error {
var (
hostname = c.String("hostname")
username = c.String("username")
password = c.String("password")
)
client, err := internal.NewClient(c)
if err != nil {
return err
}
registry := &woodpecker.Registry{
Address: hostname,
Username: username,
Password: password,
}
if strings.HasPrefix(registry.Password, "@") {
path := strings.TrimPrefix(registry.Password, "@")
out, err := os.ReadFile(path)
if err != nil {
return err
}
registry.Password = string(out)
}
_, err = client.GlobalRegistryUpdate(registry)
return err
}

30
cli/org/org.go Normal file
View file

@ -0,0 +1,30 @@
// Copyright 2024 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package org
import (
"github.com/urfave/cli/v2"
"go.woodpecker-ci.org/woodpecker/v2/cli/org/registry"
)
// Command exports the org command set.
var Command = &cli.Command{
Name: "org",
Usage: "manage organizations",
Subcommands: []*cli.Command{
registry.Command,
},
}

View file

@ -0,0 +1,60 @@
// Copyright 2024 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package registry
import (
"strconv"
"github.com/urfave/cli/v2"
"go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker"
)
// Command exports the registry command set.
var Command = &cli.Command{
Name: "registry",
Usage: "manage organization registries",
Subcommands: []*cli.Command{
registryCreateCmd,
registryDeleteCmd,
registryUpdateCmd,
registryInfoCmd,
registryListCmd,
},
}
func parseTargetArgs(client woodpecker.Client, c *cli.Context) (orgID int64, err error) {
orgIDOrName := c.String("organization")
if orgIDOrName == "" {
orgIDOrName = c.Args().First()
}
if orgIDOrName == "" {
if err := cli.ShowSubcommandHelp(c); err != nil {
return -1, err
}
}
if orgID, err := strconv.ParseInt(orgIDOrName, 10, 64); err == nil {
return orgID, nil
}
org, err := client.OrgLookup(orgIDOrName)
if err != nil {
return -1, err
}
return org.ID, nil
}

View file

@ -0,0 +1,83 @@
// Copyright 2024 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package registry
import (
"os"
"strings"
"github.com/urfave/cli/v2"
"go.woodpecker-ci.org/woodpecker/v2/cli/common"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal"
"go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker"
)
var registryCreateCmd = &cli.Command{
Name: "add",
Usage: "adds a registry",
ArgsUsage: "[org-id|org-full-name]",
Action: registryCreate,
Flags: []cli.Flag{
common.OrgFlag,
&cli.StringFlag{
Name: "hostname",
Usage: "registry hostname",
Value: "docker.io",
},
&cli.StringFlag{
Name: "username",
Usage: "registry username",
},
&cli.StringFlag{
Name: "password",
Usage: "registry password",
},
},
}
func registryCreate(c *cli.Context) error {
var (
hostname = c.String("hostname")
username = c.String("username")
password = c.String("password")
)
client, err := internal.NewClient(c)
if err != nil {
return err
}
registry := &woodpecker.Registry{
Address: hostname,
Username: username,
Password: password,
}
if strings.HasPrefix(registry.Password, "@") {
path := strings.TrimPrefix(registry.Password, "@")
out, err := os.ReadFile(path)
if err != nil {
return err
}
registry.Password = string(out)
}
orgID, err := parseTargetArgs(client, c)
if err != nil {
return err
}
_, err = client.OrgRegistryCreate(orgID, registry)
return err
}

View file

@ -0,0 +1,69 @@
// Copyright 2024 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package registry
import (
"html/template"
"os"
"github.com/urfave/cli/v2"
"go.woodpecker-ci.org/woodpecker/v2/cli/common"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal"
)
var registryInfoCmd = &cli.Command{
Name: "info",
Usage: "display registry info",
ArgsUsage: "[org-id|org-full-name]",
Action: registryInfo,
Flags: []cli.Flag{
common.OrgFlag,
&cli.StringFlag{
Name: "hostname",
Usage: "registry hostname",
Value: "docker.io",
},
common.FormatFlag(tmplRegistryList, true),
},
}
func registryInfo(c *cli.Context) error {
var (
hostname = c.String("hostname")
format = c.String("format") + "\n"
)
client, err := internal.NewClient(c)
if err != nil {
return err
}
orgID, err := parseTargetArgs(client, c)
if err != nil {
return err
}
registry, err := client.OrgRegistry(orgID, hostname)
if err != nil {
return err
}
tmpl, err := template.New("_").Parse(format)
if err != nil {
return err
}
return tmpl.Execute(os.Stdout, registry)
}

View file

@ -0,0 +1,72 @@
// Copyright 2024 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package registry
import (
"html/template"
"os"
"github.com/urfave/cli/v2"
"go.woodpecker-ci.org/woodpecker/v2/cli/common"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal"
)
var registryListCmd = &cli.Command{
Name: "ls",
Usage: "list registries",
ArgsUsage: "[org-id|org-full-name]",
Action: registryList,
Flags: []cli.Flag{
common.OrgFlag,
common.FormatFlag(tmplRegistryList, true),
},
}
func registryList(c *cli.Context) error {
format := c.String("format") + "\n"
client, err := internal.NewClient(c)
if err != nil {
return err
}
orgID, err := parseTargetArgs(client, c)
if err != nil {
return err
}
list, err := client.OrgRegistryList(orgID)
if err != nil {
return err
}
tmpl, err := template.New("_").Parse(format)
if err != nil {
return err
}
for _, registry := range list {
if err := tmpl.Execute(os.Stdout, registry); err != nil {
return err
}
}
return nil
}
// Template for registry list information.
var tmplRegistryList = "\x1b[33m{{ .Address }} \x1b[0m" + `
Username: {{ .Username }}
Email: {{ .Email }}
`

View file

@ -0,0 +1,53 @@
// Copyright 2024 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package registry
import (
"github.com/urfave/cli/v2"
"go.woodpecker-ci.org/woodpecker/v2/cli/common"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal"
)
var registryDeleteCmd = &cli.Command{
Name: "rm",
Usage: "remove a registry",
ArgsUsage: "[org-id|org-full-name]",
Action: registryDelete,
Flags: []cli.Flag{
common.OrgFlag,
&cli.StringFlag{
Name: "hostname",
Usage: "registry hostname",
Value: "docker.io",
},
},
}
func registryDelete(c *cli.Context) error {
hostname := c.String("hostname")
client, err := internal.NewClient(c)
if err != nil {
return err
}
orgID, err := parseTargetArgs(client, c)
if err != nil {
return err
}
return client.OrgRegistryDelete(orgID, hostname)
}

View file

@ -0,0 +1,84 @@
// Copyright 2024 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package registry
import (
"os"
"strings"
"github.com/urfave/cli/v2"
"go.woodpecker-ci.org/woodpecker/v2/cli/common"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal"
"go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker"
)
var registryUpdateCmd = &cli.Command{
Name: "update",
Usage: "update a registry",
ArgsUsage: "[org-id|org-full-name]",
Action: registryUpdate,
Flags: []cli.Flag{
common.OrgFlag,
&cli.StringFlag{
Name: "hostname",
Usage: "registry hostname",
Value: "docker.io",
},
&cli.StringFlag{
Name: "username",
Usage: "registry username",
},
&cli.StringFlag{
Name: "password",
Usage: "registry password",
},
},
}
func registryUpdate(c *cli.Context) error {
var (
hostname = c.String("hostname")
username = c.String("username")
password = c.String("password")
)
client, err := internal.NewClient(c)
if err != nil {
return err
}
registry := &woodpecker.Registry{
Address: hostname,
Username: username,
Password: password,
}
if strings.HasPrefix(registry.Password, "@") {
path := strings.TrimPrefix(registry.Password, "@")
out, err := os.ReadFile(path)
if err != nil {
return err
}
registry.Password = string(out)
}
orgID, err := parseTargetArgs(client, c)
if err != nil {
return err
}
_, err = client.OrgRegistryUpdate(orgID, registry)
return err
}

View file

@ -0,0 +1,44 @@
// Copyright 2023 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package registry
import (
"github.com/urfave/cli/v2"
"go.woodpecker-ci.org/woodpecker/v2/cli/internal"
"go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker"
)
// Command exports the registry command set.
var Command = &cli.Command{
Name: "registry",
Usage: "manage registries",
Subcommands: []*cli.Command{
registryCreateCmd,
registryDeleteCmd,
registryUpdateCmd,
registryInfoCmd,
registryListCmd,
},
}
func parseTargetArgs(client woodpecker.Client, c *cli.Context) (repoID int64, err error) {
repoIDOrFullName := c.String("repository")
if repoIDOrFullName == "" {
repoIDOrFullName = c.Args().First()
}
return internal.ParseRepo(client, repoIDOrFullName)
}

View file

@ -53,19 +53,12 @@ func registryCreate(c *cli.Context) error {
hostname = c.String("hostname")
username = c.String("username")
password = c.String("password")
repoIDOrFullName = c.String("repository")
)
if repoIDOrFullName == "" {
repoIDOrFullName = c.Args().First()
}
client, err := internal.NewClient(c)
if err != nil {
return err
}
repoID, err := internal.ParseRepo(client, repoIDOrFullName)
if err != nil {
return err
}
registry := &woodpecker.Registry{
Address: hostname,
Username: username,
@ -79,8 +72,12 @@ func registryCreate(c *cli.Context) error {
}
registry.Password = string(out)
}
if _, err := client.RegistryCreate(repoID, registry); err != nil {
repoID, err := parseTargetArgs(client, c)
if err != nil {
return err
}
return nil
_, err = client.RegistryCreate(repoID, registry)
return err
}

View file

@ -43,24 +43,24 @@ var registryInfoCmd = &cli.Command{
func registryInfo(c *cli.Context) error {
var (
hostname = c.String("hostname")
repoIDOrFullName = c.String("repository")
format = c.String("format") + "\n"
)
if repoIDOrFullName == "" {
repoIDOrFullName = c.Args().First()
}
client, err := internal.NewClient(c)
if err != nil {
return err
}
repoID, err := internal.ParseRepo(client, repoIDOrFullName)
repoID, err := parseTargetArgs(client, c)
if err != nil {
return err
}
registry, err := client.Registry(repoID, hostname)
if err != nil {
return err
}
tmpl, err := template.New("_").Parse(format)
if err != nil {
return err

View file

@ -36,25 +36,23 @@ var registryListCmd = &cli.Command{
}
func registryList(c *cli.Context) error {
var (
format = c.String("format") + "\n"
repoIDOrFullName = c.String("repository")
)
if repoIDOrFullName == "" {
repoIDOrFullName = c.Args().First()
}
format := c.String("format") + "\n"
client, err := internal.NewClient(c)
if err != nil {
return err
}
repoID, err := internal.ParseRepo(client, repoIDOrFullName)
repoID, err := parseTargetArgs(client, c)
if err != nil {
return err
}
list, err := client.RegistryList(repoID)
if err != nil {
return err
}
tmpl, err := template.New("_").Parse(format)
if err != nil {
return err

View file

@ -37,20 +37,17 @@ var registryDeleteCmd = &cli.Command{
}
func registryDelete(c *cli.Context) error {
var (
hostname = c.String("hostname")
repoIDOrFullName = c.String("repository")
)
if repoIDOrFullName == "" {
repoIDOrFullName = c.Args().First()
}
hostname := c.String("hostname")
client, err := internal.NewClient(c)
if err != nil {
return err
}
repoID, err := internal.ParseRepo(client, repoIDOrFullName)
repoID, err := parseTargetArgs(client, c)
if err != nil {
return err
}
return client.RegistryDelete(repoID, hostname)
}

View file

@ -53,19 +53,13 @@ func registryUpdate(c *cli.Context) error {
hostname = c.String("hostname")
username = c.String("username")
password = c.String("password")
repoIDOrFullName = c.String("repository")
)
if repoIDOrFullName == "" {
repoIDOrFullName = c.Args().First()
}
client, err := internal.NewClient(c)
if err != nil {
return err
}
repoID, err := internal.ParseRepo(client, repoIDOrFullName)
if err != nil {
return err
}
registry := &woodpecker.Registry{
Address: hostname,
Username: username,
@ -79,6 +73,12 @@ func registryUpdate(c *cli.Context) error {
}
registry.Password = string(out)
}
repoID, err := parseTargetArgs(client, c)
if err != nil {
return err
}
_, err = client.RegistryUpdate(repoID, registry)
return err
}

View file

@ -16,6 +16,8 @@ package repo
import (
"github.com/urfave/cli/v2"
"go.woodpecker-ci.org/woodpecker/v2/cli/repo/registry"
)
// Command exports the repository command.
@ -31,5 +33,6 @@ var Command = &cli.Command{
repoRepairCmd,
repoChownCmd,
repoSyncCmd,
registry.Command,
},
}

View file

@ -17,6 +17,7 @@ package main
import (
"github.com/urfave/cli/v2"
"go.woodpecker-ci.org/woodpecker/v2/cli/admin"
"go.woodpecker-ci.org/woodpecker/v2/cli/common"
"go.woodpecker-ci.org/woodpecker/v2/cli/cron"
"go.woodpecker-ci.org/woodpecker/v2/cli/deploy"
@ -25,9 +26,10 @@ import (
"go.woodpecker-ci.org/woodpecker/v2/cli/lint"
"go.woodpecker-ci.org/woodpecker/v2/cli/log"
"go.woodpecker-ci.org/woodpecker/v2/cli/loglevel"
"go.woodpecker-ci.org/woodpecker/v2/cli/org"
"go.woodpecker-ci.org/woodpecker/v2/cli/pipeline"
"go.woodpecker-ci.org/woodpecker/v2/cli/registry"
"go.woodpecker-ci.org/woodpecker/v2/cli/repo"
"go.woodpecker-ci.org/woodpecker/v2/cli/repo/registry"
"go.woodpecker-ci.org/woodpecker/v2/cli/secret"
"go.woodpecker-ci.org/woodpecker/v2/cli/setup"
"go.woodpecker-ci.org/woodpecker/v2/cli/update"
@ -47,14 +49,17 @@ func newApp() *cli.App {
app.After = common.After
app.Suggest = true
app.Commands = []*cli.Command{
admin.Command,
org.Command,
repo.Command,
pipeline.Command,
log.Command,
deploy.Command,
exec.Command,
info.Command,
// TODO: Remove in 3.x
registry.Command,
secret.Command,
repo.Command,
user.Command,
lint.Command,
loglevel.Command,

View file

@ -1123,6 +1123,233 @@ const docTemplate = `{
}
}
},
"/orgs/{org_id}/registries": {
"get": {
"produces": [
"application/json"
],
"tags": [
"Organization registries"
],
"summary": "List organization registries",
"parameters": [
{
"type": "string",
"default": "Bearer \u003cpersonal access token\u003e",
"description": "Insert your personal access token",
"name": "Authorization",
"in": "header",
"required": true
},
{
"type": "string",
"description": "the org's id",
"name": "org_id",
"in": "path",
"required": true
},
{
"type": "integer",
"default": 1,
"description": "for response pagination, page offset number",
"name": "page",
"in": "query"
},
{
"type": "integer",
"default": 50,
"description": "for response pagination, max items per page",
"name": "perPage",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/Registry"
}
}
}
}
},
"post": {
"produces": [
"application/json"
],
"tags": [
"Organization registries"
],
"summary": "Create an organization registry",
"parameters": [
{
"type": "string",
"default": "Bearer \u003cpersonal access token\u003e",
"description": "Insert your personal access token",
"name": "Authorization",
"in": "header",
"required": true
},
{
"type": "string",
"description": "the org's id",
"name": "org_id",
"in": "path",
"required": true
},
{
"description": "the new registry",
"name": "registryData",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/Registry"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/Registry"
}
}
}
}
},
"/orgs/{org_id}/registries/{registry}": {
"get": {
"produces": [
"application/json"
],
"tags": [
"Organization registries"
],
"summary": "Get a organization registry by address",
"parameters": [
{
"type": "string",
"default": "Bearer \u003cpersonal access token\u003e",
"description": "Insert your personal access token",
"name": "Authorization",
"in": "header",
"required": true
},
{
"type": "string",
"description": "the org's id",
"name": "org_id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "the registry's address",
"name": "registry",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/Registry"
}
}
}
},
"delete": {
"produces": [
"text/plain"
],
"tags": [
"Organization registries"
],
"summary": "Delete an organization registry by name",
"parameters": [
{
"type": "string",
"default": "Bearer \u003cpersonal access token\u003e",
"description": "Insert your personal access token",
"name": "Authorization",
"in": "header",
"required": true
},
{
"type": "string",
"description": "the org's id",
"name": "org_id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "the registry's name",
"name": "registry",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
}
}
},
"patch": {
"produces": [
"application/json"
],
"tags": [
"Organization registries"
],
"summary": "Update an organization registry by name",
"parameters": [
{
"type": "string",
"default": "Bearer \u003cpersonal access token\u003e",
"description": "Insert your personal access token",
"name": "Authorization",
"in": "header",
"required": true
},
{
"type": "string",
"description": "the org's id",
"name": "org_id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "the registry's name",
"name": "registry",
"in": "path",
"required": true
},
{
"description": "the update registry data",
"name": "registryData",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/Registry"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/Registry"
}
}
}
}
},
"/orgs/{org_id}/secrets": {
"get": {
"produces": [
@ -1493,6 +1720,198 @@ const docTemplate = `{
}
}
},
"/registries": {
"get": {
"produces": [
"application/json"
],
"tags": [
"Registries"
],
"summary": "List global registries",
"parameters": [
{
"type": "string",
"default": "Bearer \u003cpersonal access token\u003e",
"description": "Insert your personal access token",
"name": "Authorization",
"in": "header",
"required": true
},
{
"type": "integer",
"default": 1,
"description": "for response pagination, page offset number",
"name": "page",
"in": "query"
},
{
"type": "integer",
"default": 50,
"description": "for response pagination, max items per page",
"name": "perPage",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/Registry"
}
}
}
}
},
"post": {
"produces": [
"application/json"
],
"tags": [
"Registries"
],
"summary": "Create a global registry",
"parameters": [
{
"type": "string",
"default": "Bearer \u003cpersonal access token\u003e",
"description": "Insert your personal access token",
"name": "Authorization",
"in": "header",
"required": true
},
{
"description": "the registry object data",
"name": "registry",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/Registry"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/Registry"
}
}
}
}
},
"/registries/{registry}": {
"get": {
"produces": [
"application/json"
],
"tags": [
"Registries"
],
"summary": "Get a global registry by name",
"parameters": [
{
"type": "string",
"default": "Bearer \u003cpersonal access token\u003e",
"description": "Insert your personal access token",
"name": "Authorization",
"in": "header",
"required": true
},
{
"type": "string",
"description": "the registry's name",
"name": "registry",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/Registry"
}
}
}
},
"delete": {
"produces": [
"text/plain"
],
"tags": [
"Registries"
],
"summary": "Delete a global registry by name",
"parameters": [
{
"type": "string",
"default": "Bearer \u003cpersonal access token\u003e",
"description": "Insert your personal access token",
"name": "Authorization",
"in": "header",
"required": true
},
{
"type": "string",
"description": "the registry's name",
"name": "registry",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
}
}
},
"patch": {
"produces": [
"application/json"
],
"tags": [
"Registries"
],
"summary": "Update a global registry by name",
"parameters": [
{
"type": "string",
"default": "Bearer \u003cpersonal access token\u003e",
"description": "Insert your personal access token",
"name": "Authorization",
"in": "header",
"required": true
},
{
"type": "string",
"description": "the registry's name",
"name": "registry",
"in": "path",
"required": true
},
{
"description": "the registry's data",
"name": "registryData",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/Registry"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/Registry"
}
}
}
}
},
"/repos": {
"get": {
"description": "Returns a list of all repositories. Requires admin rights.",
@ -2799,7 +3218,7 @@ const docTemplate = `{
}
}
},
"/repos/{repo_id}/registry": {
"/repos/{repo_id}/registries": {
"get": {
"produces": [
"application/json"
@ -2895,7 +3314,7 @@ const docTemplate = `{
}
}
},
"/repos/{repo_id}/registry/{registry}": {
"/repos/{repo_id}/registries/{registry}": {
"get": {
"produces": [
"application/json"
@ -4376,9 +4795,18 @@ const docTemplate = `{
"id": {
"type": "integer"
},
"org_id": {
"type": "integer"
},
"password": {
"type": "string"
},
"readonly": {
"type": "boolean"
},
"repo_id": {
"type": "integer"
},
"username": {
"type": "string"
}

View file

@ -15,7 +15,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.34.1
// protoc-gen-go v1.34.2
// protoc v4.25.1
// source: woodpecker.proto
@ -1312,7 +1312,7 @@ func file_woodpecker_proto_rawDescGZIP() []byte {
}
var file_woodpecker_proto_msgTypes = make([]protoimpl.MessageInfo, 21)
var file_woodpecker_proto_goTypes = []interface{}{
var file_woodpecker_proto_goTypes = []any{
(*StepState)(nil), // 0: proto.StepState
(*WorkflowState)(nil), // 1: proto.WorkflowState
(*LogEntry)(nil), // 2: proto.LogEntry
@ -1380,7 +1380,7 @@ func file_woodpecker_proto_init() {
return
}
if !protoimpl.UnsafeEnabled {
file_woodpecker_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
file_woodpecker_proto_msgTypes[0].Exporter = func(v any, i int) any {
switch v := v.(*StepState); i {
case 0:
return &v.state
@ -1392,7 +1392,7 @@ func file_woodpecker_proto_init() {
return nil
}
}
file_woodpecker_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
file_woodpecker_proto_msgTypes[1].Exporter = func(v any, i int) any {
switch v := v.(*WorkflowState); i {
case 0:
return &v.state
@ -1404,7 +1404,7 @@ func file_woodpecker_proto_init() {
return nil
}
}
file_woodpecker_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
file_woodpecker_proto_msgTypes[2].Exporter = func(v any, i int) any {
switch v := v.(*LogEntry); i {
case 0:
return &v.state
@ -1416,7 +1416,7 @@ func file_woodpecker_proto_init() {
return nil
}
}
file_woodpecker_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
file_woodpecker_proto_msgTypes[3].Exporter = func(v any, i int) any {
switch v := v.(*Filter); i {
case 0:
return &v.state
@ -1428,7 +1428,7 @@ func file_woodpecker_proto_init() {
return nil
}
}
file_woodpecker_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
file_woodpecker_proto_msgTypes[4].Exporter = func(v any, i int) any {
switch v := v.(*Workflow); i {
case 0:
return &v.state
@ -1440,7 +1440,7 @@ func file_woodpecker_proto_init() {
return nil
}
}
file_woodpecker_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
file_woodpecker_proto_msgTypes[5].Exporter = func(v any, i int) any {
switch v := v.(*NextRequest); i {
case 0:
return &v.state
@ -1452,7 +1452,7 @@ func file_woodpecker_proto_init() {
return nil
}
}
file_woodpecker_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
file_woodpecker_proto_msgTypes[6].Exporter = func(v any, i int) any {
switch v := v.(*InitRequest); i {
case 0:
return &v.state
@ -1464,7 +1464,7 @@ func file_woodpecker_proto_init() {
return nil
}
}
file_woodpecker_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} {
file_woodpecker_proto_msgTypes[7].Exporter = func(v any, i int) any {
switch v := v.(*WaitRequest); i {
case 0:
return &v.state
@ -1476,7 +1476,7 @@ func file_woodpecker_proto_init() {
return nil
}
}
file_woodpecker_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} {
file_woodpecker_proto_msgTypes[8].Exporter = func(v any, i int) any {
switch v := v.(*DoneRequest); i {
case 0:
return &v.state
@ -1488,7 +1488,7 @@ func file_woodpecker_proto_init() {
return nil
}
}
file_woodpecker_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} {
file_woodpecker_proto_msgTypes[9].Exporter = func(v any, i int) any {
switch v := v.(*ExtendRequest); i {
case 0:
return &v.state
@ -1500,7 +1500,7 @@ func file_woodpecker_proto_init() {
return nil
}
}
file_woodpecker_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} {
file_woodpecker_proto_msgTypes[10].Exporter = func(v any, i int) any {
switch v := v.(*UpdateRequest); i {
case 0:
return &v.state
@ -1512,7 +1512,7 @@ func file_woodpecker_proto_init() {
return nil
}
}
file_woodpecker_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} {
file_woodpecker_proto_msgTypes[11].Exporter = func(v any, i int) any {
switch v := v.(*LogRequest); i {
case 0:
return &v.state
@ -1524,7 +1524,7 @@ func file_woodpecker_proto_init() {
return nil
}
}
file_woodpecker_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} {
file_woodpecker_proto_msgTypes[12].Exporter = func(v any, i int) any {
switch v := v.(*Empty); i {
case 0:
return &v.state
@ -1536,7 +1536,7 @@ func file_woodpecker_proto_init() {
return nil
}
}
file_woodpecker_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} {
file_woodpecker_proto_msgTypes[13].Exporter = func(v any, i int) any {
switch v := v.(*ReportHealthRequest); i {
case 0:
return &v.state
@ -1548,7 +1548,7 @@ func file_woodpecker_proto_init() {
return nil
}
}
file_woodpecker_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} {
file_woodpecker_proto_msgTypes[14].Exporter = func(v any, i int) any {
switch v := v.(*RegisterAgentRequest); i {
case 0:
return &v.state
@ -1560,7 +1560,7 @@ func file_woodpecker_proto_init() {
return nil
}
}
file_woodpecker_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} {
file_woodpecker_proto_msgTypes[15].Exporter = func(v any, i int) any {
switch v := v.(*VersionResponse); i {
case 0:
return &v.state
@ -1572,7 +1572,7 @@ func file_woodpecker_proto_init() {
return nil
}
}
file_woodpecker_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} {
file_woodpecker_proto_msgTypes[16].Exporter = func(v any, i int) any {
switch v := v.(*NextResponse); i {
case 0:
return &v.state
@ -1584,7 +1584,7 @@ func file_woodpecker_proto_init() {
return nil
}
}
file_woodpecker_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} {
file_woodpecker_proto_msgTypes[17].Exporter = func(v any, i int) any {
switch v := v.(*RegisterAgentResponse); i {
case 0:
return &v.state
@ -1596,7 +1596,7 @@ func file_woodpecker_proto_init() {
return nil
}
}
file_woodpecker_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} {
file_woodpecker_proto_msgTypes[18].Exporter = func(v any, i int) any {
switch v := v.(*AuthRequest); i {
case 0:
return &v.state
@ -1608,7 +1608,7 @@ func file_woodpecker_proto_init() {
return nil
}
}
file_woodpecker_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} {
file_woodpecker_proto_msgTypes[19].Exporter = func(v any, i int) any {
switch v := v.(*AuthResponse); i {
case 0:
return &v.state

View file

@ -0,0 +1,170 @@
// Copyright 2024 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package api
import (
"net/http"
"github.com/gin-gonic/gin"
"go.woodpecker-ci.org/woodpecker/v2/server"
"go.woodpecker-ci.org/woodpecker/v2/server/model"
"go.woodpecker-ci.org/woodpecker/v2/server/router/middleware/session"
)
// GetGlobalRegistryList
//
// @Summary List global registries
// @Router /registries [get]
// @Produce json
// @Success 200 {array} Registry
// @Tags Registries
// @Param Authorization header string true "Insert your personal access token" default(Bearer <personal access token>)
// @Param page query int false "for response pagination, page offset number" default(1)
// @Param perPage query int false "for response pagination, max items per page" default(50)
func GetGlobalRegistryList(c *gin.Context) {
registryService := server.Config.Services.Manager.RegistryService()
list, err := registryService.GlobalRegistryList(session.Pagination(c))
if err != nil {
c.String(http.StatusInternalServerError, "Error getting global registry list. %s", err)
return
}
// copy the registry detail to remove the sensitive
// password and token fields.
for i, registry := range list {
list[i] = registry.Copy()
}
c.JSON(http.StatusOK, list)
}
// GetGlobalRegistry
//
// @Summary Get a global registry by name
// @Router /registries/{registry} [get]
// @Produce json
// @Success 200 {object} Registry
// @Tags Registries
// @Param Authorization header string true "Insert your personal access token" default(Bearer <personal access token>)
// @Param registry path string true "the registry's name"
func GetGlobalRegistry(c *gin.Context) {
addr := c.Param("registry")
registryService := server.Config.Services.Manager.RegistryService()
registry, err := registryService.GlobalRegistryFind(addr)
if err != nil {
handleDBError(c, err)
return
}
c.JSON(http.StatusOK, registry.Copy())
}
// PostGlobalRegistry
//
// @Summary Create a global registry
// @Router /registries [post]
// @Produce json
// @Success 200 {object} Registry
// @Tags Registries
// @Param Authorization header string true "Insert your personal access token" default(Bearer <personal access token>)
// @Param registry body Registry true "the registry object data"
func PostGlobalRegistry(c *gin.Context) {
in := new(model.Registry)
if err := c.Bind(in); err != nil {
c.String(http.StatusBadRequest, "Error parsing global registry. %s", err)
return
}
registry := &model.Registry{
Address: in.Address,
Username: in.Username,
Password: in.Password,
}
if err := registry.Validate(); err != nil {
c.String(http.StatusBadRequest, "Error inserting global registry. %s", err)
return
}
registryService := server.Config.Services.Manager.RegistryService()
if err := registryService.GlobalRegistryCreate(registry); err != nil {
c.String(http.StatusInternalServerError, "Error inserting global registry %q. %s", in.Address, err)
return
}
c.JSON(http.StatusOK, registry.Copy())
}
// PatchGlobalRegistry
//
// @Summary Update a global registry by name
// @Router /registries/{registry} [patch]
// @Produce json
// @Success 200 {object} Registry
// @Tags Registries
// @Param Authorization header string true "Insert your personal access token" default(Bearer <personal access token>)
// @Param registry path string true "the registry's name"
// @Param registryData body Registry true "the registry's data"
func PatchGlobalRegistry(c *gin.Context) {
addr := c.Param("registry")
in := new(model.Registry)
err := c.Bind(in)
if err != nil {
c.String(http.StatusBadRequest, "Error parsing registry. %s", err)
return
}
registryService := server.Config.Services.Manager.RegistryService()
registry, err := registryService.GlobalRegistryFind(addr)
if err != nil {
handleDBError(c, err)
return
}
if in.Address != "" {
registry.Address = in.Address
}
if in.Username != "" {
registry.Username = in.Username
}
if in.Password != "" {
registry.Password = in.Password
}
if err := registry.Validate(); err != nil {
c.String(http.StatusBadRequest, "Error updating global registry. %s", err)
return
}
if err := registryService.GlobalRegistryUpdate(registry); err != nil {
c.String(http.StatusInternalServerError, "Error updating global registry %q. %s", in.Address, err)
return
}
c.JSON(http.StatusOK, registry.Copy())
}
// DeleteGlobalRegistry
//
// @Summary Delete a global registry by name
// @Router /registries/{registry} [delete]
// @Produce plain
// @Success 204
// @Tags Registries
// @Param Authorization header string true "Insert your personal access token" default(Bearer <personal access token>)
// @Param registry path string true "the registry's name"
func DeleteGlobalRegistry(c *gin.Context) {
addr := c.Param("registry")
registryService := server.Config.Services.Manager.RegistryService()
if err := registryService.GlobalRegistryDelete(addr); err != nil {
handleDBError(c, err)
return
}
c.Status(http.StatusNoContent)
}

207
server/api/org_registry.go Normal file
View file

@ -0,0 +1,207 @@
// Copyright 2024 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package api
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"go.woodpecker-ci.org/woodpecker/v2/server"
"go.woodpecker-ci.org/woodpecker/v2/server/model"
"go.woodpecker-ci.org/woodpecker/v2/server/router/middleware/session"
)
// GetOrgRegistry
//
// @Summary Get a organization registry by address
// @Router /orgs/{org_id}/registries/{registry} [get]
// @Produce json
// @Success 200 {object} Registry
// @Tags Organization registries
// @Param Authorization header string true "Insert your personal access token" default(Bearer <personal access token>)
// @Param org_id path string true "the org's id"
// @Param registry path string true "the registry's address"
func GetOrgRegistry(c *gin.Context) {
addr := c.Param("registry")
orgID, err := strconv.ParseInt(c.Param("org_id"), 10, 64)
if err != nil {
c.String(http.StatusBadRequest, "Error parsing org id. %s", err)
return
}
registryService := server.Config.Services.Manager.RegistryService()
registry, err := registryService.OrgRegistryFind(orgID, addr)
if err != nil {
handleDBError(c, err)
return
}
c.JSON(http.StatusOK, registry.Copy())
}
// GetOrgRegistryList
//
// @Summary List organization registries
// @Router /orgs/{org_id}/registries [get]
// @Produce json
// @Success 200 {array} Registry
// @Tags Organization registries
// @Param Authorization header string true "Insert your personal access token" default(Bearer <personal access token>)
// @Param org_id path string true "the org's id"
// @Param page query int false "for response pagination, page offset number" default(1)
// @Param perPage query int false "for response pagination, max items per page" default(50)
func GetOrgRegistryList(c *gin.Context) {
orgID, err := strconv.ParseInt(c.Param("org_id"), 10, 64)
if err != nil {
c.String(http.StatusBadRequest, "Error parsing org id. %s", err)
return
}
registryService := server.Config.Services.Manager.RegistryService()
list, err := registryService.OrgRegistryList(orgID, session.Pagination(c))
if err != nil {
c.String(http.StatusInternalServerError, "Error getting registry list for %q. %s", orgID, err)
return
}
// copy the registry detail to remove the sensitive
// password and token fields.
for i, registry := range list {
list[i] = registry.Copy()
}
c.JSON(http.StatusOK, list)
}
// PostOrgRegistry
//
// @Summary Create an organization registry
// @Router /orgs/{org_id}/registries [post]
// @Produce json
// @Success 200 {object} Registry
// @Tags Organization registries
// @Param Authorization header string true "Insert your personal access token" default(Bearer <personal access token>)
// @Param org_id path string true "the org's id"
// @Param registryData body Registry true "the new registry"
func PostOrgRegistry(c *gin.Context) {
orgID, err := strconv.ParseInt(c.Param("org_id"), 10, 64)
if err != nil {
c.String(http.StatusBadRequest, "Error parsing org id. %s", err)
return
}
in := new(model.Registry)
if err := c.Bind(in); err != nil {
c.String(http.StatusBadRequest, "Error parsing org %q registry. %s", orgID, err)
return
}
registry := &model.Registry{
OrgID: orgID,
Address: in.Address,
Username: in.Username,
Password: in.Password,
}
if err := registry.Validate(); err != nil {
c.String(http.StatusUnprocessableEntity, "Error inserting org %q registry. %s", orgID, err)
return
}
registryService := server.Config.Services.Manager.RegistryService()
if err := registryService.OrgRegistryCreate(orgID, registry); err != nil {
c.String(http.StatusInternalServerError, "Error inserting org %q registry %q. %s", orgID, in.Address, err)
return
}
c.JSON(http.StatusOK, registry.Copy())
}
// PatchOrgRegistry
//
// @Summary Update an organization registry by name
// @Router /orgs/{org_id}/registries/{registry} [patch]
// @Produce json
// @Success 200 {object} Registry
// @Tags Organization registries
// @Param Authorization header string true "Insert your personal access token" default(Bearer <personal access token>)
// @Param org_id path string true "the org's id"
// @Param registry path string true "the registry's name"
// @Param registryData body Registry true "the update registry data"
func PatchOrgRegistry(c *gin.Context) {
addr := c.Param("registry")
orgID, err := strconv.ParseInt(c.Param("org_id"), 10, 64)
if err != nil {
c.String(http.StatusBadRequest, "Error parsing org id. %s", err)
return
}
in := new(model.Registry)
err = c.Bind(in)
if err != nil {
c.String(http.StatusBadRequest, "Error parsing registry. %s", err)
return
}
registryService := server.Config.Services.Manager.RegistryService()
registry, err := registryService.OrgRegistryFind(orgID, addr)
if err != nil {
handleDBError(c, err)
return
}
if in.Address != "" {
registry.Address = in.Address
}
if in.Username != "" {
registry.Username = in.Username
}
if in.Password != "" {
registry.Password = in.Password
}
if err := registry.Validate(); err != nil {
c.String(http.StatusUnprocessableEntity, "Error updating org %q registry. %s", orgID, err)
return
}
if err := registryService.OrgRegistryUpdate(orgID, registry); err != nil {
c.String(http.StatusInternalServerError, "Error updating org %q registry %q. %s", orgID, in.Address, err)
return
}
c.JSON(http.StatusOK, registry.Copy())
}
// DeleteOrgRegistry
//
// @Summary Delete an organization registry by name
// @Router /orgs/{org_id}/registries/{registry} [delete]
// @Produce plain
// @Success 204
// @Tags Organization registries
// @Param Authorization header string true "Insert your personal access token" default(Bearer <personal access token>)
// @Param org_id path string true "the org's id"
// @Param registry path string true "the registry's name"
func DeleteOrgRegistry(c *gin.Context) {
addr := c.Param("registry")
orgID, err := strconv.ParseInt(c.Param("org_id"), 10, 64)
if err != nil {
c.String(http.StatusBadRequest, "Error parsing org id. %s", err)
return
}
registryService := server.Config.Services.Manager.RegistryService()
if err := registryService.OrgRegistryDelete(orgID, addr); err != nil {
handleDBError(c, err)
return
}
c.Status(http.StatusNoContent)
}

View file

@ -27,7 +27,7 @@ import (
// GetRegistry
//
// @Summary Get a registry by name
// @Router /repos/{repo_id}/registry/{registry} [get]
// @Router /repos/{repo_id}/registries/{registry} [get]
// @Produce json
// @Success 200 {object} Registry
// @Tags Repository registries
@ -36,10 +36,10 @@ import (
// @Param registry path string true "the registry name"
func GetRegistry(c *gin.Context) {
repo := session.Repo(c)
name := c.Param("registry")
addr := c.Param("registry")
registryService := server.Config.Services.Manager.RegistryServiceFromRepo(repo)
registry, err := registryService.RegistryFind(repo, name)
registry, err := registryService.RegistryFind(repo, addr)
if err != nil {
handleDBError(c, err)
return
@ -50,7 +50,7 @@ func GetRegistry(c *gin.Context) {
// PostRegistry
//
// @Summary Create a registry
// @Router /repos/{repo_id}/registry [post]
// @Router /repos/{repo_id}/registries [post]
// @Produce json
// @Success 200 {object} Registry
// @Tags Repository registries
@ -87,7 +87,7 @@ func PostRegistry(c *gin.Context) {
// PatchRegistry
//
// @Summary Update a registry by name
// @Router /repos/{repo_id}/registry/{registry} [patch]
// @Router /repos/{repo_id}/registries/{registry} [patch]
// @Produce json
// @Success 200 {object} Registry
// @Tags Repository registries
@ -96,10 +96,8 @@ func PostRegistry(c *gin.Context) {
// @Param registry path string true "the registry name"
// @Param registryData body Registry true "the attributes for the registry"
func PatchRegistry(c *gin.Context) {
var (
repo = session.Repo(c)
name = c.Param("registry")
)
repo := session.Repo(c)
addr := c.Param("registry")
in := new(model.Registry)
err := c.Bind(in)
@ -109,7 +107,7 @@ func PatchRegistry(c *gin.Context) {
}
registryService := server.Config.Services.Manager.RegistryServiceFromRepo(repo)
registry, err := registryService.RegistryFind(repo, name)
registry, err := registryService.RegistryFind(repo, addr)
if err != nil {
handleDBError(c, err)
return
@ -135,7 +133,7 @@ func PatchRegistry(c *gin.Context) {
// GetRegistryList
//
// @Summary List registries
// @Router /repos/{repo_id}/registry [get]
// @Router /repos/{repo_id}/registries [get]
// @Produce json
// @Success 200 {array} Registry
// @Tags Repository registries
@ -162,7 +160,7 @@ func GetRegistryList(c *gin.Context) {
// DeleteRegistry
//
// @Summary Delete a registry by name
// @Router /repos/{repo_id}/registry/{registry} [delete]
// @Router /repos/{repo_id}/registries/{registry} [delete]
// @Produce plain
// @Success 204
// @Tags Repository registries
@ -171,10 +169,10 @@ func GetRegistryList(c *gin.Context) {
// @Param registry path string true "the registry name"
func DeleteRegistry(c *gin.Context) {
repo := session.Repo(c)
name := c.Param("registry")
addr := c.Param("registry")
registryService := server.Config.Services.Manager.RegistryServiceFromRepo(repo)
err := registryService.RegistryDelete(repo, name)
err := registryService.RegistryDelete(repo, addr)
if err != nil {
handleDBError(c, err)
return

View file

@ -29,16 +29,33 @@ var (
// Registry represents a docker registry with credentials.
type Registry struct {
ID int64 `json:"id" xorm:"pk autoincr 'id'"`
RepoID int64 `json:"-" xorm:"UNIQUE(s) INDEX 'repo_id'"`
Address string `json:"address" xorm:"UNIQUE(s) INDEX 'address'"`
OrgID int64 `json:"org_id" xorm:"NOT NULL DEFAULT 0 UNIQUE(s) INDEX 'org_id'"`
RepoID int64 `json:"repo_id" xorm:"NOT NULL DEFAULT 0 UNIQUE(s) INDEX 'repo_id'"`
Address string `json:"address" xorm:"NOT NULL UNIQUE(s) INDEX 'address'"`
Username string `json:"username" xorm:"varchar(2000) 'username'"`
Password string `json:"password" xorm:"TEXT 'password'"`
ReadOnly bool `json:"readonly" xorm:"-"`
} // @name Registry
func (r Registry) TableName() string {
return "registries"
}
// Global registry.
func (r Registry) IsGlobal() bool {
return r.RepoID == 0 && r.OrgID == 0
}
// Organization registry.
func (r Registry) IsOrganization() bool {
return r.RepoID == 0 && r.OrgID != 0
}
// Repository registry.
func (r Registry) IsRepository() bool {
return r.RepoID != 0 && r.OrgID == 0
}
// Validate validates the registry information.
func (r *Registry) Validate() error {
switch {
@ -58,8 +75,10 @@ func (r *Registry) Validate() error {
func (r *Registry) Copy() *Registry {
return &Registry{
ID: r.ID,
OrgID: r.OrgID,
RepoID: r.RepoID,
Address: r.Address,
Username: r.Username,
ReadOnly: r.ReadOnly,
}
}

View file

@ -44,13 +44,13 @@ func parsePipeline(forge forge.Forge, store store.Store, currentPipeline *model.
}
secretService := server.Config.Services.Manager.SecretServiceFromRepo(repo)
secs, err := secretService.SecretListPipeline(repo, currentPipeline, &model.ListOptions{All: true})
secs, err := secretService.SecretListPipeline(repo, currentPipeline)
if err != nil {
log.Error().Err(err).Msgf("error getting secrets for %s#%d", repo.FullName, currentPipeline.Number)
}
registryService := server.Config.Services.Manager.RegistryServiceFromRepo(repo)
regs, err := registryService.RegistryList(repo, &model.ListOptions{All: true})
regs, err := registryService.RegistryListPipeline(repo, currentPipeline)
if err != nil {
log.Error().Err(err).Msgf("error getting registry credentials for %s#%d", repo.FullName, currentPipeline.Number)
}

View file

@ -61,11 +61,18 @@ func apiRoutes(e *gin.RouterGroup) {
org.Use(session.MustOrgMember(true))
org.DELETE("", session.MustAdmin(), api.DeleteOrg)
org.GET("", api.GetOrg)
org.GET("/secrets", api.GetOrgSecretList)
org.POST("/secrets", api.PostOrgSecret)
org.GET("/secrets/:secret", api.GetOrgSecret)
org.PATCH("/secrets/:secret", api.PatchOrgSecret)
org.DELETE("/secrets/:secret", api.DeleteOrgSecret)
org.GET("/registries", api.GetOrgRegistryList)
org.POST("/registries", api.PostOrgRegistry)
org.GET("/registries/:registry", api.GetOrgRegistry)
org.PATCH("/registries/:registry", api.PatchOrgRegistry)
org.DELETE("/registries/:registry", api.DeleteOrgRegistry)
}
}
}
@ -118,6 +125,13 @@ func apiRoutes(e *gin.RouterGroup) {
repo.DELETE("/secrets/:secret", session.MustPush, api.DeleteSecret)
// requires push permissions
repo.GET("/registries", session.MustPush, api.GetRegistryList)
repo.POST("/registries", session.MustPush, api.PostRegistry)
repo.GET("/registries/:registry", session.MustPush, api.GetRegistry)
repo.PATCH("/registries/:registry", session.MustPush, api.PatchRegistry)
repo.DELETE("/registries/:registry", session.MustPush, api.DeleteRegistry)
// TODO: remove with 3.x
repo.GET("/registry", session.MustPush, api.GetRegistryList)
repo.POST("/registry", session.MustPush, api.PostRegistry)
repo.GET("/registry/:registry", session.MustPush, api.GetRegistry)
@ -184,6 +198,21 @@ func apiRoutes(e *gin.RouterGroup) {
secrets.DELETE("/:secret", api.DeleteGlobalSecret)
}
// global registries can be read without actual values by any user
readGlobalRegistries := apiBase.Group("/registries")
{
readGlobalRegistries.Use(session.MustUser())
readGlobalRegistries.GET("", api.GetGlobalRegistryList)
readGlobalRegistries.GET("/:registry", api.GetGlobalRegistry)
}
registries := apiBase.Group("/registries")
{
registries.Use(session.MustAdmin())
registries.POST("", api.PostGlobalRegistry)
registries.PATCH("/:registry", api.PatchGlobalRegistry)
registries.DELETE("/:registry", api.DeleteGlobalRegistry)
}
logLevel := apiBase.Group("/log-level")
{
logLevel.Use(session.MustAdmin())

View file

@ -15,7 +15,10 @@
package registry
import (
"errors"
"go.woodpecker-ci.org/woodpecker/v2/server/model"
"go.woodpecker-ci.org/woodpecker/v2/server/store/types"
)
type combined struct {
@ -31,29 +34,44 @@ func NewCombined(dbRegistry Service, registries ...ReadOnlyService) Service {
}
}
func (c *combined) RegistryFind(repo *model.Repo, name string) (*model.Registry, error) {
for _, registry := range c.registries {
res, err := registry.RegistryFind(repo, name)
if err != nil {
return nil, err
}
if res != nil {
return res, nil
}
}
return nil, nil
func (c *combined) RegistryFind(repo *model.Repo, addr string) (*model.Registry, error) {
return c.dbRegistry.RegistryFind(repo, addr)
}
func (c *combined) RegistryList(repo *model.Repo, p *model.ListOptions) ([]*model.Registry, error) {
var registries []*model.Registry
for _, registry := range c.registries {
list, err := registry.RegistryList(repo, &model.ListOptions{All: true})
return c.dbRegistry.RegistryList(repo, p)
}
func (c *combined) RegistryListPipeline(repo *model.Repo, pipeline *model.Pipeline) ([]*model.Registry, error) {
dbRegistries, err := c.dbRegistry.RegistryListPipeline(repo, pipeline)
if err != nil {
return nil, err
}
registries = append(registries, list...)
registries := make([]*model.Registry, 0, len(dbRegistries))
exists := make(map[string]struct{}, len(dbRegistries))
// Assign database stored registries to the map to avoid duplicates
// from the combined registries so to prioritize ones in database.
for _, reg := range dbRegistries {
exists[reg.Address] = struct{}{}
}
return model.ApplyPagination(p, registries), nil
for _, registry := range c.registries {
list, err := registry.GlobalRegistryList(&model.ListOptions{All: true})
if err != nil {
return nil, err
}
for _, reg := range list {
if _, ok := exists[reg.Address]; ok {
continue
}
exists[reg.Address] = struct{}{}
registries = append(registries, reg)
}
}
return append(registries, dbRegistries...), nil
}
func (c *combined) RegistryCreate(repo *model.Repo, registry *model.Registry) error {
@ -64,6 +82,86 @@ func (c *combined) RegistryUpdate(repo *model.Repo, registry *model.Registry) er
return c.dbRegistry.RegistryUpdate(repo, registry)
}
func (c *combined) RegistryDelete(repo *model.Repo, name string) error {
return c.dbRegistry.RegistryDelete(repo, name)
func (c *combined) RegistryDelete(repo *model.Repo, addr string) error {
return c.dbRegistry.RegistryDelete(repo, addr)
}
func (c *combined) OrgRegistryFind(owner int64, addr string) (*model.Registry, error) {
return c.dbRegistry.OrgRegistryFind(owner, addr)
}
func (c *combined) OrgRegistryList(owner int64, p *model.ListOptions) ([]*model.Registry, error) {
return c.dbRegistry.OrgRegistryList(owner, p)
}
func (c *combined) OrgRegistryCreate(owner int64, registry *model.Registry) error {
return c.dbRegistry.OrgRegistryCreate(owner, registry)
}
func (c *combined) OrgRegistryUpdate(owner int64, registry *model.Registry) error {
return c.dbRegistry.OrgRegistryUpdate(owner, registry)
}
func (c *combined) OrgRegistryDelete(owner int64, addr string) error {
return c.dbRegistry.OrgRegistryDelete(owner, addr)
}
func (c *combined) GlobalRegistryFind(addr string) (*model.Registry, error) {
registry, err := c.dbRegistry.GlobalRegistryFind(addr)
if err != nil && !errors.Is(err, types.RecordNotExist) {
return nil, err
}
if registry != nil {
return registry, nil
}
for _, reg := range c.registries {
if registry, err := reg.GlobalRegistryFind(addr); err == nil {
return registry, nil
}
}
return nil, types.RecordNotExist
}
func (c *combined) GlobalRegistryList(p *model.ListOptions) ([]*model.Registry, error) {
dbRegistries, err := c.dbRegistry.GlobalRegistryList(&model.ListOptions{All: true})
if err != nil {
return nil, err
}
registries := make([]*model.Registry, 0, len(dbRegistries))
exists := make(map[string]struct{}, len(dbRegistries))
// Assign database stored registries to the map to avoid duplicates
// from the combined registries so to prioritize ones in database.
for _, reg := range dbRegistries {
exists[reg.Address] = struct{}{}
}
for _, registry := range c.registries {
list, err := registry.GlobalRegistryList(&model.ListOptions{All: true})
if err != nil {
return nil, err
}
for _, reg := range list {
if _, ok := exists[reg.Address]; ok {
continue
}
exists[reg.Address] = struct{}{}
registries = append(registries, reg)
}
}
return model.ApplyPagination(p, append(registries, dbRegistries...)), nil
}
func (c *combined) GlobalRegistryCreate(registry *model.Registry) error {
return c.dbRegistry.GlobalRegistryCreate(registry)
}
func (c *combined) GlobalRegistryUpdate(registry *model.Registry) error {
return c.dbRegistry.GlobalRegistryUpdate(registry)
}
func (c *combined) GlobalRegistryDelete(addr string) error {
return c.dbRegistry.GlobalRegistryDelete(addr)
}

View file

@ -28,12 +28,45 @@ func NewDB(store store.Store) Service {
return &db{store}
}
func (d *db) RegistryFind(repo *model.Repo, name string) (*model.Registry, error) {
return d.store.RegistryFind(repo, name)
func (d *db) RegistryFind(repo *model.Repo, addr string) (*model.Registry, error) {
return d.store.RegistryFind(repo, addr)
}
func (d *db) RegistryList(repo *model.Repo, p *model.ListOptions) ([]*model.Registry, error) {
return d.store.RegistryList(repo, p)
return d.store.RegistryList(repo, false, p)
}
func (d *db) RegistryListPipeline(repo *model.Repo, _ *model.Pipeline) ([]*model.Registry, error) {
r, err := d.store.RegistryList(repo, true, &model.ListOptions{All: true})
if err != nil {
return nil, err
}
// Return only registries with unique address
// Priority order in case of duplicate addresses are repository, user/organization, global
registries := make([]*model.Registry, 0, len(r))
uniq := make(map[string]struct{})
for _, condition := range []struct {
IsRepository bool
IsOrganization bool
IsGlobal bool
}{
{IsRepository: true},
{IsOrganization: true},
{IsGlobal: true},
} {
for _, registry := range r {
if registry.IsRepository() != condition.IsRepository || registry.IsOrganization() != condition.IsOrganization || registry.IsGlobal() != condition.IsGlobal {
continue
}
if _, ok := uniq[registry.Address]; ok {
continue
}
uniq[registry.Address] = struct{}{}
registries = append(registries, registry)
}
}
return registries, nil
}
func (d *db) RegistryCreate(_ *model.Repo, in *model.Registry) error {
@ -45,5 +78,57 @@ func (d *db) RegistryUpdate(_ *model.Repo, in *model.Registry) error {
}
func (d *db) RegistryDelete(repo *model.Repo, addr string) error {
return d.store.RegistryDelete(repo, addr)
registry, err := d.store.RegistryFind(repo, addr)
if err != nil {
return err
}
return d.store.RegistryDelete(registry)
}
func (d *db) OrgRegistryFind(owner int64, name string) (*model.Registry, error) {
return d.store.OrgRegistryFind(owner, name)
}
func (d *db) OrgRegistryList(owner int64, p *model.ListOptions) ([]*model.Registry, error) {
return d.store.OrgRegistryList(owner, p)
}
func (d *db) OrgRegistryCreate(_ int64, in *model.Registry) error {
return d.store.RegistryCreate(in)
}
func (d *db) OrgRegistryUpdate(_ int64, in *model.Registry) error {
return d.store.RegistryUpdate(in)
}
func (d *db) OrgRegistryDelete(owner int64, addr string) error {
registry, err := d.store.OrgRegistryFind(owner, addr)
if err != nil {
return err
}
return d.store.RegistryDelete(registry)
}
func (d *db) GlobalRegistryFind(addr string) (*model.Registry, error) {
return d.store.GlobalRegistryFind(addr)
}
func (d *db) GlobalRegistryList(p *model.ListOptions) ([]*model.Registry, error) {
return d.store.GlobalRegistryList(p)
}
func (d *db) GlobalRegistryCreate(in *model.Registry) error {
return d.store.RegistryCreate(in)
}
func (d *db) GlobalRegistryUpdate(in *model.Registry) error {
return d.store.RegistryUpdate(in)
}
func (d *db) GlobalRegistryDelete(addr string) error {
registry, err := d.store.GlobalRegistryFind(addr)
if err != nil {
return err
}
return d.store.RegistryDelete(registry)
}

View file

@ -25,6 +25,7 @@ import (
"github.com/docker/cli/cli/config/types"
"go.woodpecker-ci.org/woodpecker/v2/server/model"
model_types "go.woodpecker-ci.org/woodpecker/v2/server/store/types"
)
type filesystem struct {
@ -79,17 +80,29 @@ func parseDockerConfig(path string) ([]*model.Registry, error) {
Address: key,
Username: auth.Username,
Password: auth.Password,
ReadOnly: true,
})
}
return registries, nil
}
func (f *filesystem) RegistryFind(*model.Repo, string) (*model.Registry, error) {
return nil, nil
func (f *filesystem) GlobalRegistryFind(addr string) (*model.Registry, error) {
registries, err := f.GlobalRegistryList(&model.ListOptions{All: true})
if err != nil {
return nil, err
}
for _, reg := range registries {
if reg.Address == addr {
return reg, nil
}
}
return nil, model_types.RecordNotExist
}
func (f *filesystem) RegistryList(_ *model.Repo, p *model.ListOptions) ([]*model.Registry, error) {
func (f *filesystem) GlobalRegistryList(p *model.ListOptions) ([]*model.Registry, error) {
regs, err := parseDockerConfig(f.path)
if err != nil {
return nil, err

View file

@ -18,15 +18,29 @@ import "go.woodpecker-ci.org/woodpecker/v2/server/model"
// Service defines a service for managing registries.
type Service interface {
RegistryListPipeline(*model.Repo, *model.Pipeline) ([]*model.Registry, error)
// Repository registries
RegistryFind(*model.Repo, string) (*model.Registry, error)
RegistryList(*model.Repo, *model.ListOptions) ([]*model.Registry, error)
RegistryCreate(*model.Repo, *model.Registry) error
RegistryUpdate(*model.Repo, *model.Registry) error
RegistryDelete(*model.Repo, string) error
// Organization registries
OrgRegistryFind(int64, string) (*model.Registry, error)
OrgRegistryList(int64, *model.ListOptions) ([]*model.Registry, error)
OrgRegistryCreate(int64, *model.Registry) error
OrgRegistryUpdate(int64, *model.Registry) error
OrgRegistryDelete(int64, string) error
// Global registries
GlobalRegistryFind(string) (*model.Registry, error)
GlobalRegistryList(*model.ListOptions) ([]*model.Registry, error)
GlobalRegistryCreate(*model.Registry) error
GlobalRegistryUpdate(*model.Registry) error
GlobalRegistryDelete(string) error
}
// ReadOnlyService defines a service for managing registries.
type ReadOnlyService interface {
RegistryFind(*model.Repo, string) (*model.Registry, error)
RegistryList(*model.Repo, *model.ListOptions) ([]*model.Registry, error)
GlobalRegistryFind(string) (*model.Registry, error)
GlobalRegistryList(*model.ListOptions) ([]*model.Registry, error)
}

View file

@ -36,8 +36,8 @@ func (d *db) SecretList(repo *model.Repo, p *model.ListOptions) ([]*model.Secret
return d.store.SecretList(repo, false, p)
}
func (d *db) SecretListPipeline(repo *model.Repo, _ *model.Pipeline, p *model.ListOptions) ([]*model.Secret, error) {
s, err := d.store.SecretList(repo, true, p)
func (d *db) SecretListPipeline(repo *model.Repo, _ *model.Pipeline) ([]*model.Secret, error) {
s, err := d.store.SecretList(repo, true, &model.ListOptions{All: true})
if err != nil {
return nil, err
}

View file

@ -64,7 +64,7 @@ func TestSecretListPipeline(t *testing.T) {
repoSecret,
}, nil)
s, err := secret.NewDB(mockStore).SecretListPipeline(&model.Repo{}, &model.Pipeline{}, &model.ListOptions{})
s, err := secret.NewDB(mockStore).SecretListPipeline(&model.Repo{}, &model.Pipeline{})
g.Assert(err).IsNil()
g.Assert(len(s)).Equal(1)
@ -77,7 +77,7 @@ func TestSecretListPipeline(t *testing.T) {
orgSecret,
}, nil)
s, err := secret.NewDB(mockStore).SecretListPipeline(&model.Repo{}, &model.Pipeline{}, &model.ListOptions{})
s, err := secret.NewDB(mockStore).SecretListPipeline(&model.Repo{}, &model.Pipeline{})
g.Assert(err).IsNil()
g.Assert(len(s)).Equal(1)
@ -89,7 +89,7 @@ func TestSecretListPipeline(t *testing.T) {
globalSecret,
}, nil)
s, err := secret.NewDB(mockStore).SecretListPipeline(&model.Repo{}, &model.Pipeline{}, &model.ListOptions{})
s, err := secret.NewDB(mockStore).SecretListPipeline(&model.Repo{}, &model.Pipeline{})
g.Assert(err).IsNil()
g.Assert(len(s)).Equal(1)

View file

@ -18,7 +18,7 @@ import "go.woodpecker-ci.org/woodpecker/v2/server/model"
// Service defines a service for managing secrets.
type Service interface {
SecretListPipeline(*model.Repo, *model.Pipeline, *model.ListOptions) ([]*model.Secret, error)
SecretListPipeline(*model.Repo, *model.Pipeline) ([]*model.Secret, error)
// Repository secrets
SecretFind(*model.Repo, string) (*model.Secret, error)
SecretList(*model.Repo, *model.ListOptions) ([]*model.Secret, error)

View file

@ -0,0 +1,33 @@
// Copyright 2024 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package migration
import (
"src.techknowlogick.com/xormigrate"
"xorm.io/xorm"
)
var alterTableRegistriesFixRequiredFields = xormigrate.Migration{
ID: "alter-table-registries-fix-required-fields",
MigrateSession: func(sess *xorm.Session) error {
if err := alterColumnDefault(sess, "registries", "repo_id", "0"); err != nil {
return err
}
if err := alterColumnNull(sess, "registries", "repo_id", false); err != nil {
return err
}
return alterColumnNull(sess, "registries", "address", false)
},
}

View file

@ -195,6 +195,7 @@ func alterColumnDefault(sess *xorm.Session, table, column, defValue string) erro
}
}
//nolint:unparam
func alterColumnNull(sess *xorm.Session, table, column string, null bool) error {
val := "NULL"
if !null {

View file

@ -61,6 +61,7 @@ var migrationTasks = []*xormigrate.Migration{
&cleanRegistryPipeline,
&setForgeID,
&unifyColumnsTables,
&alterTableRegistriesFixRequiredFields,
}
var allBeans = []any{

View file

@ -20,6 +20,8 @@ import (
"go.woodpecker-ci.org/woodpecker/v2/server/model"
)
const orderRegistriesBy = "id"
func (s storage) RegistryFind(repo *model.Repo, addr string) (*model.Registry, error) {
reg := new(model.Registry)
return reg, wrapGet(s.engine.Where(
@ -27,9 +29,19 @@ func (s storage) RegistryFind(repo *model.Repo, addr string) (*model.Registry, e
).Get(reg))
}
func (s storage) RegistryList(repo *model.Repo, p *model.ListOptions) ([]*model.Registry, error) {
func (s storage) RegistryList(repo *model.Repo, includeGlobalAndOrg bool, p *model.ListOptions) ([]*model.Registry, error) {
var regs []*model.Registry
return regs, s.paginate(p).OrderBy("id").Where("repo_id = ?", repo.ID).Find(&regs)
var cond builder.Cond = builder.Eq{"repo_id": repo.ID}
if includeGlobalAndOrg {
cond = cond.Or(builder.Eq{"org_id": repo.OrgID}).
Or(builder.And(builder.Eq{"org_id": 0}, builder.Eq{"repo_id": 0}))
}
return regs, s.paginate(p).Where(cond).OrderBy(orderRegistriesBy).Find(&regs)
}
func (s storage) RegistryListAll() ([]*model.Registry, error) {
var registries []*model.Registry
return registries, s.engine.Find(&registries)
}
func (s storage) RegistryCreate(registry *model.Registry) error {
@ -43,10 +55,32 @@ func (s storage) RegistryUpdate(registry *model.Registry) error {
return err
}
func (s storage) RegistryDelete(repo *model.Repo, addr string) error {
registry, err := s.RegistryFind(repo, addr)
if err != nil {
return err
}
func (s storage) RegistryDelete(registry *model.Registry) error {
return wrapDelete(s.engine.ID(registry.ID).Delete(new(model.Registry)))
}
func (s storage) OrgRegistryFind(orgID int64, name string) (*model.Registry, error) {
registry := new(model.Registry)
return registry, wrapGet(s.engine.Where(
builder.Eq{"org_id": orgID, "address": name},
).Get(registry))
}
func (s storage) OrgRegistryList(orgID int64, p *model.ListOptions) ([]*model.Registry, error) {
registries := make([]*model.Registry, 0)
return registries, s.paginate(p).Where("org_id = ?", orgID).OrderBy(orderRegistriesBy).Find(&registries)
}
func (s storage) GlobalRegistryFind(name string) (*model.Registry, error) {
registry := new(model.Registry)
return registry, wrapGet(s.engine.Where(
builder.Eq{"org_id": 0, "repo_id": 0, "address": name},
).Get(registry))
}
func (s storage) GlobalRegistryList(p *model.ListOptions) ([]*model.Registry, error) {
registries := make([]*model.Registry, 0)
return registries, s.paginate(p).Where(
builder.Eq{"org_id": 0, "repo_id": 0},
).OrderBy(orderRegistriesBy).Find(&registries)
}

View file

@ -18,6 +18,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.woodpecker-ci.org/woodpecker/v2/server/model"
"go.woodpecker-ci.org/woodpecker/v2/server/store/types"
@ -60,7 +61,7 @@ func TestRegistryList(t *testing.T) {
Password: "bar",
}))
list, err := store.RegistryList(&model.Repo{ID: 1}, &model.ListOptions{Page: 1, PerPage: 50})
list, err := store.RegistryList(&model.Repo{ID: 1}, false, &model.ListOptions{Page: 1, PerPage: 50})
assert.NoError(t, err)
assert.Len(t, list, 2)
}
@ -117,6 +118,88 @@ func TestRegistryDelete(t *testing.T) {
return
}
assert.NoError(t, store.RegistryDelete(&model.Repo{ID: 1}, "index.docker.io"))
assert.ErrorIs(t, store.RegistryDelete(&model.Repo{ID: 1}, "index.docker.io"), types.RecordNotExist)
assert.NoError(t, store.RegistryDelete(reg1))
assert.ErrorIs(t, store.RegistryDelete(reg1), types.RecordNotExist)
}
func createTestRegistries(t *testing.T, store *storage) {
assert.NoError(t, store.RegistryCreate(&model.Registry{
OrgID: 12,
Address: "my.regsitry.local",
}))
assert.NoError(t, store.RegistryCreate(&model.Registry{
RepoID: 1,
Address: "private.registry.local",
}))
assert.NoError(t, store.RegistryCreate(&model.Registry{
RepoID: 1,
Address: "very-private.registry.local",
}))
assert.NoError(t, store.RegistryCreate(&model.Registry{
Address: "index.docker.io",
}))
}
func TestOrgRegistryFind(t *testing.T) {
store, closer := newTestStore(t, new(model.Registry))
defer closer()
err := store.RegistryCreate(&model.Registry{
OrgID: 12,
Address: "my.regsitry.local",
Username: "username",
Password: "password",
})
assert.NoError(t, err)
registry, err := store.OrgRegistryFind(12, "my.regsitry.local")
assert.NoError(t, err)
assert.EqualValues(t, 12, registry.OrgID)
assert.Equal(t, "my.regsitry.local", registry.Address)
assert.Equal(t, "username", registry.Username)
assert.Equal(t, "password", registry.Password)
}
func TestOrgRegistryList(t *testing.T) {
store, closer := newTestStore(t, new(model.Registry))
defer closer()
createTestRegistries(t, store)
list, err := store.OrgRegistryList(12, &model.ListOptions{All: true})
assert.NoError(t, err)
require.Len(t, list, 1)
assert.True(t, list[0].IsOrganization())
}
func TestGlobalRegistryFind(t *testing.T) {
store, closer := newTestStore(t, new(model.Registry))
defer closer()
err := store.RegistryCreate(&model.Registry{
Address: "my.regsitry.local",
Username: "username",
Password: "password",
})
assert.NoError(t, err)
registry, err := store.GlobalRegistryFind("my.regsitry.local")
assert.NoError(t, err)
assert.Equal(t, "my.regsitry.local", registry.Address)
assert.Equal(t, "username", registry.Username)
assert.Equal(t, "password", registry.Password)
}
func TestGlobalRegistryList(t *testing.T) {
store, closer := newTestStore(t, new(model.Registry))
defer closer()
createTestRegistries(t, store)
list, err := store.GlobalRegistryList(&model.ListOptions{All: true})
assert.NoError(t, err)
assert.Len(t, list, 1)
assert.True(t, list[0].IsGlobal())
}

View file

@ -1190,6 +1190,66 @@ func (_m *Store) GetUserRemoteID(_a0 model.ForgeRemoteID, _a1 string) (*model.Us
return r0, r1
}
// GlobalRegistryFind provides a mock function with given fields: _a0
func (_m *Store) GlobalRegistryFind(_a0 string) (*model.Registry, error) {
ret := _m.Called(_a0)
if len(ret) == 0 {
panic("no return value specified for GlobalRegistryFind")
}
var r0 *model.Registry
var r1 error
if rf, ok := ret.Get(0).(func(string) (*model.Registry, error)); ok {
return rf(_a0)
}
if rf, ok := ret.Get(0).(func(string) *model.Registry); ok {
r0 = rf(_a0)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Registry)
}
}
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(_a0)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GlobalRegistryList provides a mock function with given fields: _a0
func (_m *Store) GlobalRegistryList(_a0 *model.ListOptions) ([]*model.Registry, error) {
ret := _m.Called(_a0)
if len(ret) == 0 {
panic("no return value specified for GlobalRegistryList")
}
var r0 []*model.Registry
var r1 error
if rf, ok := ret.Get(0).(func(*model.ListOptions) ([]*model.Registry, error)); ok {
return rf(_a0)
}
if rf, ok := ret.Get(0).(func(*model.ListOptions) []*model.Registry); ok {
r0 = rf(_a0)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Registry)
}
}
if rf, ok := ret.Get(1).(func(*model.ListOptions) error); ok {
r1 = rf(_a0)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GlobalSecretFind provides a mock function with given fields: _a0
func (_m *Store) GlobalSecretFind(_a0 string) (*model.Secret, error) {
ret := _m.Called(_a0)
@ -1488,6 +1548,66 @@ func (_m *Store) OrgList(_a0 *model.ListOptions) ([]*model.Org, error) {
return r0, r1
}
// OrgRegistryFind provides a mock function with given fields: _a0, _a1
func (_m *Store) OrgRegistryFind(_a0 int64, _a1 string) (*model.Registry, error) {
ret := _m.Called(_a0, _a1)
if len(ret) == 0 {
panic("no return value specified for OrgRegistryFind")
}
var r0 *model.Registry
var r1 error
if rf, ok := ret.Get(0).(func(int64, string) (*model.Registry, error)); ok {
return rf(_a0, _a1)
}
if rf, ok := ret.Get(0).(func(int64, string) *model.Registry); ok {
r0 = rf(_a0, _a1)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Registry)
}
}
if rf, ok := ret.Get(1).(func(int64, string) error); ok {
r1 = rf(_a0, _a1)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// OrgRegistryList provides a mock function with given fields: _a0, _a1
func (_m *Store) OrgRegistryList(_a0 int64, _a1 *model.ListOptions) ([]*model.Registry, error) {
ret := _m.Called(_a0, _a1)
if len(ret) == 0 {
panic("no return value specified for OrgRegistryList")
}
var r0 []*model.Registry
var r1 error
if rf, ok := ret.Get(0).(func(int64, *model.ListOptions) ([]*model.Registry, error)); ok {
return rf(_a0, _a1)
}
if rf, ok := ret.Get(0).(func(int64, *model.ListOptions) []*model.Registry); ok {
r0 = rf(_a0, _a1)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Registry)
}
}
if rf, ok := ret.Get(1).(func(int64, *model.ListOptions) error); ok {
r1 = rf(_a0, _a1)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// OrgRepoList provides a mock function with given fields: _a0, _a1
func (_m *Store) OrgRepoList(_a0 *model.Org, _a1 *model.ListOptions) ([]*model.Repo, error) {
ret := _m.Called(_a0, _a1)
@ -1698,17 +1818,17 @@ func (_m *Store) RegistryCreate(_a0 *model.Registry) error {
return r0
}
// RegistryDelete provides a mock function with given fields: repo, addr
func (_m *Store) RegistryDelete(repo *model.Repo, addr string) error {
ret := _m.Called(repo, addr)
// RegistryDelete provides a mock function with given fields: _a0
func (_m *Store) RegistryDelete(_a0 *model.Registry) error {
ret := _m.Called(_a0)
if len(ret) == 0 {
panic("no return value specified for RegistryDelete")
}
var r0 error
if rf, ok := ret.Get(0).(func(*model.Repo, string) error); ok {
r0 = rf(repo, addr)
if rf, ok := ret.Get(0).(func(*model.Registry) error); ok {
r0 = rf(_a0)
} else {
r0 = ret.Error(0)
}
@ -1746,9 +1866,9 @@ func (_m *Store) RegistryFind(_a0 *model.Repo, _a1 string) (*model.Registry, err
return r0, r1
}
// RegistryList provides a mock function with given fields: _a0, _a1
func (_m *Store) RegistryList(_a0 *model.Repo, _a1 *model.ListOptions) ([]*model.Registry, error) {
ret := _m.Called(_a0, _a1)
// RegistryList provides a mock function with given fields: _a0, _a1, _a2
func (_m *Store) RegistryList(_a0 *model.Repo, _a1 bool, _a2 *model.ListOptions) ([]*model.Registry, error) {
ret := _m.Called(_a0, _a1, _a2)
if len(ret) == 0 {
panic("no return value specified for RegistryList")
@ -1756,19 +1876,49 @@ func (_m *Store) RegistryList(_a0 *model.Repo, _a1 *model.ListOptions) ([]*model
var r0 []*model.Registry
var r1 error
if rf, ok := ret.Get(0).(func(*model.Repo, *model.ListOptions) ([]*model.Registry, error)); ok {
return rf(_a0, _a1)
if rf, ok := ret.Get(0).(func(*model.Repo, bool, *model.ListOptions) ([]*model.Registry, error)); ok {
return rf(_a0, _a1, _a2)
}
if rf, ok := ret.Get(0).(func(*model.Repo, *model.ListOptions) []*model.Registry); ok {
r0 = rf(_a0, _a1)
if rf, ok := ret.Get(0).(func(*model.Repo, bool, *model.ListOptions) []*model.Registry); ok {
r0 = rf(_a0, _a1, _a2)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Registry)
}
}
if rf, ok := ret.Get(1).(func(*model.Repo, *model.ListOptions) error); ok {
r1 = rf(_a0, _a1)
if rf, ok := ret.Get(1).(func(*model.Repo, bool, *model.ListOptions) error); ok {
r1 = rf(_a0, _a1, _a2)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// RegistryListAll provides a mock function with given fields:
func (_m *Store) RegistryListAll() ([]*model.Registry, error) {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for RegistryListAll")
}
var r0 []*model.Registry
var r1 error
if rf, ok := ret.Get(0).(func() ([]*model.Registry, error)); ok {
return rf()
}
if rf, ok := ret.Get(0).(func() []*model.Registry); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Registry)
}
}
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}

View file

@ -120,10 +120,15 @@ type Store interface {
// Registries
RegistryFind(*model.Repo, string) (*model.Registry, error)
RegistryList(*model.Repo, *model.ListOptions) ([]*model.Registry, error)
RegistryList(*model.Repo, bool, *model.ListOptions) ([]*model.Registry, error)
RegistryListAll() ([]*model.Registry, error)
RegistryCreate(*model.Registry) error
RegistryUpdate(*model.Registry) error
RegistryDelete(repo *model.Repo, addr string) error
RegistryDelete(*model.Registry) error
OrgRegistryFind(int64, string) (*model.Registry, error)
OrgRegistryList(int64, *model.ListOptions) ([]*model.Registry, error)
GlobalRegistryFind(string) (*model.Registry, error)
GlobalRegistryList(*model.ListOptions) ([]*model.Registry, error)
// Steps
StepLoad(int64) (*model.Step, error)

6
web/components.d.ts vendored
View file

@ -14,6 +14,7 @@ declare module 'vue' {
AdminOrgsTab: typeof import('./src/components/admin/settings/AdminOrgsTab.vue')['default']
AdminQueueStats: typeof import('./src/components/admin/settings/queue/AdminQueueStats.vue')['default']
AdminQueueTab: typeof import('./src/components/admin/settings/AdminQueueTab.vue')['default']
AdminRegistriesTab: typeof import('./src/components/admin/settings/AdminRegistriesTab.vue')['default']
AdminReposTab: typeof import('./src/components/admin/settings/AdminReposTab.vue')['default']
AdminSecretsTab: typeof import('./src/components/admin/settings/AdminSecretsTab.vue')['default']
AdminUsersTab: typeof import('./src/components/admin/settings/AdminUsersTab.vue')['default']
@ -66,12 +67,12 @@ declare module 'vue' {
IMdiPlay: typeof import('~icons/mdi/play')['default']
IMdiRadioboxBlank: typeof import('~icons/mdi/radiobox-blank')['default']
IMdiRadioboxIndeterminateVariant: typeof import('~icons/mdi/radiobox-indeterminate-variant')['default']
IMdiSync: typeof import('~icons/mdi/sync')['default']
IMdiSourceBranch: typeof import('~icons/mdi/source-branch')['default']
IMdiSourceCommit: typeof import('~icons/mdi/source-commit')['default']
IMdiSourceMerge: typeof import('~icons/mdi/source-merge')['default']
IMdiSourcePull: typeof import('~icons/mdi/source-pull')['default']
IMdiStop: typeof import('~icons/mdi/stop')['default']
IMdiSync: typeof import('~icons/mdi/sync')['default']
IMdiTagOutline: typeof import('~icons/mdi/tag-outline')['default']
InputField: typeof import('./src/components/form/InputField.vue')['default']
IPhGitlabLogoSimpleFill: typeof import('~icons/ph/gitlab-logo-simple-fill')['default']
@ -85,6 +86,7 @@ declare module 'vue' {
ManualPipelinePopup: typeof import('./src/components/layout/popups/ManualPipelinePopup.vue')['default']
Navbar: typeof import('./src/components/layout/header/Navbar.vue')['default']
NumberField: typeof import('./src/components/form/NumberField.vue')['default']
OrgRegistriesTab: typeof import('./src/components/org/settings/OrgRegistriesTab.vue')['default']
OrgSecretsTab: typeof import('./src/components/org/settings/OrgSecretsTab.vue')['default']
Panel: typeof import('./src/components/layout/Panel.vue')['default']
PipelineFeedItem: typeof import('./src/components/pipeline-feed/PipelineFeedItem.vue')['default']
@ -98,7 +100,9 @@ declare module 'vue' {
PipelineStepList: typeof import('./src/components/repo/pipeline/PipelineStepList.vue')['default']
Popup: typeof import('./src/components/layout/Popup.vue')['default']
RadioField: typeof import('./src/components/form/RadioField.vue')['default']
RegistryEdit: typeof import('./src/components/registry/RegistryEdit.vue')['default']
RegistriesTab: typeof import('./src/components/repo/settings/RegistriesTab.vue')['default']
RegistryList: typeof import('./src/components/registry/RegistryList.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
Scaffold: typeof import('./src/components/layout/scaffold/Scaffold.vue')['default']

View file

@ -122,24 +122,6 @@
"desc": "Enable to cancel pending and running pipelines of the same event and context before starting the newly triggered one."
}
},
"registries": {
"registries": "Registries",
"credentials": "Registry credentials",
"desc": "Registries credentials can be added to use private images for your pipeline.",
"show": "Show registries",
"add": "Add registry",
"none": "There are no registry credentials yet.",
"save": "Save registry",
"created": "Registry credentials created",
"saved": "Registry credentials saved",
"deleted": "Registry credentials deleted",
"address": {
"address": "Address",
"placeholder": "Registry Address (e.g. docker.io)"
},
"edit": "Edit registry",
"delete": "Delete registry"
},
"crons": {
"crons": "Crons",
"desc": "Cron jobs can be used to trigger pipelines on a regular basis.",
@ -266,6 +248,9 @@
"not_allowed": "You are not allowed to access this organization's settings",
"secrets": {
"desc": "Organization secrets can be passed to all organization's repository individual pipeline steps at runtime as environmental variables."
},
"registries": {
"desc": "Organization registry credentials can be added to use private images for all organization's pipelines."
}
}
},
@ -276,6 +261,10 @@
"desc": "Global secrets can be passed to all repositories individual pipeline steps at runtime as environmental variables.",
"warning": "These secrets will be available for all server users."
},
"registries": {
"desc": "Global registry credentials can be added to use private images for all server's pipelines.",
"warning": "These registry creditentials will be available for all server users."
},
"agents": {
"agents": "Agents",
"desc": "Agents registered for this server",
@ -435,6 +424,26 @@
"edit": "Edit secret",
"delete": "Delete secret"
},
"registries": {
"registries": "Registries",
"credentials": "Registry credentials",
"desc": "Registries credentials can be added to use private images for your pipeline.",
"none": "There are no registry credentials yet.",
"address": {
"address": "Address",
"desc": "Registry Address (e.g. docker.io)"
},
"show": "Show registries",
"save": "Save registry",
"add": "Add registry",
"view": "View registry",
"edit": "Edit registry",
"delete": "Delete registry",
"delete_confirm": "Do you really want to delete this registry?",
"created": "Registry credentials created",
"saved": "Registry credentials saved",
"deleted": "Registry credentials deleted"
},
"default": "default",
"info": "Info",
"running_version": "You are running Woodpecker {0}",

View file

@ -0,0 +1,101 @@
<template>
<Settings
:title="$t('registries.registries')"
:desc="$t('admin.settings.registries.desc')"
docs-url="docs/usage/registries"
:warning="$t('admin.settings.registries.warning')"
>
<template #titleActions>
<Button
v-if="selectedRegistry"
:text="$t('registries.show')"
start-icon="back"
@click="selectedRegistry = undefined"
/>
<Button v-else :text="$t('registries.add')" start-icon="plus" @click="showAddRegistry" />
</template>
<RegistryList
v-if="!selectedRegistry"
v-model="registries"
:is-deleting="isDeleting"
@edit="editRegistry"
@delete="deleteRegistry"
/>
<RegistryEdit
v-else
v-model="selectedRegistry"
:is-saving="isSaving"
@save="createRegistry"
@cancel="selectedRegistry = undefined"
/>
</Settings>
</template>
<script lang="ts" setup>
import { cloneDeep } from 'lodash';
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import Button from '~/components/atomic/Button.vue';
import Settings from '~/components/layout/Settings.vue';
import RegistryEdit from '~/components/registry/RegistryEdit.vue';
import RegistryList from '~/components/registry/RegistryList.vue';
import useApiClient from '~/compositions/useApiClient';
import { useAsyncAction } from '~/compositions/useAsyncAction';
import useNotifications from '~/compositions/useNotifications';
import { usePagination } from '~/compositions/usePaginate';
import type { Registry } from '~/lib/api/types';
const emptyRegistry: Partial<Registry> = {
address: '',
username: '',
password: '',
};
const apiClient = useApiClient();
const notifications = useNotifications();
const i18n = useI18n();
const selectedRegistry = ref<Partial<Registry>>();
const isEditingRegistry = computed(() => !!selectedRegistry.value?.id);
async function loadRegistries(page: number): Promise<Registry[] | null> {
return apiClient.getGlobalRegistryList({ page });
}
const { resetPage, data: registries } = usePagination(loadRegistries, () => !selectedRegistry.value);
const { doSubmit: createRegistry, isLoading: isSaving } = useAsyncAction(async () => {
if (!selectedRegistry.value) {
throw new Error("Unexpected: Can't get registry");
}
if (isEditingRegistry.value) {
await apiClient.updateGlobalRegistry(selectedRegistry.value);
} else {
await apiClient.createGlobalRegistry(selectedRegistry.value);
}
notifications.notify({
title: isEditingRegistry.value ? i18n.t('registries.saved') : i18n.t('registries.created'),
type: 'success',
});
selectedRegistry.value = undefined;
resetPage();
});
const { doSubmit: deleteRegistry, isLoading: isDeleting } = useAsyncAction(async (_registry: Registry) => {
await apiClient.deleteGlobalRegistry(_registry.address);
notifications.notify({ title: i18n.t('registries.deleted'), type: 'success' });
resetPage();
});
function editRegistry(registry: Registry) {
selectedRegistry.value = cloneDeep(registry);
}
function showAddRegistry() {
selectedRegistry.value = cloneDeep(emptyRegistry);
}
</script>

View file

@ -3,7 +3,7 @@
<button
v-for="tab in tabs"
:key="tab.id"
class="w-full py-1 md:py-2 md:w-auto md:px-8 flex cursor-pointer md:border-b-2 text-wp-text-100 hover:text-wp-text-200 items-center"
class="w-full py-1 md:py-2 md:w-auto md:px-6 flex cursor-pointer md:border-b-2 text-wp-text-100 hover:text-wp-text-200 items-center"
:class="{
'border-wp-text-100': activeTab === tab.id,
'border-transparent': activeTab !== tab.id,

View file

@ -0,0 +1,113 @@
<template>
<Settings
:title="$t('registries.registries')"
:desc="$t('org.settings.registries.desc')"
docs-url="docs/usage/registries"
>
<template #titleActions>
<Button
v-if="selectedRegistry"
:text="$t('registries.show')"
start-icon="back"
@click="selectedRegistry = undefined"
/>
<Button v-else :text="$t('registries.add')" start-icon="plus" @click="showAddRegistry" />
</template>
<RegistryList
v-if="!selectedRegistry"
v-model="registries"
:is-deleting="isDeleting"
@edit="editRegistry"
@delete="deleteRegistry"
/>
<RegistryEdit
v-else
v-model="selectedRegistry"
:is-saving="isSaving"
@save="createRegistry"
@cancel="selectedRegistry = undefined"
/>
</Settings>
</template>
<script lang="ts" setup>
import { cloneDeep } from 'lodash';
import { computed, inject, ref, type Ref } from 'vue';
import { useI18n } from 'vue-i18n';
import Button from '~/components/atomic/Button.vue';
import Settings from '~/components/layout/Settings.vue';
import RegistryEdit from '~/components/registry/RegistryEdit.vue';
import RegistryList from '~/components/registry/RegistryList.vue';
import useApiClient from '~/compositions/useApiClient';
import { useAsyncAction } from '~/compositions/useAsyncAction';
import useNotifications from '~/compositions/useNotifications';
import { usePagination } from '~/compositions/usePaginate';
import type { Org, Registry } from '~/lib/api/types';
const emptyRegistry: Partial<Registry> = {
address: '',
username: '',
password: '',
};
const apiClient = useApiClient();
const notifications = useNotifications();
const i18n = useI18n();
const org = inject<Ref<Org>>('org');
const selectedRegistry = ref<Partial<Registry>>();
const isEditing = computed(() => !!selectedRegistry.value?.id);
async function loadRegistries(page: number): Promise<Registry[] | null> {
if (!org?.value) {
throw new Error("Unexpected: Can't load org");
}
return apiClient.getOrgRegistryList(org.value.id, { page });
}
const { resetPage, data: registries } = usePagination(loadRegistries, () => !selectedRegistry.value);
const { doSubmit: createRegistry, isLoading: isSaving } = useAsyncAction(async () => {
if (!org?.value) {
throw new Error("Unexpected: Can't load org");
}
if (!selectedRegistry.value) {
throw new Error("Unexpected: Can't get registry");
}
if (isEditing.value) {
await apiClient.updateOrgRegistry(org.value.id, selectedRegistry.value);
} else {
await apiClient.createOrgRegistry(org.value.id, selectedRegistry.value);
}
notifications.notify({
title: isEditing.value ? i18n.t('registries.saved') : i18n.t('registries.created'),
type: 'success',
});
selectedRegistry.value = undefined;
resetPage();
});
const { doSubmit: deleteRegistry, isLoading: isDeleting } = useAsyncAction(async (_registry: Registry) => {
if (!org?.value) {
throw new Error("Unexpected: Can't load org");
}
await apiClient.deleteOrgRegistry(org.value.id, _registry.address);
notifications.notify({ title: i18n.t('registries.deleted'), type: 'success' });
resetPage();
});
function editRegistry(registry: Registry) {
selectedRegistry.value = cloneDeep(registry);
}
function showAddRegistry() {
selectedRegistry.value = cloneDeep(emptyRegistry);
}
</script>

View file

@ -0,0 +1,78 @@
<template>
<div v-if="innerValue" class="space-y-4">
<form @submit.prevent="save">
<InputField v-slot="{ id }" :label="$t('registries.address.address')">
<!-- TODO: check input field Address is a valid address -->
<TextField
:id="id"
v-model="innerValue.address"
:placeholder="$t('registries.address.desc')"
required
:disabled="isEditing || isReadOnly"
/>
</InputField>
<InputField v-slot="{ id }" :label="$t('username')">
<TextField
:id="id"
v-model="innerValue.username"
:placeholder="$t('username')"
required
:disabled="isReadOnly"
/>
</InputField>
<InputField v-if="!isReadOnly" v-slot="{ id }" :label="$t('password')">
<TextField :id="id" v-model="innerValue.password" :placeholder="$t('password')" :required="!isEditing" />
</InputField>
<div v-if="!isReadOnly" class="flex gap-2">
<Button type="button" color="gray" :text="$t('cancel')" @click="$emit('cancel')" />
<Button
type="submit"
color="green"
:is-loading="isSaving"
:text="isEditing ? $t('registries.save') : $t('registries.add')"
/>
</div>
</form>
</div>
</template>
<script lang="ts" setup>
import { computed, toRef } from 'vue';
import Button from '~/components/atomic/Button.vue';
import InputField from '~/components/form/InputField.vue';
import TextField from '~/components/form/TextField.vue';
import type { Registry } from '~/lib/api/types';
const props = defineProps<{
modelValue: Partial<Registry>;
isSaving: boolean;
}>();
const emit = defineEmits<{
(event: 'update:modelValue', value: Partial<Registry> | undefined): void;
(event: 'save', value: Partial<Registry>): void;
(event: 'cancel'): void;
}>();
const modelValue = toRef(props, 'modelValue');
const innerValue = computed({
get: () => modelValue.value,
set: (value) => {
emit('update:modelValue', value);
},
});
const isEditing = computed(() => !!innerValue.value?.id);
const isReadOnly = computed(() => !!innerValue.value?.readonly);
function save() {
if (!innerValue.value) {
return;
}
emit('save', innerValue.value);
}
</script>

View file

@ -0,0 +1,63 @@
<template>
<div class="space-y-4 text-wp-text-100">
<ListItem
v-for="registry in registries"
:key="registry.id"
class="items-center !bg-wp-background-200 !dark:bg-wp-background-100"
>
<span>{{ registry.address }}</span>
<IconButton
:icon="registry.readonly ? 'chevron-right' : 'edit'"
class="ml-auto w-8 h-8"
:title="registry.readonly ? $t('registries.view') : $t('registries.edit')"
@click="editRegistry(registry)"
/>
<IconButton
v-if="!registry.readonly"
icon="trash"
class="w-8 h-8 hover:text-wp-control-error-100"
:is-loading="isDeleting"
:title="$t('registries.delete')"
@click="deleteRegistry(registry)"
/>
</ListItem>
<div v-if="registries?.length === 0" class="ml-2">{{ $t('registries.none') }}</div>
</div>
</template>
<script lang="ts" setup>
import { toRef } from 'vue';
import { useI18n } from 'vue-i18n';
import IconButton from '~/components/atomic/IconButton.vue';
import ListItem from '~/components/atomic/ListItem.vue';
import type { Registry } from '~/lib/api/types';
const props = defineProps<{
modelValue: (Registry & { edit?: boolean })[];
isDeleting: boolean;
}>();
const emit = defineEmits<{
(event: 'edit', registry: Registry): void;
(event: 'delete', registry: Registry): void;
}>();
const i18n = useI18n();
const registries = toRef(props, 'modelValue');
function editRegistry(registry: Registry) {
emit('edit', registry);
}
function deleteRegistry(registry: Registry) {
// TODO: use proper dialog
// eslint-disable-next-line no-alert
if (!confirm(i18n.t('registries.delete_confirm'))) {
return;
}
emit('delete', registry);
}
</script>

View file

@ -1,95 +1,54 @@
<template>
<Settings
:title="$t('repo.settings.registries.credentials')"
:desc="$t('repo.settings.registries.desc')"
docs-url="docs/usage/registries"
>
<Settings :title="$t('registries.credentials')" :desc="$t('registries.desc')" docs-url="docs/usage/registries">
<template #titleActions>
<Button
v-if="selectedRegistry"
:text="$t('registries.show')"
start-icon="back"
:text="$t('repo.settings.registries.show')"
@click="selectedRegistry = undefined"
/>
<Button v-else start-icon="plus" :text="$t('repo.settings.registries.add')" @click="selectedRegistry = {}" />
<Button v-else :text="$t('registries.add')" start-icon="plus" @click="showAddRegistry" />
</template>
<div v-if="!selectedRegistry" class="space-y-4 text-wp-text-100">
<ListItem
v-for="registry in registries"
:key="registry.id"
class="items-center !bg-wp-background-200 !dark:bg-wp-background-100"
>
<span>{{ registry.address }}</span>
<IconButton
icon="edit"
class="ml-auto w-8 h-8"
:title="$t('repo.settings.registries.edit')"
@click="selectedRegistry = registry"
<RegistryList
v-if="!selectedRegistry"
v-model="registries"
:is-deleting="isDeleting"
@edit="editRegistry"
@delete="deleteRegistry"
/>
<IconButton
icon="trash"
class="w-8 h-8 hover:text-wp-control-error-100"
:is-loading="isDeleting"
:title="$t('repo.settings.registries.delete')"
@click="deleteRegistry(registry)"
<RegistryEdit
v-else
v-model="selectedRegistry"
:is-saving="isSaving"
@save="createRegistry"
@cancel="selectedRegistry = undefined"
/>
</ListItem>
<div v-if="registries?.length === 0" class="ml-2">{{ $t('repo.settings.registries.none') }}</div>
</div>
<div v-else class="space-y-4">
<form @submit.prevent="createRegistry">
<InputField v-slot="{ id }" :label="$t('repo.settings.registries.address.address')">
<!-- TODO: check input field Address is a valid address -->
<TextField
:id="id"
v-model="selectedRegistry.address"
:placeholder="$t('repo.settings.registries.address.placeholder')"
required
:disabled="isEditingRegistry"
/>
</InputField>
<InputField v-slot="{ id }" :label="$t('username')">
<TextField :id="id" v-model="selectedRegistry.username" :placeholder="$t('username')" required />
</InputField>
<InputField v-slot="{ id }" :label="$t('password')">
<TextField :id="id" v-model="selectedRegistry.password" :placeholder="$t('password')" required />
</InputField>
<div class="flex gap-2">
<Button type="button" color="gray" :text="$t('cancel')" @click="selectedRegistry = undefined" />
<Button
type="submit"
color="green"
:is-loading="isSaving"
:text="isEditingRegistry ? $t('repo.settings.registries.save') : $t('repo.settings.registries.add')"
/>
</div>
</form>
</div>
</Settings>
</template>
<script lang="ts" setup>
import { cloneDeep } from 'lodash';
import { computed, inject, ref, type Ref } from 'vue';
import { useI18n } from 'vue-i18n';
import Button from '~/components/atomic/Button.vue';
import IconButton from '~/components/atomic/IconButton.vue';
import ListItem from '~/components/atomic/ListItem.vue';
import InputField from '~/components/form/InputField.vue';
import TextField from '~/components/form/TextField.vue';
import Settings from '~/components/layout/Settings.vue';
import RegistryEdit from '~/components/registry/RegistryEdit.vue';
import RegistryList from '~/components/registry/RegistryList.vue';
import useApiClient from '~/compositions/useApiClient';
import { useAsyncAction } from '~/compositions/useAsyncAction';
import useNotifications from '~/compositions/useNotifications';
import { usePagination } from '~/compositions/usePaginate';
import type { Registry, Repo } from '~/lib/api/types';
const emptyRegistry: Partial<Registry> = {
address: '',
username: '',
password: '',
};
const apiClient = useApiClient();
const notifications = useNotifications();
const i18n = useI18n();
@ -123,9 +82,7 @@ const { doSubmit: createRegistry, isLoading: isSaving } = useAsyncAction(async (
await apiClient.createRegistry(repo.value.id, selectedRegistry.value);
}
notifications.notify({
title: isEditingRegistry.value
? i18n.t('repo.settings.registries.saved')
: i18n.t('repo.settings.registries.created'),
title: isEditingRegistry.value ? i18n.t('registries.saved') : i18n.t('registries.created'),
type: 'success',
});
selectedRegistry.value = undefined;
@ -139,7 +96,15 @@ const { doSubmit: deleteRegistry, isLoading: isDeleting } = useAsyncAction(async
const registryAddress = encodeURIComponent(_registry.address);
await apiClient.deleteRegistry(repo.value.id, registryAddress);
notifications.notify({ title: i18n.t('repo.settings.registries.deleted'), type: 'success' });
notifications.notify({ title: i18n.t('registries.deleted'), type: 'success' });
resetPage();
});
function editRegistry(registry: Registry) {
selectedRegistry.value = cloneDeep(registry);
}
function showAddRegistry() {
selectedRegistry.value = cloneDeep(emptyRegistry);
}
</script>

View file

@ -170,19 +170,53 @@ export default class WoodpeckerClient extends ApiClient {
getRegistryList(repoId: number, opts?: PaginationOptions): Promise<Registry[] | null> {
const query = encodeQueryString(opts);
return this._get(`/api/repos/${repoId}/registry?${query}`) as Promise<Registry[] | null>;
return this._get(`/api/repos/${repoId}/registries?${query}`) as Promise<Registry[] | null>;
}
createRegistry(repoId: number, registry: Partial<Registry>): Promise<unknown> {
return this._post(`/api/repos/${repoId}/registry`, registry);
return this._post(`/api/repos/${repoId}/registries`, registry);
}
updateRegistry(repoId: number, registry: Partial<Registry>): Promise<unknown> {
return this._patch(`/api/repos/${repoId}/registry/${registry.address}`, registry);
return this._patch(`/api/repos/${repoId}/registries/${registry.address}`, registry);
}
deleteRegistry(repoId: number, registryAddress: string): Promise<unknown> {
return this._delete(`/api/repos/${repoId}/registry/${registryAddress}`);
return this._delete(`/api/repos/${repoId}/registries/${registryAddress}`);
}
getOrgRegistryList(orgId: number, opts?: PaginationOptions): Promise<Registry[] | null> {
const query = encodeQueryString(opts);
return this._get(`/api/orgs/${orgId}/registries?${query}`) as Promise<Registry[] | null>;
}
createOrgRegistry(orgId: number, registry: Partial<Registry>): Promise<unknown> {
return this._post(`/api/orgs/${orgId}/registries`, registry);
}
updateOrgRegistry(orgId: number, registry: Partial<Registry>): Promise<unknown> {
return this._patch(`/api/orgs/${orgId}/registries/${registry.address}`, registry);
}
deleteOrgRegistry(orgId: number, registryAddress: string): Promise<unknown> {
return this._delete(`/api/orgs/${orgId}/registries/${registryAddress}`);
}
getGlobalRegistryList(opts?: PaginationOptions): Promise<Registry[] | null> {
const query = encodeQueryString(opts);
return this._get(`/api/registries?${query}`) as Promise<Registry[] | null>;
}
createGlobalRegistry(registry: Partial<Registry>): Promise<unknown> {
return this._post(`/api/registries`, registry);
}
updateGlobalRegistry(registry: Partial<Registry>): Promise<unknown> {
return this._patch(`/api/registries/${registry.address}`, registry);
}
deleteGlobalRegistry(registryAddress: string): Promise<unknown> {
return this._delete(`/api/registries/${registryAddress}`);
}
getCronList(repoId: number, opts?: PaginationOptions): Promise<Cron[] | null> {

View file

@ -1,6 +1,9 @@
export interface Registry {
id: string;
repo_id: number;
org_id: number;
address: string;
username: string;
password: string;
readonly: boolean;
}

View file

@ -9,6 +9,9 @@
<Tab id="secrets" :title="$t('secrets.secrets')">
<AdminSecretsTab />
</Tab>
<Tab id="registries" :title="$t('registries.registries')">
<AdminRegistriesTab />
</Tab>
<Tab id="repos" :title="$t('admin.settings.repos.repos')">
<AdminReposTab />
</Tab>
@ -36,6 +39,7 @@ import AdminAgentsTab from '~/components/admin/settings/AdminAgentsTab.vue';
import AdminInfoTab from '~/components/admin/settings/AdminInfoTab.vue';
import AdminOrgsTab from '~/components/admin/settings/AdminOrgsTab.vue';
import AdminQueueTab from '~/components/admin/settings/AdminQueueTab.vue';
import AdminRegistriesTab from '~/components/admin/settings/AdminRegistriesTab.vue';
import AdminReposTab from '~/components/admin/settings/AdminReposTab.vue';
import AdminSecretsTab from '~/components/admin/settings/AdminSecretsTab.vue';
import AdminUsersTab from '~/components/admin/settings/AdminUsersTab.vue';

View file

@ -14,6 +14,10 @@
<Tab id="secrets" :title="$t('secrets.secrets')">
<OrgSecretsTab />
</Tab>
<Tab id="registries" :title="$t('registries.registries')">
<OrgRegistriesTab />
</Tab>
</Scaffold>
</template>
@ -23,6 +27,7 @@ import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import Tab from '~/components/layout/scaffold/Tab.vue';
import OrgRegistriesTab from '~/components/org/settings/OrgRegistriesTab.vue';
import OrgSecretsTab from '~/components/org/settings/OrgSecretsTab.vue';
import { inject } from '~/compositions/useInjectProvide';
import useNotifications from '~/compositions/useNotifications';

View file

@ -2,15 +2,15 @@
<Scaffold enable-tabs :go-back="goBack">
<template #title>
<span>
<router-link :to="{ name: 'org', params: { orgId: repo!.org_id } }" class="hover:underline">
{{ repo!.owner }}
<!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -->
</router-link>
<router-link :to="{ name: 'org', params: { orgId: repo!.org_id } }" class="hover:underline">{{
repo!.owner
/* eslint-disable-next-line @intlify/vue-i18n/no-raw-text */
}}</router-link>
/
<router-link :to="{ name: 'repo' }" class="hover:underline">
{{ repo!.name }}
<!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -->
</router-link>
<router-link :to="{ name: 'repo' }" class="hover:underline">{{
repo!.name
/* eslint-disable-next-line @intlify/vue-i18n/no-raw-text */
}}</router-link>
/
{{ $t('settings') }}
</span>
@ -22,7 +22,7 @@
<Tab id="secrets" :title="$t('secrets.secrets')">
<SecretsTab />
</Tab>
<Tab id="registries" :title="$t('repo.settings.registries.registries')">
<Tab id="registries" :title="$t('registries.registries')">
<RegistriesTab />
</Tab>
<Tab id="crons" :title="$t('repo.settings.crons.crons')">

View file

@ -9,8 +9,10 @@
<span class="flex">
<router-link :to="{ name: 'org', params: { orgId: repo.org_id } }" class="hover:underline">{{
repo.owner
/* eslint-disable-next-line @intlify/vue-i18n/no-raw-text */
}}</router-link>
{{ `&nbsp;/&nbsp;${repo.name}` }}
&nbsp;/
{{ repo.name }}
</span>
</template>
<template #titleActions>

View file

@ -10,14 +10,12 @@
>
<template #title>
<span>
<router-link :to="{ name: 'org', params: { orgId: repo.org_id } }" class="hover:underline">
{{ repo.owner }}
<!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -->
</router-link>
<router-link :to="{ name: 'org', params: { orgId: repo.org_id } }" class="hover:underline">{{
repo.owner
/* eslint-disable-next-line @intlify/vue-i18n/no-raw-text */
}}</router-link>
/
<router-link :to="{ name: 'repo' }" class="hover:underline">
{{ repo.name }}
</router-link>
<router-link :to="{ name: 'repo' }" class="hover:underline">{{ repo.name }}</router-link>
</span>
</template>

View file

@ -0,0 +1,46 @@
package woodpecker
import "fmt"
const (
pathGlobalRegistries = "%s/api/registries"
pathGlobalRegistry = "%s/api/registries/%s"
)
// GlobalRegistry returns an global registry by name.
func (c *client) GlobalRegistry(registry string) (*Registry, error) {
out := new(Registry)
uri := fmt.Sprintf(pathGlobalRegistry, c.addr, registry)
err := c.get(uri, out)
return out, err
}
// GlobalRegistryList returns a list of all global registries.
func (c *client) GlobalRegistryList() ([]*Registry, error) {
var out []*Registry
uri := fmt.Sprintf(pathGlobalRegistries, c.addr)
err := c.get(uri, &out)
return out, err
}
// GlobalRegistryCreate creates a global registry.
func (c *client) GlobalRegistryCreate(in *Registry) (*Registry, error) {
out := new(Registry)
uri := fmt.Sprintf(pathGlobalRegistries, c.addr)
err := c.post(uri, in, out)
return out, err
}
// GlobalRegistryUpdate updates a global registry.
func (c *client) GlobalRegistryUpdate(in *Registry) (*Registry, error) {
out := new(Registry)
uri := fmt.Sprintf(pathGlobalRegistry, c.addr, in.Address)
err := c.patch(uri, in, out)
return out, err
}
// GlobalRegistryDelete deletes a global registry.
func (c *client) GlobalRegistryDelete(registry string) error {
uri := fmt.Sprintf(pathGlobalRegistry, c.addr, registry)
return c.delete(uri)
}

View file

@ -138,6 +138,36 @@ type Client interface {
// RegistryDelete deletes a registry.
RegistryDelete(repoID int64, hostname string) error
// OrgRegistry returns an organization registry by address.
OrgRegistry(orgID int64, registry string) (*Registry, error)
// OrgRegistryList returns a list of all organization registries.
OrgRegistryList(orgID int64) ([]*Registry, error)
// OrgRegistryCreate creates an organization registry.
OrgRegistryCreate(orgID int64, registry *Registry) (*Registry, error)
// OrgRegistryUpdate updates an organization registry.
OrgRegistryUpdate(orgID int64, registry *Registry) (*Registry, error)
// OrgRegistryDelete deletes an organization registry.
OrgRegistryDelete(orgID int64, registry string) error
// GlobalRegistry returns an global registry by address.
GlobalRegistry(registry string) (*Registry, error)
// GlobalRegistryList returns a list of all global registries.
GlobalRegistryList() ([]*Registry, error)
// GlobalRegistryCreate creates a global registry.
GlobalRegistryCreate(registry *Registry) (*Registry, error)
// GlobalRegistryUpdate updates a global registry.
GlobalRegistryUpdate(registry *Registry) (*Registry, error)
// GlobalRegistryDelete deletes a global registry.
GlobalRegistryDelete(registry string) error
// Secret returns a secret by name.
Secret(repoID int64, secret string) (*Secret, error)

View file

@ -353,6 +353,144 @@ func (_m *Client) Deploy(repoID int64, pipeline int64, env string, params map[st
return r0, r1
}
// GlobalRegistry provides a mock function with given fields: registry
func (_m *Client) GlobalRegistry(registry string) (*woodpecker.Registry, error) {
ret := _m.Called(registry)
if len(ret) == 0 {
panic("no return value specified for GlobalRegistry")
}
var r0 *woodpecker.Registry
var r1 error
if rf, ok := ret.Get(0).(func(string) (*woodpecker.Registry, error)); ok {
return rf(registry)
}
if rf, ok := ret.Get(0).(func(string) *woodpecker.Registry); ok {
r0 = rf(registry)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*woodpecker.Registry)
}
}
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(registry)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GlobalRegistryCreate provides a mock function with given fields: registry
func (_m *Client) GlobalRegistryCreate(registry *woodpecker.Registry) (*woodpecker.Registry, error) {
ret := _m.Called(registry)
if len(ret) == 0 {
panic("no return value specified for GlobalRegistryCreate")
}
var r0 *woodpecker.Registry
var r1 error
if rf, ok := ret.Get(0).(func(*woodpecker.Registry) (*woodpecker.Registry, error)); ok {
return rf(registry)
}
if rf, ok := ret.Get(0).(func(*woodpecker.Registry) *woodpecker.Registry); ok {
r0 = rf(registry)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*woodpecker.Registry)
}
}
if rf, ok := ret.Get(1).(func(*woodpecker.Registry) error); ok {
r1 = rf(registry)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GlobalRegistryDelete provides a mock function with given fields: registry
func (_m *Client) GlobalRegistryDelete(registry string) error {
ret := _m.Called(registry)
if len(ret) == 0 {
panic("no return value specified for GlobalRegistryDelete")
}
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(registry)
} else {
r0 = ret.Error(0)
}
return r0
}
// GlobalRegistryList provides a mock function with given fields:
func (_m *Client) GlobalRegistryList() ([]*woodpecker.Registry, error) {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for GlobalRegistryList")
}
var r0 []*woodpecker.Registry
var r1 error
if rf, ok := ret.Get(0).(func() ([]*woodpecker.Registry, error)); ok {
return rf()
}
if rf, ok := ret.Get(0).(func() []*woodpecker.Registry); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*woodpecker.Registry)
}
}
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GlobalRegistryUpdate provides a mock function with given fields: registry
func (_m *Client) GlobalRegistryUpdate(registry *woodpecker.Registry) (*woodpecker.Registry, error) {
ret := _m.Called(registry)
if len(ret) == 0 {
panic("no return value specified for GlobalRegistryUpdate")
}
var r0 *woodpecker.Registry
var r1 error
if rf, ok := ret.Get(0).(func(*woodpecker.Registry) (*woodpecker.Registry, error)); ok {
return rf(registry)
}
if rf, ok := ret.Get(0).(func(*woodpecker.Registry) *woodpecker.Registry); ok {
r0 = rf(registry)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*woodpecker.Registry)
}
}
if rf, ok := ret.Get(1).(func(*woodpecker.Registry) error); ok {
r1 = rf(registry)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GlobalSecret provides a mock function with given fields: secret
func (_m *Client) GlobalSecret(secret string) (*woodpecker.Secret, error) {
ret := _m.Called(secret)
@ -599,6 +737,144 @@ func (_m *Client) OrgLookup(orgName string) (*woodpecker.Org, error) {
return r0, r1
}
// OrgRegistry provides a mock function with given fields: orgID, registry
func (_m *Client) OrgRegistry(orgID int64, registry string) (*woodpecker.Registry, error) {
ret := _m.Called(orgID, registry)
if len(ret) == 0 {
panic("no return value specified for OrgRegistry")
}
var r0 *woodpecker.Registry
var r1 error
if rf, ok := ret.Get(0).(func(int64, string) (*woodpecker.Registry, error)); ok {
return rf(orgID, registry)
}
if rf, ok := ret.Get(0).(func(int64, string) *woodpecker.Registry); ok {
r0 = rf(orgID, registry)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*woodpecker.Registry)
}
}
if rf, ok := ret.Get(1).(func(int64, string) error); ok {
r1 = rf(orgID, registry)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// OrgRegistryCreate provides a mock function with given fields: orgID, registry
func (_m *Client) OrgRegistryCreate(orgID int64, registry *woodpecker.Registry) (*woodpecker.Registry, error) {
ret := _m.Called(orgID, registry)
if len(ret) == 0 {
panic("no return value specified for OrgRegistryCreate")
}
var r0 *woodpecker.Registry
var r1 error
if rf, ok := ret.Get(0).(func(int64, *woodpecker.Registry) (*woodpecker.Registry, error)); ok {
return rf(orgID, registry)
}
if rf, ok := ret.Get(0).(func(int64, *woodpecker.Registry) *woodpecker.Registry); ok {
r0 = rf(orgID, registry)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*woodpecker.Registry)
}
}
if rf, ok := ret.Get(1).(func(int64, *woodpecker.Registry) error); ok {
r1 = rf(orgID, registry)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// OrgRegistryDelete provides a mock function with given fields: orgID, registry
func (_m *Client) OrgRegistryDelete(orgID int64, registry string) error {
ret := _m.Called(orgID, registry)
if len(ret) == 0 {
panic("no return value specified for OrgRegistryDelete")
}
var r0 error
if rf, ok := ret.Get(0).(func(int64, string) error); ok {
r0 = rf(orgID, registry)
} else {
r0 = ret.Error(0)
}
return r0
}
// OrgRegistryList provides a mock function with given fields: orgID
func (_m *Client) OrgRegistryList(orgID int64) ([]*woodpecker.Registry, error) {
ret := _m.Called(orgID)
if len(ret) == 0 {
panic("no return value specified for OrgRegistryList")
}
var r0 []*woodpecker.Registry
var r1 error
if rf, ok := ret.Get(0).(func(int64) ([]*woodpecker.Registry, error)); ok {
return rf(orgID)
}
if rf, ok := ret.Get(0).(func(int64) []*woodpecker.Registry); ok {
r0 = rf(orgID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*woodpecker.Registry)
}
}
if rf, ok := ret.Get(1).(func(int64) error); ok {
r1 = rf(orgID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// OrgRegistryUpdate provides a mock function with given fields: orgID, registry
func (_m *Client) OrgRegistryUpdate(orgID int64, registry *woodpecker.Registry) (*woodpecker.Registry, error) {
ret := _m.Called(orgID, registry)
if len(ret) == 0 {
panic("no return value specified for OrgRegistryUpdate")
}
var r0 *woodpecker.Registry
var r1 error
if rf, ok := ret.Get(0).(func(int64, *woodpecker.Registry) (*woodpecker.Registry, error)); ok {
return rf(orgID, registry)
}
if rf, ok := ret.Get(0).(func(int64, *woodpecker.Registry) *woodpecker.Registry); ok {
r0 = rf(orgID, registry)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*woodpecker.Registry)
}
}
if rf, ok := ret.Get(1).(func(int64, *woodpecker.Registry) error); ok {
r1 = rf(orgID, registry)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// OrgSecret provides a mock function with given fields: orgID, secret
func (_m *Client) OrgSecret(orgID int64, secret string) (*woodpecker.Secret, error) {
ret := _m.Called(orgID, secret)

View file

@ -7,6 +7,8 @@ const (
pathOrgLookup = "%s/api/orgs/lookup/%s"
pathOrgSecrets = "%s/api/orgs/%d/secrets"
pathOrgSecret = "%s/api/orgs/%d/secrets/%s"
pathOrgRegistries = "%s/api/orgs/%d/registries"
pathOrgRegistry = "%s/api/orgs/%d/registries/%s"
)
// Org returns an organization by id.
@ -62,3 +64,41 @@ func (c *client) OrgSecretDelete(orgID int64, secret string) error {
uri := fmt.Sprintf(pathOrgSecret, c.addr, orgID, secret)
return c.delete(uri)
}
// OrgRegistry returns an organization registry by address.
func (c *client) OrgRegistry(orgID int64, registry string) (*Registry, error) {
out := new(Registry)
uri := fmt.Sprintf(pathOrgRegistry, c.addr, orgID, registry)
err := c.get(uri, out)
return out, err
}
// OrgRegistryList returns a list of all organization registries.
func (c *client) OrgRegistryList(orgID int64) ([]*Registry, error) {
var out []*Registry
uri := fmt.Sprintf(pathOrgRegistries, c.addr, orgID)
err := c.get(uri, &out)
return out, err
}
// OrgRegistryCreate creates an organization registry.
func (c *client) OrgRegistryCreate(orgID int64, in *Registry) (*Registry, error) {
out := new(Registry)
uri := fmt.Sprintf(pathOrgRegistries, c.addr, orgID)
err := c.post(uri, in, out)
return out, err
}
// OrgRegistryUpdate updates an organization registry.
func (c *client) OrgRegistryUpdate(orgID int64, in *Registry) (*Registry, error) {
out := new(Registry)
uri := fmt.Sprintf(pathOrgRegistry, c.addr, orgID, in.Address)
err := c.patch(uri, in, out)
return out, err
}
// OrgRegistryDelete deletes an organization registry.
func (c *client) OrgRegistryDelete(orgID int64, registry string) error {
uri := fmt.Sprintf(pathOrgRegistry, c.addr, orgID, registry)
return c.delete(uri)
}

View file

@ -18,8 +18,8 @@ const (
pathStop = "%s/api/repos/%d/pipelines/%d/cancel"
pathRepoSecrets = "%s/api/repos/%d/secrets"
pathRepoSecret = "%s/api/repos/%d/secrets/%s"
pathRepoRegistries = "%s/api/repos/%d/registry"
pathRepoRegistry = "%s/api/repos/%d/registry/%s"
pathRepoRegistries = "%s/api/repos/%d/registries"
pathRepoRegistry = "%s/api/repos/%d/registries/%s"
pathRepoCrons = "%s/api/repos/%d/cron"
pathRepoCron = "%s/api/repos/%d/cron/%d"
)

View file

@ -132,6 +132,8 @@ type (
// Registry represents a docker registry with credentials.
Registry struct {
ID int64 `json:"id"`
OrgID int64 `json:"org_id"`
RepoID int64 `json:"repo_id"`
Address string `json:"address"`
Username string `json:"username"`
Password string `json:"password,omitempty"`