From 61eefbaf6c716855acfe1918596f210c4427b992 Mon Sep 17 00:00:00 2001 From: Steven Zou Date: Fri, 8 Dec 2017 16:13:09 +0800 Subject: [PATCH] add api testing for harbor including image pull and push --- .../api-testing/client/docker_client.go | 97 ++++++++++ .../api-testing/client/harbor_api_client.go | 170 +++++++++++++++++ .../apitests/api-testing/envs/concourse_ci.go | 17 ++ .../apitests/api-testing/envs/environment.go | 127 +++++++++++++ tests/apitests/api-testing/lib/image.go | 137 ++++++++++++++ tests/apitests/api-testing/lib/project.go | 169 +++++++++++++++++ tests/apitests/api-testing/lib/report.go | 43 +++++ tests/apitests/api-testing/lib/system.go | 50 +++++ tests/apitests/api-testing/lib/user.go | 118 ++++++++++++ tests/apitests/api-testing/models/endpoint.go | 10 + tests/apitests/api-testing/models/image.go | 20 ++ tests/apitests/api-testing/models/member.go | 7 + tests/apitests/api-testing/models/project.go | 18 ++ .../api-testing/models/replication.go | 10 + .../api-testing/models/system_info.go | 7 + tests/apitests/api-testing/models/user.go | 16 ++ .../api-testing/tests/suites/suite.go | 11 ++ .../tests/suites/suite01/run_test.go | 22 +++ .../api-testing/tests/suites/suite01/suite.go | 177 ++++++++++++++++++ 19 files changed, 1226 insertions(+) create mode 100644 tests/apitests/api-testing/client/docker_client.go create mode 100644 tests/apitests/api-testing/client/harbor_api_client.go create mode 100644 tests/apitests/api-testing/envs/concourse_ci.go create mode 100644 tests/apitests/api-testing/envs/environment.go create mode 100644 tests/apitests/api-testing/lib/image.go create mode 100644 tests/apitests/api-testing/lib/project.go create mode 100644 tests/apitests/api-testing/lib/report.go create mode 100644 tests/apitests/api-testing/lib/system.go create mode 100644 tests/apitests/api-testing/lib/user.go create mode 100644 tests/apitests/api-testing/models/endpoint.go create mode 100644 tests/apitests/api-testing/models/image.go create mode 100644 tests/apitests/api-testing/models/member.go create mode 100644 tests/apitests/api-testing/models/project.go create mode 100644 tests/apitests/api-testing/models/replication.go create mode 100644 tests/apitests/api-testing/models/system_info.go create mode 100644 tests/apitests/api-testing/models/user.go create mode 100644 tests/apitests/api-testing/tests/suites/suite.go create mode 100644 tests/apitests/api-testing/tests/suites/suite01/run_test.go create mode 100644 tests/apitests/api-testing/tests/suites/suite01/suite.go diff --git a/tests/apitests/api-testing/client/docker_client.go b/tests/apitests/api-testing/client/docker_client.go new file mode 100644 index 000000000..0ee6fee6f --- /dev/null +++ b/tests/apitests/api-testing/client/docker_client.go @@ -0,0 +1,97 @@ +package client + +import "os/exec" +import "strings" +import "errors" +import "bufio" +import "fmt" + +//DockerClient : Run docker commands +type DockerClient struct{} + +//Status : Check if docker daemon is there +func (dc *DockerClient) Status() error { + cmdName := "docker" + args := []string{"info"} + + return dc.runCommand(cmdName, args) +} + +//Pull : Pull image +func (dc *DockerClient) Pull(image string) error { + if len(strings.TrimSpace(image)) == 0 { + return errors.New("Empty image") + } + + cmdName := "docker" + args := []string{"pull", image} + + return dc.runCommandWithOutput(cmdName, args) +} + +//Tag :Tag image +func (dc *DockerClient) Tag(source, target string) error { + if len(strings.TrimSpace(source)) == 0 || + len(strings.TrimSpace(target)) == 0 { + return errors.New("Empty images") + } + + cmdName := "docker" + args := []string{"tag", source, target} + + return dc.runCommandWithOutput(cmdName, args) +} + +//Push : push image +func (dc *DockerClient) Push(image string) error { + if len(strings.TrimSpace(image)) == 0 { + return errors.New("Empty image") + } + + cmdName := "docker" + args := []string{"push", image} + + return dc.runCommandWithOutput(cmdName, args) +} + +//Login : Login docker +func (dc *DockerClient) Login(userName, password string, uri string) error { + if len(strings.TrimSpace(userName)) == 0 || + len(strings.TrimSpace(password)) == 0 { + return errors.New("Invlaid credential") + } + + cmdName := "docker" + args := []string{"login", "-u", userName, "-p", password, uri} + + return dc.runCommandWithOutput(cmdName, args) +} + +func (dc *DockerClient) runCommand(cmdName string, args []string) error { + return exec.Command(cmdName, args...).Run() +} + +func (dc *DockerClient) runCommandWithOutput(cmdName string, args []string) error { + cmd := exec.Command(cmdName, args...) + cmdReader, err := cmd.StdoutPipe() + if err != nil { + return err + } + + scanner := bufio.NewScanner(cmdReader) + go func() { + for scanner.Scan() { + fmt.Printf("%s out | %s\n", cmdName, scanner.Text()) + } + }() + + if err = cmd.Start(); err != nil { + return err + } + + if err = cmd.Wait(); err != nil { + return err + } + + return nil +} diff --git a/tests/apitests/api-testing/client/harbor_api_client.go b/tests/apitests/api-testing/client/harbor_api_client.go new file mode 100644 index 000000000..71c8ec1df --- /dev/null +++ b/tests/apitests/api-testing/client/harbor_api_client.go @@ -0,0 +1,170 @@ +package client + +import ( + "crypto/tls" + "crypto/x509" + "errors" + "io/ioutil" + "net/http" + "strings" +) + +const ( + httpHeaderJSON = "application/json" + httpHeaderContentType = "Content-Type" + httpHeaderAccept = "Accept" +) + +//APIClientConfig : Keep config options for APIClient +type APIClientConfig struct { + Username string + Password string + CaFile string + CertFile string + KeyFile string +} + +//APIClient provided the http client for trigger http requests +type APIClient struct { + //http client + client *http.Client + + //Configuration + config APIClientConfig +} + +//NewAPIClient is constructor of APIClient +func NewAPIClient(config APIClientConfig) (*APIClient, error) { + //Load client cert + cert, err := tls.LoadX509KeyPair(config.CertFile, config.KeyFile) + if err != nil { + return nil, err + } + + //Add ca + caCert, err := ioutil.ReadFile(config.CaFile) + if err != nil { + return nil, err + } + + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(caCert) + + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{cert}, + RootCAs: caCertPool, + } + + tlsConfig.BuildNameToCertificate() + transport := &http.Transport{ + TLSClientConfig: tlsConfig, + } + + client := &http.Client{ + Transport: transport, + } + + return &APIClient{ + client: client, + config: config, + }, nil + +} + +//Get data +func (ac *APIClient) Get(url string) ([]byte, error) { + if strings.TrimSpace(url) == "" { + return nil, errors.New("empty url") + } + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + + req.Header.Set(httpHeaderAccept, httpHeaderJSON) + req.SetBasicAuth(ac.config.Username, ac.config.Password) + + resp, err := ac.client.Do(req) + if err != nil { + return nil, err + } + defer func() { + if resp.Body != nil { + resp.Body.Close() + } + }() + if resp.StatusCode != http.StatusOK { + return nil, errors.New(resp.Status) + } + + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + return data, nil +} + +//Post data +func (ac *APIClient) Post(url string, data []byte) error { + if strings.TrimSpace(url) == "" { + return errors.New("Empty url") + } + + req, err := http.NewRequest("POST", url, strings.NewReader(string(data))) + if err != nil { + return err + } + + req.Header.Set(httpHeaderContentType, httpHeaderJSON) + req.SetBasicAuth(ac.config.Username, ac.config.Password) + resp, err := ac.client.Do(req) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusCreated && + resp.StatusCode != http.StatusOK { + return errors.New(resp.Status) + } + + return nil +} + +//Delete data +func (ac *APIClient) Delete(url string) error { + if strings.TrimSpace(url) == "" { + return errors.New("Empty url") + } + + req, err := http.NewRequest("DELETE", url, nil) + if err != nil { + return err + } + + req.Header.Set(httpHeaderAccept, httpHeaderJSON) + req.SetBasicAuth(ac.config.Username, ac.config.Password) + + resp, err := ac.client.Do(req) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusOK { + return errors.New(resp.Status) + } + + return nil +} + +//SwitchAccount : Switch account +func (ac *APIClient) SwitchAccount(username, password string) { + if len(strings.TrimSpace(username)) == 0 || + len(strings.TrimSpace(password)) == 0 { + return + } + + ac.config.Username = username + ac.config.Password = password +} diff --git a/tests/apitests/api-testing/envs/concourse_ci.go b/tests/apitests/api-testing/envs/concourse_ci.go new file mode 100644 index 000000000..89c1c59d3 --- /dev/null +++ b/tests/apitests/api-testing/envs/concourse_ci.go @@ -0,0 +1,17 @@ +package envs + +//ConcourseCIEnv : Env for concourse pipeline +var ConcourseCIEnv = Environment{ + Protocol: "https", + TestingProject: "concoursecitesting01", + ImageName: "busybox", + ImageTag: "latest", + CAFile: "../../../ca.crt", + KeyFile: "../../../key.crt", + CertFile: "../../../cert.crt", + Account: "cody", + Password: "Admin!23", + Admin: "admin", + AdminPass: "pksxgxmifc0cnwa5px9h", + Hostname: "10.112.122.1", +} diff --git a/tests/apitests/api-testing/envs/environment.go b/tests/apitests/api-testing/envs/environment.go new file mode 100644 index 000000000..1e25f0f48 --- /dev/null +++ b/tests/apitests/api-testing/envs/environment.go @@ -0,0 +1,127 @@ +package envs + +import ( + "fmt" + "os" + "strings" + + "github.com/vmware/harbor/tests/apitests/api-testing/client" +) + +//Environment keeps the testing env info +type Environment struct { + Protocol string //env var: HTTP_PROTOCOL + Hostname string //env var: TESTING_ENV_HOSTNAME + Account string //env var: TESTING_ENV_ACCOUNT + Password string //env var: TESTING_ENV_PASSWORD + Admin string //env var: TESTING_ENV_ADMIN + AdminPass string //env var: TESTING_ENV_ADMIN_PASS + TestingProject string //env var: TESTING_PROJECT_NAME + ImageName string //env var: TESTING_IMAGE_NAME + ImageTag string //env var: TESTING_IMAGE_TAG + CAFile string //env var: CA_FILE_PATH + CertFile string //env var: CERT_FILE_PATH + KeyFile string //env var: KEY_FILE_PATH + + //API client + HTTPClient *client.APIClient + + //Docker client + DockerClient *client.DockerClient + + //Initialize status + loaded bool +} + +//Load test env info +func (env *Environment) Load() error { + host := os.Getenv("TESTING_ENV_HOSTNAME") + if isNotEmpty(host) { + env.Hostname = host + } + + account := os.Getenv("TESTING_ENV_ACCOUNT") + if isNotEmpty(account) { + env.Account = account + } + + pwd := os.Getenv("TESTING_ENV_PASSWORD") + if isNotEmpty(pwd) { + env.Password = pwd + } + + admin := os.Getenv("TESTING_ENV_ADMIN") + if isNotEmpty(admin) { + env.Admin = admin + } + + adminPwd := os.Getenv("TESTING_ENV_ADMIN_PASS") + if isNotEmpty(adminPwd) { + env.AdminPass = adminPwd + } + + pro := os.Getenv("TESTING_PROJECT_NAME") + if isNotEmpty(pro) { + env.TestingProject = pro + } + + imgName := os.Getenv("TESTING_IMAGE_NAME") + if isNotEmpty(imgName) { + env.ImageName = imgName + } + + imgTag := os.Getenv("TESTING_IMAGE_TAG") + if isNotEmpty(imgTag) { + env.ImageTag = imgTag + } + + protocol := os.Getenv("HTTP_PROTOCOL") + if isNotEmpty(protocol) { + env.Protocol = protocol + } + + caFile := os.Getenv("CA_FILE_PATH") + if isNotEmpty(caFile) { + env.CAFile = caFile + } + + keyFile := os.Getenv("KEY_FILE_PATH") + if isNotEmpty(keyFile) { + env.KeyFile = keyFile + } + + certFile := os.Getenv("CERT_FILE_PATH") + if isNotEmpty(certFile) { + env.CertFile = certFile + } + + if !env.loaded { + cfg := client.APIClientConfig{ + Username: env.Admin, + Password: env.AdminPass, + CaFile: env.CAFile, + CertFile: env.CertFile, + KeyFile: env.KeyFile, + } + + httpClient, err := client.NewAPIClient(cfg) + if err != nil { + return err + } + env.HTTPClient = httpClient + env.DockerClient = &client.DockerClient{} + + env.loaded = true + } + + return nil +} + +//RootURI : The root URI like https:// +func (env *Environment) RootURI() string { + return fmt.Sprintf("%s://%s", env.Protocol, env.Hostname) +} + +func isNotEmpty(str string) bool { + return len(strings.TrimSpace(str)) > 0 +} diff --git a/tests/apitests/api-testing/lib/image.go b/tests/apitests/api-testing/lib/image.go new file mode 100644 index 000000000..6775d7478 --- /dev/null +++ b/tests/apitests/api-testing/lib/image.go @@ -0,0 +1,137 @@ +package lib + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + "time" + + "github.com/vmware/harbor/tests/apitests/api-testing/client" + "github.com/vmware/harbor/tests/apitests/api-testing/models" +) + +//ImageUtil : For repository and tag functions +type ImageUtil struct { + rootURI string + testingClient *client.APIClient +} + +//NewImageUtil : Constructor +func NewImageUtil(rootURI string, httpClient *client.APIClient) *ImageUtil { + if len(strings.TrimSpace(rootURI)) == 0 || httpClient == nil { + return nil + } + + return &ImageUtil{ + rootURI: rootURI, + testingClient: httpClient, + } +} + +//DeleteRepo : Delete repo +func (iu *ImageUtil) DeleteRepo(repoName string) error { + if len(strings.TrimSpace(repoName)) == 0 { + return errors.New("Empty repo name for deleting") + } + + url := fmt.Sprintf("%s%s%s", iu.rootURI, "/api/repositories/", repoName) + if err := iu.testingClient.Delete(url); err != nil { + return err + } + + return nil +} + +//ScanTag :Scan a tag +func (iu *ImageUtil) ScanTag(repoName string, tagName string) error { + if len(strings.TrimSpace(repoName)) == 0 { + return errors.New("Empty repo name for scanning") + } + + if len(strings.TrimSpace(tagName)) == 0 { + return errors.New("Empty tag name for scanning") + } + + url := fmt.Sprintf("%s%s%s%s%s%s", iu.rootURI, "/api/repositories/", repoName, "/tags/", tagName, "/scan") + if err := iu.testingClient.Post(url, nil); err != nil { + return err + } + + tk := time.NewTicker(1 * time.Second) + defer tk.Stop() + done := make(chan bool) + errchan := make(chan error) + url = fmt.Sprintf("%s%s%s%s%s", iu.rootURI, "/api/repositories/", repoName, "/tags/", tagName) + go func() { + for _ = range tk.C { + data, err := iu.testingClient.Get(url) + if err != nil { + errchan <- err + return + } + var tag models.Tag + if err = json.Unmarshal(data, &tag); err != nil { + errchan <- err + return + } + + if tag.ScanOverview != nil && tag.ScanOverview.Status == "finished" { + done <- true + } + } + }() + + select { + case <-done: + return nil + case <-time.After(20 * time.Second): + return errors.New("Scan timeout after 30 seconds") + } +} + +//GetRepos : Get repos in the project +func (iu *ImageUtil) GetRepos(projectName string) ([]models.Repository, error) { + if len(strings.TrimSpace(projectName)) == 0 { + return nil, errors.New("Empty project name for getting repos") + } + + proUtil := NewProjectUtil(iu.rootURI, iu.testingClient) + pid := proUtil.GetProjectID(projectName) + if pid == -1 { + return nil, fmt.Errorf("Failed to get project ID with name %s", projectName) + } + + url := fmt.Sprintf("%s%s%d", iu.rootURI, "/api/repositories?project_id=", pid) + data, err := iu.testingClient.Get(url) + if err != nil { + return nil, err + } + + var repos []models.Repository + if err = json.Unmarshal(data, &repos); err != nil { + return nil, err + } + + return repos, nil +} + +//GetTags : Get tags +func (iu *ImageUtil) GetTags(repoName string) ([]models.Tag, error) { + if len(strings.TrimSpace(repoName)) == 0 { + return nil, errors.New("Empty repository name for getting tags") + } + + url := fmt.Sprintf("%s%s%s%s", iu.rootURI, "/api/repositories/", repoName, "/tags") + tagData, err := iu.testingClient.Get(url) + if err != nil { + return nil, err + } + + var tags []models.Tag + if err = json.Unmarshal(tagData, &tags); err != nil { + return nil, err + } + + return tags, nil +} diff --git a/tests/apitests/api-testing/lib/project.go b/tests/apitests/api-testing/lib/project.go new file mode 100644 index 000000000..c00d3e836 --- /dev/null +++ b/tests/apitests/api-testing/lib/project.go @@ -0,0 +1,169 @@ +package lib + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + + "github.com/vmware/harbor/tests/apitests/api-testing/client" + "github.com/vmware/harbor/tests/apitests/api-testing/models" +) + +//ProjectUtil : Util methods for project related +type ProjectUtil struct { + rootURI string + testingClient *client.APIClient +} + +//NewProjectUtil : Constructor +func NewProjectUtil(rootURI string, httpClient *client.APIClient) *ProjectUtil { + if len(strings.TrimSpace(rootURI)) == 0 || httpClient == nil { + return nil + } + + return &ProjectUtil{ + rootURI: rootURI, + testingClient: httpClient, + } +} + +//GetProjects : Get projects +//If name specified, then only get the specified project +func (pu *ProjectUtil) GetProjects(name string) ([]models.ExistingProject, error) { + url := pu.rootURI + "/api/projects" + if len(strings.TrimSpace(name)) > 0 { + url = url + "?name=" + name + } + data, err := pu.testingClient.Get(url) + if err != nil { + return nil, err + } + + var pros []models.ExistingProject + if err = json.Unmarshal(data, &pros); err != nil { + return nil, err + } + + return pros, nil +} + +//GetProjectID : Get the project ID +//If no project existing with the name, then return -1 +func (pu *ProjectUtil) GetProjectID(projectName string) int { + pros, err := pu.GetProjects(projectName) + if err != nil { + return -1 + } + + if len(pros) == 0 { + return -1 + } + + for _, pro := range pros { + if pro.Name == projectName { + return pro.ID + } + } + + return -1 +} + +//CreateProject :Create project +func (pu *ProjectUtil) CreateProject(projectName string, accessLevel bool) error { + if len(strings.TrimSpace(projectName)) == 0 { + return errors.New("Empty project name for creating") + } + + p := models.Project{ + Name: projectName, + Metadata: &models.Metadata{ + AccessLevel: fmt.Sprintf("%v", accessLevel), + }, + } + + body, err := json.Marshal(&p) + if err != nil { + return err + } + + url := pu.rootURI + "/api/projects" + if err = pu.testingClient.Post(url, body); err != nil { + return err + } + + return nil +} + +//DeleteProject : Delete project +func (pu *ProjectUtil) DeleteProject(projectName string) error { + if len(strings.TrimSpace(projectName)) == 0 { + return errors.New("Empty project name for deleting") + } + + pid := pu.GetProjectID(projectName) + if pid == -1 { + return errors.New("Failed to get project ID") + } + + url := fmt.Sprintf("%s%s%d", pu.rootURI, "/api/projects/", pid) + + if err := pu.testingClient.Delete(url); err != nil { + return err + } + + return nil +} + +//AssignRole : Assign role to user +func (pu *ProjectUtil) AssignRole(projectName, username string) error { + if len(strings.TrimSpace(projectName)) == 0 || + len(strings.TrimSpace(username)) == 0 { + return errors.New("Project name and username are required for assigning role") + } + + pid := pu.GetProjectID(projectName) + if pid == -1 { + return fmt.Errorf("Failed to get project ID with name %s", projectName) + } + + m := models.Member{ + UserName: username, + Roles: []int{2}, + } + + body, err := json.Marshal(&m) + if err != nil { + return err + } + + url := fmt.Sprintf("%s%s%d%s", pu.rootURI, "/api/projects/", pid, "/members") + if err := pu.testingClient.Post(url, body); err != nil { + return err + } + + return nil +} + +//RevokeRole : RevokeRole role from user +func (pu *ProjectUtil) RevokeRole(projectName string, uid int) error { + if len(strings.TrimSpace(projectName)) == 0 { + return errors.New("Project name is required for revoking role") + } + + if uid == 0 { + return errors.New("User ID is required for revoking role") + } + + pid := pu.GetProjectID(projectName) + if pid == -1 { + return fmt.Errorf("Failed to get project ID with name %s", projectName) + } + + url := fmt.Sprintf("%s%s%d%s%d", pu.rootURI, "/api/projects/", pid, "/members/", uid) + if err := pu.testingClient.Delete(url); err != nil { + return err + } + + return nil +} diff --git a/tests/apitests/api-testing/lib/report.go b/tests/apitests/api-testing/lib/report.go new file mode 100644 index 000000000..abda88a5f --- /dev/null +++ b/tests/apitests/api-testing/lib/report.go @@ -0,0 +1,43 @@ +package lib + +import ( + "fmt" +) + +type Report struct { + passed []string + failed []string +} + +//Passed case +func (r *Report) Passed(caseName string) { + r.passed = append(r.passed, fmt.Sprintf("%s: [%s]", caseName, "PASSED")) +} + +//Failed case +func (r *Report) Failed(caseName string, err error) { + r.failed = append(r.failed, fmt.Sprintf("%s: [%s] %s", caseName, "FAILED", err.Error())) +} + +//Print report +func (r *Report) Print() { + passed := len(r.passed) + failed := len(r.failed) + total := passed + failed + + fmt.Println("=====================================") + fmt.Printf("Overall: %d/%d passed , %d/%d failed\n", passed, total, failed, total) + fmt.Println("=====================================") + for _, res := range r.passed { + fmt.Println(res) + } + + for _, res := range r.failed { + fmt.Println(res) + } +} + +//IsFail : Overall result +func (r *Report) IsFail() bool { + return len(r.failed) > 0 +} diff --git a/tests/apitests/api-testing/lib/system.go b/tests/apitests/api-testing/lib/system.go new file mode 100644 index 000000000..c7cc77d3d --- /dev/null +++ b/tests/apitests/api-testing/lib/system.go @@ -0,0 +1,50 @@ +package lib + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/vmware/harbor/tests/apitests/api-testing/client" + "github.com/vmware/harbor/tests/apitests/api-testing/models" +) + +//SystemUtil : For getting system info +type SystemUtil struct { + rootURI string + hostname string + testingClient *client.APIClient +} + +//NewSystemUtil : Constructor +func NewSystemUtil(rootURI, hostname string, httpClient *client.APIClient) *SystemUtil { + if len(strings.TrimSpace(rootURI)) == 0 || httpClient == nil { + return nil + } + + return &SystemUtil{ + rootURI: rootURI, + hostname: hostname, + testingClient: httpClient, + } +} + +//GetSystemInfo : Get systeminfo +func (nsu *SystemUtil) GetSystemInfo() error { + url := nsu.rootURI + "/api/systeminfo" + data, err := nsu.testingClient.Get(url) + if err != nil { + return err + } + + var info models.SystemInfo + if err := json.Unmarshal(data, &info); err != nil { + return err + } + + if info.RegistryURL != nsu.hostname { + return fmt.Errorf("Invalid registry url in system info: expect %s got %s ", nsu.hostname, info.RegistryURL) + } + + return nil +} diff --git a/tests/apitests/api-testing/lib/user.go b/tests/apitests/api-testing/lib/user.go new file mode 100644 index 000000000..5bb639e56 --- /dev/null +++ b/tests/apitests/api-testing/lib/user.go @@ -0,0 +1,118 @@ +package lib + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + + "github.com/vmware/harbor/tests/apitests/api-testing/client" + "github.com/vmware/harbor/tests/apitests/api-testing/models" +) + +//UserUtil : For user related +type UserUtil struct { + rootURI string + testingClient *client.APIClient +} + +//NewUserUtil : Constructor +func NewUserUtil(rootURI string, httpClient *client.APIClient) *UserUtil { + if len(strings.TrimSpace(rootURI)) == 0 || httpClient == nil { + return nil + } + + return &UserUtil{ + rootURI: rootURI, + testingClient: httpClient, + } +} + +//CreateUser : Create user +func (uu *UserUtil) CreateUser(username, password string) error { + if len(strings.TrimSpace(username)) == 0 || + len(strings.TrimSpace(password)) == 0 { + return errors.New("Username and password required for creating user") + } + + u := models.User{ + Username: username, + Password: password, + Email: username + "@vmware.com", + RealName: username + "pks", + Comment: "testing", + } + + body, err := json.Marshal(&u) + if err != nil { + return err + } + + url := fmt.Sprintf("%s%s", uu.rootURI, "/api/users") + if err := uu.testingClient.Post(url, body); err != nil { + return err + } + + return nil +} + +//DeleteUser : Delete testing account +func (uu *UserUtil) DeleteUser(username string) error { + uid := uu.GetUserID(username) + if uid == -1 { + return fmt.Errorf("Failed to get user with name %s", username) + } + + url := fmt.Sprintf("%s%s%d", uu.rootURI, "/api/users/", uid) + if err := uu.testingClient.Delete(url); err != nil { + return err + } + + return nil +} + +//GetUsers : Get users +//If name specified, then return that one +func (uu *UserUtil) GetUsers(name string) ([]models.ExistingUser, error) { + url := fmt.Sprintf("%s%s", uu.rootURI, "/api/users") + if len(strings.TrimSpace(name)) > 0 { + url = url + "?username=" + name + } + + data, err := uu.testingClient.Get(url) + if err != nil { + return nil, err + } + + var users []models.ExistingUser + if err = json.Unmarshal(data, &users); err != nil { + return nil, err + } + + return users, nil +} + +//GetUserID : Get user ID +//If user with the username is not existing, then return -1 +func (uu *UserUtil) GetUserID(username string) int { + if len(strings.TrimSpace(username)) == 0 { + return -1 + } + + users, err := uu.GetUsers(username) + if err != nil { + return -1 + } + + if len(users) == 0 { + return -1 + } + + for _, u := range users { + if u.Username == username { + return u.ID + } + } + + return -1 +} diff --git a/tests/apitests/api-testing/models/endpoint.go b/tests/apitests/api-testing/models/endpoint.go new file mode 100644 index 000000000..56b4a9c23 --- /dev/null +++ b/tests/apitests/api-testing/models/endpoint.go @@ -0,0 +1,10 @@ +package models + +//Endpoint : For /api/targets +type Endpoint struct { + Endpoint string `json:"endpoint"` + Name string `json:"name"` + Username string `json:"username"` + Password string `json:"password"` + Type int `json:"type"` +} diff --git a/tests/apitests/api-testing/models/image.go b/tests/apitests/api-testing/models/image.go new file mode 100644 index 000000000..61e62f00f --- /dev/null +++ b/tests/apitests/api-testing/models/image.go @@ -0,0 +1,20 @@ +package models + +//Repository : For /api/repositories +type Repository struct { + ID int `json:"id"` + Name string `json:"name"` +} + +//Tag : For /api/repositories/:repo/tags +type Tag struct { + Digest string `json:"digest"` + Name string `json:"name"` + Signature map[string]interface{} `json:"signature, omitempty"` + ScanOverview *ScanOverview `json:"scan_overview, omitempty"` +} + +//ScanOverview : For scanning +type ScanOverview struct { + Status string `json:"scan_status"` +} diff --git a/tests/apitests/api-testing/models/member.go b/tests/apitests/api-testing/models/member.go new file mode 100644 index 000000000..3d7983afe --- /dev/null +++ b/tests/apitests/api-testing/models/member.go @@ -0,0 +1,7 @@ +package models + +//Member : For /api/projects/:pid/members +type Member struct { + UserName string `json:"username"` + Roles []int `json:"roles"` +} diff --git a/tests/apitests/api-testing/models/project.go b/tests/apitests/api-testing/models/project.go new file mode 100644 index 000000000..690d9adf9 --- /dev/null +++ b/tests/apitests/api-testing/models/project.go @@ -0,0 +1,18 @@ +package models + +//Project : For /api/projects +type Project struct { + Name string `json:"project_name"` + Metadata *Metadata `json:"metadata, omitempty"` +} + +//Metadata : Metadata for project +type Metadata struct { + AccessLevel string `json:"public"` +} + +//ExistingProject : For /api/projects?name=*** +type ExistingProject struct { + Name string `json:"name"` + ID int `json:"project_id"` +} diff --git a/tests/apitests/api-testing/models/replication.go b/tests/apitests/api-testing/models/replication.go new file mode 100644 index 000000000..a2cf28de2 --- /dev/null +++ b/tests/apitests/api-testing/models/replication.go @@ -0,0 +1,10 @@ +package models + +//ReplicationPolicy : For /api/replications +type ReplicationPolicy struct { + ProjectID int `json:"project_id"` + +} + +type ExistingReplicationPolicy struct { +} diff --git a/tests/apitests/api-testing/models/system_info.go b/tests/apitests/api-testing/models/system_info.go new file mode 100644 index 000000000..78da9aa4d --- /dev/null +++ b/tests/apitests/api-testing/models/system_info.go @@ -0,0 +1,7 @@ +package models + +//SystemInfo : For GET /api/systeminfo +type SystemInfo struct { + AuthMode string `json:"auth_mode"` + RegistryURL string `json:"registry_url"` +} diff --git a/tests/apitests/api-testing/models/user.go b/tests/apitests/api-testing/models/user.go new file mode 100644 index 000000000..b6b2d6055 --- /dev/null +++ b/tests/apitests/api-testing/models/user.go @@ -0,0 +1,16 @@ +package models + +//User : For /api/users +type User struct { + Username string `json:"username"` + RealName string `json:"realname"` + Password string `json:"password"` + Email string `json:"email"` + Comment string `json:"comment"` +} + +//ExistingUser : For GET /api/users +type ExistingUser struct { + User + ID int `json:"user_id"` +} diff --git a/tests/apitests/api-testing/tests/suites/suite.go b/tests/apitests/api-testing/tests/suites/suite.go new file mode 100644 index 000000000..c95add3d1 --- /dev/null +++ b/tests/apitests/api-testing/tests/suites/suite.go @@ -0,0 +1,11 @@ +package suites + +import ( + "github.com/vmware/harbor/tests/apitests/api-testing/envs" + "github.com/vmware/harbor/tests/apitests/api-testing/lib" +) + +//Suite : Run a group of test cases +type Suite interface { + Run(onEnvironment envs.Environment) *lib.Report +} diff --git a/tests/apitests/api-testing/tests/suites/suite01/run_test.go b/tests/apitests/api-testing/tests/suites/suite01/run_test.go new file mode 100644 index 000000000..c86166f48 --- /dev/null +++ b/tests/apitests/api-testing/tests/suites/suite01/run_test.go @@ -0,0 +1,22 @@ +package suite01 + +import ( + "testing" + + "github.com/vmware/harbor/tests/apitests/api-testing/envs" +) + +//TestRun : Start to run the case +func TestRun(t *testing.T) { + //Initialize env + if err := envs.ConcourseCIEnv.Load(); err != nil { + t.Fatal(err.Error()) + } + + suite := ConcourseCiSuite01{} + report := suite.Run(&envs.ConcourseCIEnv) + report.Print() + if report.IsFail() { + t.Fail() + } +} diff --git a/tests/apitests/api-testing/tests/suites/suite01/suite.go b/tests/apitests/api-testing/tests/suites/suite01/suite.go new file mode 100644 index 000000000..63f05bcc8 --- /dev/null +++ b/tests/apitests/api-testing/tests/suites/suite01/suite.go @@ -0,0 +1,177 @@ +package suite01 + +import ( + "fmt" + + "github.com/vmware/harbor/tests/apitests/api-testing/envs" + "github.com/vmware/harbor/tests/apitests/api-testing/lib" +) + +//Steps of suite01: +// s0: Get systeminfo +// s1: create project +// s2: create user "cody" +// s3: assign cody as developer +// s4: push a busybox image to project +// s5: scan image +// s6: pull image from project +// s7: remove "cody" from project member list +// s8: pull image from project [FAIL] +// s9: remove repository busybox +// s10: delete project +// s11: delete user + +//ConcourseCiSuite01 : For harbor journey in concourse pipeline +type ConcourseCiSuite01 struct{} + +//Run : Run a group of cases +func (ccs *ConcourseCiSuite01) Run(onEnvironment *envs.Environment) *lib.Report { + report := &lib.Report{} + + //s0 + sys := lib.NewSystemUtil(onEnvironment.RootURI(), onEnvironment.Hostname, onEnvironment.HTTPClient) + if err := sys.GetSystemInfo(); err != nil { + report.Failed("GetSystemInfo", err) + } else { + report.Passed("GetSystemInfo") + } + + //s1 + pro := lib.NewProjectUtil(onEnvironment.RootURI(), onEnvironment.HTTPClient) + if err := pro.CreateProject(onEnvironment.TestingProject, false); err != nil { + report.Failed("CreateProject", err) + } else { + report.Passed("CreateProject") + } + + //s2 + usr := lib.NewUserUtil(onEnvironment.RootURI(), onEnvironment.HTTPClient) + if err := usr.CreateUser(onEnvironment.Account, onEnvironment.Password); err != nil { + report.Failed("CreateUser", err) + } else { + report.Passed("CreateUser") + } + + //s3 + if err := pro.AssignRole(onEnvironment.TestingProject, onEnvironment.Account); err != nil { + report.Failed("AssignRole", err) + } else { + report.Passed("AssignRole") + } + + //s4 + if err := ccs.pushImage(onEnvironment); err != nil { + report.Failed("pushImage", err) + } else { + report.Passed("pushImage") + } + + //s5 + img := lib.NewImageUtil(onEnvironment.RootURI(), onEnvironment.HTTPClient) + repoName := fmt.Sprintf("%s/%s", onEnvironment.TestingProject, onEnvironment.ImageName) + if err := img.ScanTag(repoName, onEnvironment.ImageTag); err != nil { + report.Failed("ScanTag", err) + } else { + report.Passed("ScanTag") + } + + //s6 + if err := ccs.pullImage(onEnvironment); err != nil { + report.Failed("pullImage[1]", err) + } else { + report.Passed("pullImage[1]") + } + + //s7 + uid := usr.GetUserID(onEnvironment.Account) + if err := pro.RevokeRole(onEnvironment.TestingProject, uid); err != nil { + report.Failed("RevokeRole", err) + } else { + report.Passed("RevokeRole") + } + + //s8 + if err := ccs.pullImage(onEnvironment); err == nil { + report.Failed("pullImage[2]", err) + } else { + report.Passed("pullImage[2]") + } + + //s9 + if err := img.DeleteRepo(repoName); err != nil { + report.Failed("DeleteRepo", err) + } else { + report.Passed("DeleteRepo") + } + + //s10 + if err := pro.DeleteProject(onEnvironment.TestingProject); err != nil { + report.Failed("DeleteProject", err) + } else { + report.Passed("DeleteProject") + } + + //s11 + if err := usr.DeleteUser(onEnvironment.Account); err != nil { + report.Failed("DeleteUser", err) + } else { + report.Passed("DeleteUser") + } + + return report +} + +func (ccs *ConcourseCiSuite01) pushImage(onEnvironment *envs.Environment) error { + docker := onEnvironment.DockerClient + if err := docker.Status(); err != nil { + return err + } + + imagePulling := fmt.Sprintf("%s:%s", onEnvironment.ImageName, onEnvironment.ImageTag) + if err := docker.Pull(imagePulling); err != nil { + return err + } + + if err := docker.Login(onEnvironment.Account, onEnvironment.Password, onEnvironment.Hostname); err != nil { + return err + } + + imagePushing := fmt.Sprintf("%s/%s/%s:%s", + onEnvironment.Hostname, + onEnvironment.TestingProject, + onEnvironment.ImageName, + onEnvironment.ImageTag) + + if err := docker.Tag(imagePulling, imagePushing); err != nil { + return err + } + + if err := docker.Push(imagePushing); err != nil { + return err + } + + return nil +} + +func (ccs *ConcourseCiSuite01) pullImage(onEnvironment *envs.Environment) error { + docker := onEnvironment.DockerClient + if err := docker.Status(); err != nil { + return err + } + + if err := docker.Login(onEnvironment.Account, onEnvironment.Password, onEnvironment.Hostname); err != nil { + return err + } + + imagePulling := fmt.Sprintf("%s/%s/%s:%s", + onEnvironment.Hostname, + onEnvironment.TestingProject, + onEnvironment.ImageName, + onEnvironment.ImageTag) + + if err := docker.Pull(imagePulling); err != nil { + return err + } + + return nil +}