mirror of
https://github.com/Kichiyaki/terraform-provider-woodpecker.git
synced 2024-06-29 07:38:05 +00:00
571 lines
14 KiB
Go
571 lines
14 KiB
Go
package internal_test
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"math/rand"
|
|
"net/http"
|
|
"net/http/cookiejar"
|
|
urlpkg "net/url"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"code.gitea.io/sdk/gitea"
|
|
"github.com/Kichiyaki/terraform-provider-woodpecker/internal/woodpecker"
|
|
"github.com/google/uuid"
|
|
"github.com/ory/dockertest/v3"
|
|
"github.com/ory/dockertest/v3/docker"
|
|
"golang.org/x/oauth2"
|
|
)
|
|
|
|
var (
|
|
giteaClient *gitea.Client
|
|
woodpeckerClient woodpecker.Client
|
|
)
|
|
|
|
func TestMain(m *testing.M) {
|
|
pool := newDockertestPool()
|
|
|
|
var deferFuncs []func()
|
|
|
|
network := newDockerNetwork(pool)
|
|
deferFuncs = append(deferFuncs, func() {
|
|
_ = network.Close()
|
|
})
|
|
|
|
resourceGitea := runGitea(pool, network)
|
|
deferFuncs = append(deferFuncs, func() {
|
|
_ = resourceGitea.Close()
|
|
})
|
|
|
|
giteaClient = newGiteaClient(resourceGitea.httpURL, resourceGitea.user)
|
|
|
|
resourceWoodpecker := runWoodpecker(
|
|
pool,
|
|
network,
|
|
resourceGitea.httpURL,
|
|
resourceGitea.privateHTTPURL,
|
|
resourceGitea.user,
|
|
)
|
|
deferFuncs = append(deferFuncs, func() {
|
|
_ = resourceWoodpecker.Close()
|
|
})
|
|
|
|
woodpeckerClient = newWoodpeckerClient(resourceWoodpecker.httpURL, resourceWoodpecker.token)
|
|
|
|
// set required envs
|
|
_ = os.Setenv("TF_ACC", "1")
|
|
_ = os.Setenv("WOODPECKER_SERVER", resourceWoodpecker.httpURL.String())
|
|
_ = os.Setenv("WOODPECKER_TOKEN", resourceWoodpecker.token)
|
|
|
|
code := m.Run()
|
|
|
|
// LIFO
|
|
for i := len(deferFuncs) - 1; i >= 0; i-- {
|
|
deferFuncs[i]()
|
|
}
|
|
|
|
os.Exit(code)
|
|
}
|
|
|
|
func newDockertestPool() *dockertest.Pool {
|
|
pool, err := dockertest.NewPool("")
|
|
if err != nil {
|
|
log.Fatalf("couldn't construct pool: %s", err)
|
|
}
|
|
|
|
if err = pool.Client.Ping(); err != nil {
|
|
log.Fatalf("couldn't connect to Docker: %s", err)
|
|
}
|
|
|
|
return pool
|
|
}
|
|
|
|
func newDockerNetwork(pool *dockertest.Pool) *dockertest.Network {
|
|
network, err := pool.CreateNetwork(uuid.NewString())
|
|
if err != nil {
|
|
log.Fatalln("couldn't create docker network:", err)
|
|
}
|
|
return network
|
|
}
|
|
|
|
const giteaContainerExpInSec = 120
|
|
|
|
type giteaResource struct {
|
|
docker *dockertest.Resource
|
|
httpURL *urlpkg.URL
|
|
privateHTTPURL *urlpkg.URL
|
|
user *urlpkg.Userinfo
|
|
}
|
|
|
|
func runGitea(pool *dockertest.Pool, network *dockertest.Network) giteaResource {
|
|
repo, tag := getGiteaRepoTag()
|
|
|
|
giteaRsc, err := pool.RunWithOptions(&dockertest.RunOptions{
|
|
Repository: repo,
|
|
Tag: tag,
|
|
Networks: []*dockertest.Network{network},
|
|
Env: []string{
|
|
"GITEA__security__INSTALL_LOCK=true",
|
|
},
|
|
}, func(config *docker.HostConfig) {
|
|
config.AutoRemove = true
|
|
config.RestartPolicy = docker.RestartPolicy{
|
|
Name: "no",
|
|
}
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("couldn't run gitea: %s", err)
|
|
}
|
|
|
|
if err = giteaRsc.Expire(giteaContainerExpInSec); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
httpURL := &urlpkg.URL{
|
|
Scheme: "http",
|
|
Host: getHostPort(giteaRsc, "3000/tcp"),
|
|
}
|
|
|
|
if err = pool.Retry(func() error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
defer cancel()
|
|
|
|
req, reqErr := http.NewRequestWithContext(ctx, http.MethodGet, httpURL.String(), nil)
|
|
if reqErr != nil {
|
|
return reqErr
|
|
}
|
|
|
|
res, resErr := (&http.Client{}).Do(req)
|
|
if resErr != nil {
|
|
return resErr
|
|
}
|
|
defer func() {
|
|
_ = res.Body.Close()
|
|
}()
|
|
|
|
_, _ = io.Copy(io.Discard, res.Body)
|
|
|
|
if res.StatusCode != http.StatusOK {
|
|
return errors.New("request to gitea failed")
|
|
}
|
|
|
|
return nil
|
|
}); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
return giteaResource{
|
|
docker: giteaRsc,
|
|
httpURL: httpURL,
|
|
privateHTTPURL: &urlpkg.URL{
|
|
Scheme: httpURL.Scheme,
|
|
Host: giteaRsc.GetIPInNetwork(network) + ":3000",
|
|
},
|
|
user: createGiteaUser(pool, giteaRsc),
|
|
}
|
|
}
|
|
|
|
func (r giteaResource) Close() error {
|
|
return r.docker.Close()
|
|
}
|
|
|
|
const defaultGiteaImage = "gitea/gitea:1.20"
|
|
|
|
//nolint:nonamedreturns
|
|
func getGiteaRepoTag() (repository string, tag string) {
|
|
val := os.Getenv("GITEA_IMAGE")
|
|
if val == "" {
|
|
val = defaultGiteaImage
|
|
}
|
|
return docker.ParseRepositoryTag(val)
|
|
}
|
|
|
|
func createGiteaUser(pool *dockertest.Pool, giteaRsc *dockertest.Resource) *urlpkg.Userinfo {
|
|
username := strings.ReplaceAll(uuid.NewString(), "-", "")
|
|
password := uuid.NewString()
|
|
|
|
stdOutBuf := bytes.NewBuffer(nil)
|
|
stdErrBuf := bytes.NewBuffer(nil)
|
|
|
|
exec, err := pool.Client.CreateExec(docker.CreateExecOptions{
|
|
Container: giteaRsc.Container.ID,
|
|
User: "git",
|
|
Cmd: []string{
|
|
"gitea",
|
|
"admin",
|
|
"user",
|
|
"create",
|
|
"--admin",
|
|
"--username=" + username,
|
|
"--password=" + password,
|
|
fmt.Sprintf("--email=%s@localhost", username),
|
|
},
|
|
AttachStdout: true,
|
|
AttachStderr: true,
|
|
})
|
|
if err != nil {
|
|
log.Fatalln("couldn't create exec:", err)
|
|
}
|
|
|
|
err = pool.Client.StartExec(exec.ID, docker.StartExecOptions{
|
|
OutputStream: stdOutBuf,
|
|
ErrorStream: stdErrBuf,
|
|
})
|
|
if err != nil {
|
|
log.Fatalln("couldn't start exec:", err)
|
|
}
|
|
|
|
inspectExec, err := pool.Client.InspectExec(exec.ID)
|
|
if err != nil {
|
|
log.Fatalln("couldn't inspect exec:", err)
|
|
}
|
|
|
|
if inspectExec.ExitCode != 0 {
|
|
log.Fatalln("couldn't create user\nstdout:", stdOutBuf.String(), "\nstderr:", stdErrBuf.String())
|
|
}
|
|
|
|
return urlpkg.UserPassword(username, password)
|
|
}
|
|
|
|
type woodpeckerResource struct {
|
|
docker *dockertest.Resource
|
|
httpURL *urlpkg.URL
|
|
token string
|
|
}
|
|
|
|
const woodpeckerContainerExpInSec = 120
|
|
|
|
func runWoodpecker(
|
|
pool *dockertest.Pool,
|
|
network *dockertest.Network,
|
|
giteaPublicURL, giteaPrivateURL *urlpkg.URL,
|
|
giteaUser *urlpkg.Userinfo,
|
|
) woodpeckerResource {
|
|
httpURL := &urlpkg.URL{
|
|
Scheme: giteaPublicURL.Scheme,
|
|
//nolint:gosec
|
|
Host: giteaPublicURL.Hostname() + ":" + strconv.Itoa(rand.New(rand.NewSource(time.Now().Unix())).Intn(5000)+35000),
|
|
}
|
|
|
|
oauthApp, _, err := giteaClient.CreateOauth2(gitea.CreateOauth2Option{
|
|
Name: "woodpecker",
|
|
ConfidentialClient: true,
|
|
RedirectURIs: []string{httpURL.String() + "/authorize"},
|
|
})
|
|
if err != nil {
|
|
log.Fatalln("couldn't create oauth2 app:", err)
|
|
}
|
|
|
|
repo, tag := getWoodpeckerRepoTag()
|
|
woodpeckerRsc, err := pool.RunWithOptions(&dockertest.RunOptions{
|
|
Repository: repo,
|
|
Tag: tag,
|
|
Networks: []*dockertest.Network{network},
|
|
PortBindings: map[docker.Port][]docker.PortBinding{
|
|
"8000/tcp": {
|
|
{
|
|
HostPort: httpURL.Port(),
|
|
},
|
|
},
|
|
},
|
|
Env: []string{
|
|
"WOODPECKER_OPEN=true",
|
|
"WOODPECKER_HOST=" + httpURL.String(),
|
|
"WOODPECKER_AGENT_SECRET=" + uuid.NewString(),
|
|
"WOODPECKER_GITEA=true",
|
|
"WOODPECKER_GITEA_URL=" + giteaPrivateURL.String(),
|
|
"WOODPECKER_GITEA_CLIENT=" + oauthApp.ClientID,
|
|
"WOODPECKER_GITEA_SECRET=" + oauthApp.ClientSecret,
|
|
"WOODPECKER_ADMIN=" + giteaUser.Username(),
|
|
"WOODPECKER_LOG_LEVEL=debug",
|
|
},
|
|
}, func(config *docker.HostConfig) {
|
|
config.AutoRemove = true
|
|
config.Mounts = append(config.Mounts, docker.HostMount{
|
|
Type: "tmpfs",
|
|
Target: "/var/lib/woodpecker",
|
|
})
|
|
config.RestartPolicy = docker.RestartPolicy{
|
|
Name: "no",
|
|
}
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("couldn't run woodpecker: %s", err)
|
|
}
|
|
|
|
if err = woodpeckerRsc.Expire(woodpeckerContainerExpInSec); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
if err = pool.Retry(func() error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
defer cancel()
|
|
|
|
req, reqErr := http.NewRequestWithContext(ctx, http.MethodGet, httpURL.String(), nil)
|
|
if reqErr != nil {
|
|
return reqErr
|
|
}
|
|
|
|
res, resErr := (&http.Client{}).Do(req)
|
|
if resErr != nil {
|
|
return resErr
|
|
}
|
|
defer func() {
|
|
_ = res.Body.Close()
|
|
}()
|
|
|
|
_, _ = io.Copy(io.Discard, res.Body)
|
|
|
|
if res.StatusCode != http.StatusOK {
|
|
return errors.New("request to woodpecker failed")
|
|
}
|
|
|
|
return nil
|
|
}); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
return woodpeckerResource{
|
|
docker: woodpeckerRsc,
|
|
httpURL: httpURL,
|
|
token: newWoodpeckerTokenProvider(oauthApp, giteaUser, giteaPublicURL, httpURL).token(),
|
|
}
|
|
}
|
|
|
|
func (r woodpeckerResource) Close() error {
|
|
return r.docker.Close()
|
|
}
|
|
|
|
const defaultWoodpeckerImage = "woodpeckerci/woodpecker-server:v2.4.1"
|
|
|
|
//nolint:nonamedreturns
|
|
func getWoodpeckerRepoTag() (repo string, tag string) {
|
|
val := os.Getenv("WOODPECKER_IMAGE")
|
|
if val == "" {
|
|
val = defaultWoodpeckerImage
|
|
}
|
|
return docker.ParseRepositoryTag(val)
|
|
}
|
|
|
|
type woodpeckerTokenProvider struct {
|
|
client *http.Client
|
|
oauthApp *gitea.Oauth2
|
|
giteaUser *urlpkg.Userinfo
|
|
giteaURL *urlpkg.URL
|
|
woodpeckerURL *urlpkg.URL
|
|
}
|
|
|
|
func newWoodpeckerTokenProvider(
|
|
oauthApp *gitea.Oauth2,
|
|
giteaUser *urlpkg.Userinfo,
|
|
giteaURL *urlpkg.URL,
|
|
woodpeckerURL *urlpkg.URL,
|
|
) woodpeckerTokenProvider {
|
|
cookieJar, _ := cookiejar.New(nil)
|
|
return woodpeckerTokenProvider{
|
|
client: &http.Client{
|
|
Timeout: 10 * time.Second,
|
|
Jar: cookieJar,
|
|
},
|
|
oauthApp: oauthApp,
|
|
giteaUser: giteaUser,
|
|
giteaURL: giteaURL,
|
|
woodpeckerURL: woodpeckerURL,
|
|
}
|
|
}
|
|
|
|
func (p woodpeckerTokenProvider) token() string {
|
|
ctx := context.Background()
|
|
|
|
// we need to make this request to get the csrf token
|
|
respGetLoginPage := p.get(ctx, p.giteaURL.String()+"/user/login")
|
|
_, _ = io.Copy(io.Discard, respGetLoginPage.Body)
|
|
_ = respGetLoginPage.Body.Close()
|
|
|
|
// log in to Gitea
|
|
password, _ := p.giteaUser.Password()
|
|
respLogin := p.postForm(ctx, p.giteaURL.String()+"/user/login", urlpkg.Values{
|
|
"_csrf": []string{p.getCSRFTokenFromCookies(p.client.Jar.Cookies(p.giteaURL))},
|
|
"user_name": []string{p.giteaUser.Username()},
|
|
"password": []string{password},
|
|
})
|
|
_, _ = io.Copy(io.Discard, respLogin.Body)
|
|
_ = respLogin.Body.Close()
|
|
|
|
// we need to make this request to get the csrf token
|
|
respAuthorize := p.get(ctx, (&urlpkg.URL{
|
|
Scheme: p.giteaURL.Scheme,
|
|
Host: p.giteaURL.Host,
|
|
Path: "/login/oauth/authorize",
|
|
RawQuery: urlpkg.Values{
|
|
"client_id": []string{p.oauthApp.ClientID},
|
|
"redirect_uri": []string{p.oauthApp.RedirectURIs[0]},
|
|
"response_type": []string{"code"},
|
|
"state": []string{p.oauthApp.Name},
|
|
}.Encode(),
|
|
}).String())
|
|
_, _ = io.Copy(io.Discard, respAuthorize.Body)
|
|
_ = respAuthorize.Body.Close()
|
|
|
|
// log in to Woodpecker
|
|
respGrant := p.postForm(ctx, p.giteaURL.String()+"/login/oauth/grant", urlpkg.Values{
|
|
"_csrf": []string{p.getCSRFTokenFromCookies(p.client.Jar.Cookies(p.giteaURL))},
|
|
"client_id": []string{p.oauthApp.ClientID},
|
|
"redirect_uri": []string{p.oauthApp.RedirectURIs[0]},
|
|
"state": []string{p.oauthApp.Name},
|
|
"response_type": []string{"code"},
|
|
"scope": []string{""},
|
|
"nonce": []string{""},
|
|
})
|
|
_, _ = io.Copy(io.Discard, respGrant.Body)
|
|
_ = respGrant.Body.Close()
|
|
|
|
// we need to make this request to get the csrf token
|
|
respWebConfig := p.get(ctx, p.woodpeckerURL.String()+"/web-config.js")
|
|
csrfToken := p.readCSRFTokenFromWoodpeckerWebConfig(respWebConfig.Body)
|
|
_ = respWebConfig.Body.Close()
|
|
if csrfToken == "" {
|
|
log.Fatalln("couldn't extract csrf token from woodpecker web config")
|
|
}
|
|
|
|
// finally generate the woodpecker token
|
|
reqToken := p.newRequestWithContext(ctx, http.MethodPost, p.woodpeckerURL.String()+"/api/user/token", nil)
|
|
reqToken.Header.Set("X-Csrf-Token", csrfToken)
|
|
|
|
respToken := p.do(reqToken)
|
|
token, err := io.ReadAll(respToken.Body)
|
|
if err != nil {
|
|
log.Fatalln("couldn't read token from response:", err)
|
|
}
|
|
_ = respToken.Body.Close()
|
|
|
|
return string(token)
|
|
}
|
|
|
|
func (p woodpeckerTokenProvider) postForm(ctx context.Context, url string, values urlpkg.Values) *http.Response {
|
|
req := p.newRequestWithContext(ctx, http.MethodPost, url, strings.NewReader(values.Encode()))
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
return p.do(req)
|
|
}
|
|
|
|
func (p woodpeckerTokenProvider) get(ctx context.Context, url string) *http.Response {
|
|
return p.do(p.newRequestWithContext(ctx, http.MethodGet, url, nil))
|
|
}
|
|
|
|
func (p woodpeckerTokenProvider) newRequestWithContext(
|
|
ctx context.Context,
|
|
method string,
|
|
url string,
|
|
body io.Reader,
|
|
) *http.Request {
|
|
req, err := http.NewRequestWithContext(ctx, method, url, body)
|
|
if err != nil {
|
|
log.Fatalf("couldn't construct request for url %s: %s", url, err)
|
|
}
|
|
|
|
return req
|
|
}
|
|
|
|
func (p woodpeckerTokenProvider) do(req *http.Request) *http.Response {
|
|
resp, err := p.client.Do(req)
|
|
if err != nil {
|
|
log.Fatalf("request to %s failed: %s", req.URL.String(), err)
|
|
}
|
|
if resp.StatusCode/100 != 2 { // accept only 2XX requests
|
|
b, _ := io.ReadAll(resp.Body)
|
|
_ = resp.Body.Close()
|
|
log.Fatalf("request to %s failed (status code = %d): %s", req.URL.String(), resp.StatusCode, b)
|
|
}
|
|
|
|
return resp
|
|
}
|
|
|
|
func (p woodpeckerTokenProvider) getCSRFTokenFromCookies(cookies []*http.Cookie) string {
|
|
for _, cookie := range cookies {
|
|
if cookie.Name == "_csrf" {
|
|
return cookie.Value
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (p woodpeckerTokenProvider) readCSRFTokenFromWoodpeckerWebConfig(r io.Reader) string {
|
|
defer func() {
|
|
// discard remaining bytes
|
|
_, _ = io.Copy(io.Discard, r)
|
|
}()
|
|
|
|
scanner := bufio.NewScanner(r)
|
|
scanner.Split(bufio.ScanLines)
|
|
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
|
|
if !strings.HasPrefix(line, "window.WOODPECKER_CSRF") {
|
|
continue
|
|
}
|
|
|
|
return line[strings.Index(line, `"`)+1 : strings.LastIndex(line, `"`)]
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func getHostPort(resource *dockertest.Resource, id string) string {
|
|
dockerURL := os.Getenv("DOCKER_HOST")
|
|
if dockerURL == "" {
|
|
return resource.GetHostPort(id)
|
|
}
|
|
|
|
u, err := urlpkg.Parse(dockerURL)
|
|
if err != nil {
|
|
log.Fatalln("couldn't parse DOCKER_HOST:", err)
|
|
}
|
|
|
|
return u.Hostname() + ":" + resource.GetPort(id)
|
|
}
|
|
|
|
func newGiteaClient(url *urlpkg.URL, user *urlpkg.Userinfo) *gitea.Client {
|
|
password, _ := user.Password()
|
|
client, err := gitea.NewClient(
|
|
url.String(),
|
|
gitea.SetHTTPClient(&http.Client{
|
|
Timeout: 10 * time.Second,
|
|
}),
|
|
gitea.SetBasicAuth(user.Username(), password),
|
|
)
|
|
if err != nil {
|
|
log.Fatalln("couldn't construct *gitea.Client:", err)
|
|
}
|
|
|
|
if _, _, err = client.GetMyUserInfo(); err != nil {
|
|
log.Fatalln("couldn't get user info:", err)
|
|
}
|
|
|
|
return client
|
|
}
|
|
|
|
func newWoodpeckerClient(url *urlpkg.URL, token string) woodpecker.Client {
|
|
client := woodpecker.NewClient(
|
|
url.String(),
|
|
(&oauth2.Config{}).Client(context.Background(), &oauth2.Token{
|
|
AccessToken: token,
|
|
}),
|
|
)
|
|
|
|
if _, err := client.Self(); err != nil {
|
|
log.Fatalln("couldn't get user info:", err)
|
|
}
|
|
|
|
return client
|
|
}
|