diff --git a/main.go b/main.go index 15a3fe7..d27e055 100644 --- a/main.go +++ b/main.go @@ -28,6 +28,7 @@ func main() { showVersion = false confirm = false validate = false + checkNames = false exportMode = false createRepositories = false deleteRepositories = false @@ -36,7 +37,8 @@ func main() { flag.StringVar(&configFile, "config", configFile, "path to the config.yaml") flag.BoolVar(&showVersion, "version", showVersion, "show the Aquayman version and exit") flag.BoolVar(&confirm, "confirm", confirm, "must be set to actually perform any changes on quay.io") - flag.BoolVar(&validate, "validate", validate, "validate the given configuration and then exit") + flag.BoolVar(&validate, "validate", validate, "validate the given configuration syntax and then exit") + flag.BoolVar(&checkNames, "check-names", checkNames, "(only with -validate) validate that users actually exist (requires valid quay.io credentials)") flag.BoolVar(&exportMode, "export", exportMode, "export quay.io state and update the config file (-config flag)") flag.BoolVar(&createRepositories, "create-repos", createRepositories, "create repositories listed in the config file but not existing on quay.io yet") flag.BoolVar(&deleteRepositories, "delete-repos", deleteRepositories, "delete repositories on quay.io that are not listed in the config file") @@ -58,10 +60,21 @@ func main() { log.Fatalf("⚠ Failed to load config %q: %v.", configFile, err) } + var ( + client *quay.Client + ) + // validate config unless in export mode, where an incomplete // configuration is allowed and even expected if !exportMode { - if err := cfg.Validate(); err != nil { + if checkNames { + client, err = quay.NewClient(getToken(), 30*time.Second, true) + if err != nil { + log.Fatalf("⚠ Failed to create quay.io API client: %v.", err) + } + } + + if err := cfg.Validate(ctx, client); err != nil { log.Fatalf("Configuration is invalid: %v", err) } } @@ -71,9 +84,11 @@ func main() { return } - client, err := quay.NewClient(getToken(), 30*time.Second, !confirm) - if err != nil { - log.Fatalf("⚠ Failed to create quay.io API client: %v.", err) + if client == nil { + client, err = quay.NewClient(getToken(), 30*time.Second, !confirm) + if err != nil { + log.Fatalf("⚠ Failed to create quay.io API client: %v.", err) + } } if exportMode { diff --git a/pkg/config/config.go b/pkg/config/config.go index 32e593d..fbbce8d 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -1,6 +1,7 @@ package config import ( + "context" "errors" "fmt" "os" @@ -98,7 +99,7 @@ func validRepositoryRole(role quay.RepositoryRole) bool { return false } -func (c *Config) Validate() error { +func (c *Config) Validate(ctx context.Context, client *quay.Client) error { if c.Organization == "" { return errors.New("no organization configured") } @@ -115,6 +116,36 @@ func (c *Config) Validate() error { } teamNames = append(teamNames, team.Name) + + if client != nil { + for _, member := range team.Members { + if _, err := client.GetUser(ctx, member); err != nil { + return fmt.Errorf("user %q in team %q does not exist: %v", member, team.Name, err) + } + } + } + } + + robotNames := []string{} + robotPattern := regexp.MustCompile(`^[a-z][a-z0-9_]{1,254}$`) + prefix := c.Organization + "+" + + for _, robot := range c.Robots { + fullName := fmt.Sprintf("%s+%s", c.Organization, robot.Name) + + if util.StringSliceContains(robotNames, fullName) { + return fmt.Errorf("duplicate robot %q defined", robot.Name) + } + + if strings.HasPrefix(robot.Name, prefix) { + return fmt.Errorf("robot %q must be given as a short name, without the organization prefix (must be \"%s\")", robot.Name, strings.TrimPrefix(robot.Name, prefix)) + } + + if !robotPattern.MatchString(robot.Name) { + return fmt.Errorf("robot %q has an invalid name, must be alphanumeric lowercase", robot.Name) + } + + robotNames = append(robotNames, fullName) } repoNames := []string{} @@ -133,6 +164,10 @@ func (c *Config) Validate() error { } for teamName, roleName := range repo.Teams { + if !util.StringSliceContains(teamNames, teamName) { + return fmt.Errorf("invalid team %q assigned to repo %q: team does not exist", teamName, repo.Name) + } + if !validRepositoryRole(roleName) { return fmt.Errorf("role for team %s in repo %q is invalid (%q), must be one of %v", teamName, repo.Name, roleName, quay.AllRepositoryRoles) } @@ -142,29 +177,19 @@ func (c *Config) Validate() error { if !validRepositoryRole(roleName) { return fmt.Errorf("role for user %s in repo %q is invalid (%q), must be one of %v", userName, repo.Name, roleName, quay.AllRepositoryRoles) } - } - - repoNames = append(repoNames, repo.Name) - } - - robotNames := []string{} - robotPattern := regexp.MustCompile(`^[a-z][a-z0-9_]{1,254}$`) - prefix := c.Organization + "+" - - for _, robot := range c.Robots { - if util.StringSliceContains(robotNames, robot.Name) { - return fmt.Errorf("duplicate robot %q defined", robot.Name) - } - if strings.HasPrefix(robot.Name, prefix) { - return fmt.Errorf("robot %q must be given as a short name, without the organization prefix (must be \"%s\")", robot.Name, strings.TrimPrefix(robot.Name, prefix)) - } - - if !robotPattern.MatchString(robot.Name) { - return fmt.Errorf("robot %q has an invalid name, must be alphanumeric lowercase", robot.Name) + if quay.IsRobotUsername(userName) { + if !util.StringSliceContains(robotNames, userName) { + return fmt.Errorf("invalid robot %q assigned to repo %q: robot does not exist", userName, repo.Name) + } + } else if client != nil { + if _, err := client.GetUser(ctx, userName); err != nil { + return fmt.Errorf("invalid user %q assigned to repo %q: user does not exist", userName, repo.Name) + } + } } - robotNames = append(robotNames, robot.Name) + repoNames = append(repoNames, repo.Name) } return nil diff --git a/pkg/quay/robot.go b/pkg/quay/robot.go index 788e1b5..87745f6 100644 --- a/pkg/quay/robot.go +++ b/pkg/quay/robot.go @@ -8,6 +8,10 @@ import ( "strings" ) +func IsRobotUsername(name string) bool { + return strings.Contains(name, "+") +} + type Robot struct { Name string `json:"name"` Description string `json:"description"` diff --git a/pkg/quay/user.go b/pkg/quay/user.go new file mode 100644 index 0000000..47bb71d --- /dev/null +++ b/pkg/quay/user.go @@ -0,0 +1,19 @@ +package quay + +import ( + "context" + "fmt" + "net/url" +) + +type User struct { + Username string `json:"username"` +} + +func (c *Client) GetUser(ctx context.Context, username string) (*User, error) { + response := User{} + path := fmt.Sprintf("/users/%s", url.PathEscape(username)) + err := c.call(ctx, "GET", path, nil, nil, &response) + + return &response, err +}