You've already forked woodpecker
							
							
				mirror of
				https://github.com/woodpecker-ci/woodpecker.git
				synced 2025-10-30 23:27:39 +02:00 
			
		
		
		
	Global and organization registries (#1672)
Co-authored-by: Anbraten <6918444+anbraten@users.noreply.github.com>
This commit is contained in:
		
							
								
								
									
										30
									
								
								cli/admin/admin.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								cli/admin/admin.go
									
									
									
									
									
										Normal 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, | ||||
| 	}, | ||||
| } | ||||
| @@ -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, | ||||
							
								
								
									
										75
									
								
								cli/admin/registry/registry_add.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								cli/admin/registry/registry_add.go
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
							
								
								
									
										62
									
								
								cli/admin/registry/registry_info.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								cli/admin/registry/registry_info.go
									
									
									
									
									
										Normal 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) | ||||
| } | ||||
							
								
								
									
										65
									
								
								cli/admin/registry/registry_list.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								cli/admin/registry/registry_list.go
									
									
									
									
									
										Normal 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 }} | ||||
| ` | ||||
							
								
								
									
										45
									
								
								cli/admin/registry/registry_rm.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								cli/admin/registry/registry_rm.go
									
									
									
									
									
										Normal 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) | ||||
| } | ||||
							
								
								
									
										78
									
								
								cli/admin/registry/registry_set.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								cli/admin/registry/registry_set.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										30
									
								
								cli/org/org.go
									
									
									
									
									
										Normal 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, | ||||
| 	}, | ||||
| } | ||||
							
								
								
									
										60
									
								
								cli/org/registry/registry.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								cli/org/registry/registry.go
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
							
								
								
									
										83
									
								
								cli/org/registry/registry_add.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								cli/org/registry/registry_add.go
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
							
								
								
									
										69
									
								
								cli/org/registry/registry_info.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								cli/org/registry/registry_info.go
									
									
									
									
									
										Normal 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) | ||||
| } | ||||
							
								
								
									
										72
									
								
								cli/org/registry/registry_list.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								cli/org/registry/registry_list.go
									
									
									
									
									
										Normal 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 }} | ||||
| ` | ||||
							
								
								
									
										53
									
								
								cli/org/registry/registry_rm.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								cli/org/registry/registry_rm.go
									
									
									
									
									
										Normal 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) | ||||
| } | ||||
							
								
								
									
										84
									
								
								cli/org/registry/registry_set.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								cli/org/registry/registry_set.go
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
							
								
								
									
										44
									
								
								cli/repo/registry/registry.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								cli/repo/registry/registry.go
									
									
									
									
									
										Normal 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) | ||||
| } | ||||
| @@ -50,22 +50,15 @@ var registryCreateCmd = &cli.Command{ | ||||
| 
 | ||||
| func registryCreate(c *cli.Context) error { | ||||
| 	var ( | ||||
| 		hostname         = c.String("hostname") | ||||
| 		username         = c.String("username") | ||||
| 		password         = c.String("password") | ||||
| 		repoIDOrFullName = c.String("repository") | ||||
| 		hostname = c.String("hostname") | ||||
| 		username = c.String("username") | ||||
| 		password = c.String("password") | ||||
| 	) | ||||
| 	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 | ||||
| } | ||||
| @@ -42,25 +42,25 @@ var registryInfoCmd = &cli.Command{ | ||||
| 
 | ||||
| func registryInfo(c *cli.Context) error { | ||||
| 	var ( | ||||
| 		hostname         = c.String("hostname") | ||||
| 		repoIDOrFullName = c.String("repository") | ||||
| 		format           = c.String("format") + "\n" | ||||
| 		hostname = c.String("hostname") | ||||
| 		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 | ||||
| @@ -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 | ||||
| @@ -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) | ||||
| } | ||||
| @@ -50,22 +50,16 @@ var registryUpdateCmd = &cli.Command{ | ||||
| 
 | ||||
| func registryUpdate(c *cli.Context) error { | ||||
| 	var ( | ||||
| 		hostname         = c.String("hostname") | ||||
| 		username         = c.String("username") | ||||
| 		password         = c.String("password") | ||||
| 		repoIDOrFullName = c.String("repository") | ||||
| 		hostname = c.String("hostname") | ||||
| 		username = c.String("username") | ||||
| 		password = c.String("password") | ||||
| 	) | ||||
| 	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 | ||||
| } | ||||
| @@ -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, | ||||
| 	}, | ||||
| } | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -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" | ||||
|                 } | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										170
									
								
								server/api/global_registry.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								server/api/global_registry.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										207
									
								
								server/api/org_registry.go
									
									
									
									
									
										Normal 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) | ||||
| } | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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, | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -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) | ||||
| 	} | ||||
|   | ||||
| @@ -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()) | ||||
|   | ||||
| @@ -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 | ||||
| 	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 := 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.RegistryList(repo, &model.ListOptions{All: true}) | ||||
| 		list, err := registry.GlobalRegistryList(&model.ListOptions{All: true}) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		registries = append(registries, list...) | ||||
| 		for _, reg := range list { | ||||
| 			if _, ok := exists[reg.Address]; ok { | ||||
| 				continue | ||||
| 			} | ||||
| 			exists[reg.Address] = struct{}{} | ||||
| 			registries = append(registries, reg) | ||||
| 		} | ||||
| 	} | ||||
| 	return model.ApplyPagination(p, registries), nil | ||||
|  | ||||
| 	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) | ||||
| } | ||||
|   | ||||
| @@ -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) | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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) | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
| 	} | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
							
								
								
									
										33
									
								
								server/store/datastore/migration/032_registries_add_user.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								server/store/datastore/migration/032_registries_add_user.go
									
									
									
									
									
										Normal 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) | ||||
| 	}, | ||||
| } | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -61,6 +61,7 @@ var migrationTasks = []*xormigrate.Migration{ | ||||
| 	&cleanRegistryPipeline, | ||||
| 	&setForgeID, | ||||
| 	&unifyColumnsTables, | ||||
| 	&alterTableRegistriesFixRequiredFields, | ||||
| } | ||||
|  | ||||
| var allBeans = []any{ | ||||
|   | ||||
| @@ -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(®s) | ||||
| 	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(®s) | ||||
| } | ||||
|  | ||||
| func (s storage) RegistryListAll() ([]*model.Registry, error) { | ||||
| 	var registries []*model.Registry | ||||
| 	return registries, s.engine.Find(®istries) | ||||
| } | ||||
|  | ||||
| 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(®istries) | ||||
| } | ||||
|  | ||||
| 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(®istries) | ||||
| } | ||||
|   | ||||
| @@ -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()) | ||||
| } | ||||
|   | ||||
| @@ -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) | ||||
| 	} | ||||
|   | ||||
| @@ -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
									
									
								
							
							
						
						
									
										6
									
								
								web/components.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -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'] | ||||
|   | ||||
| @@ -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}", | ||||
|   | ||||
							
								
								
									
										101
									
								
								web/src/components/admin/settings/AdminRegistriesTab.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								web/src/components/admin/settings/AdminRegistriesTab.vue
									
									
									
									
									
										Normal 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> | ||||
| @@ -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, | ||||
|   | ||||
							
								
								
									
										113
									
								
								web/src/components/org/settings/OrgRegistriesTab.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								web/src/components/org/settings/OrgRegistriesTab.vue
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										78
									
								
								web/src/components/registry/RegistryEdit.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								web/src/components/registry/RegistryEdit.vue
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										63
									
								
								web/src/components/registry/RegistryList.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								web/src/components/registry/RegistryList.vue
									
									
									
									
									
										Normal 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> | ||||
| @@ -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" | ||||
|         /> | ||||
|         <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)" | ||||
|         /> | ||||
|       </ListItem> | ||||
|     <RegistryList | ||||
|       v-if="!selectedRegistry" | ||||
|       v-model="registries" | ||||
|       :is-deleting="isDeleting" | ||||
|       @edit="editRegistry" | ||||
|       @delete="deleteRegistry" | ||||
|     /> | ||||
|  | ||||
|       <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> | ||||
|     <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 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> | ||||
|   | ||||
| @@ -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> { | ||||
|   | ||||
| @@ -1,6 +1,9 @@ | ||||
| export interface Registry { | ||||
|   id: string; | ||||
|   repo_id: number; | ||||
|   org_id: number; | ||||
|   address: string; | ||||
|   username: string; | ||||
|   password: string; | ||||
|   readonly: boolean; | ||||
| } | ||||
|   | ||||
| @@ -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'; | ||||
|   | ||||
| @@ -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'; | ||||
|   | ||||
| @@ -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')"> | ||||
|   | ||||
| @@ -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> | ||||
|         {{ ` / ${repo.name}` }} | ||||
|          / | ||||
|         {{ repo.name }} | ||||
|       </span> | ||||
|     </template> | ||||
|     <template #titleActions> | ||||
|   | ||||
| @@ -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> | ||||
|  | ||||
|   | ||||
							
								
								
									
										46
									
								
								woodpecker-go/woodpecker/global_registry.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								woodpecker-go/woodpecker/global_registry.go
									
									
									
									
									
										Normal 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) | ||||
| } | ||||
| @@ -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) | ||||
|  | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -3,10 +3,12 @@ package woodpecker | ||||
| import "fmt" | ||||
|  | ||||
| const ( | ||||
| 	pathOrg        = "%s/api/orgs/%d" | ||||
| 	pathOrgLookup  = "%s/api/orgs/lookup/%s" | ||||
| 	pathOrgSecrets = "%s/api/orgs/%d/secrets" | ||||
| 	pathOrgSecret  = "%s/api/orgs/%d/secrets/%s" | ||||
| 	pathOrg           = "%s/api/orgs/%d" | ||||
| 	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) | ||||
| } | ||||
|   | ||||
| @@ -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" | ||||
| ) | ||||
|   | ||||
| @@ -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"` | ||||
|   | ||||
		Reference in New Issue
	
	Block a user