integration: add cluster setup and configuration and a few tests

This commit is contained in:
Caleb Doxsey 2020-04-28 07:33:33 -06:00
parent 9860c3ce9f
commit 8fd716e1d8
24 changed files with 1689 additions and 2 deletions

View file

@ -85,3 +85,51 @@ jobs:
uses: actions/checkout@v2
- name: build
run: docker build .
integration-tests:
name: Integration Test
runs-on: ubuntu-latest
steps:
- name: cache binaries
uses: actions/cache@v1
env:
cache-name: cache-binaries
with:
path: /opt/binaries/
key: ${{ runner.os }}-binaries
- name: install binaries
run: |
#!/bin/bash
sudo mkdir -p /usr/local/bin/
sudo mkdir -p /opt/minikube/bin/
cd /opt/minikube/bin
if [ ! -f minikube ]; then
echo "downloading minikube"
sudo curl -Lo minikube https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
sudo chmod +x minikube
fi
sudo install minikube /usr/local/bin/
if [ ! -f mkcert ]; then
echo "downloading mkcert"
sudo curl -Lo mkcert https://github.com/FiloSottile/mkcert/releases/download/v1.4.1/mkcert-v1.4.1-linux-amd64
sudo chmod +x mkcert
fi
sudo install mkcert /usr/local/bin/
- name: start minikube
run: |
minikube start
kubectl cluster-info
- name: install go
uses: actions/setup-go@v1
with:
go-version: 1.14.x
- name: checkout code
uses: actions/checkout@v2
- name: build dev docker image
run: |
./scripts/build-dev-docker.bash
- name: test
run: go test -v

5
go.mod
View file

@ -14,9 +14,12 @@ require (
github.com/golang/mock v1.4.3
github.com/golang/protobuf v1.3.5
github.com/google/go-cmp v0.4.0
github.com/google/go-jsonnet v0.15.0
github.com/google/uuid v1.1.1
github.com/gorilla/mux v1.7.4
github.com/mitchellh/hashstructure v1.0.0
github.com/onsi/ginkgo v1.11.0 // indirect
github.com/onsi/gocleanup v0.0.0-20140331211545-c1a5478700b5
github.com/onsi/gomega v1.8.1 // indirect
github.com/open-policy-agent/opa v0.18.0
github.com/pelletier/go-toml v1.6.0 // indirect
@ -34,10 +37,12 @@ require (
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.6.3
github.com/stretchr/testify v1.4.0
github.com/uber/jaeger-client-go v2.20.1+incompatible // indirect
go.etcd.io/bbolt v1.3.4
go.opencensus.io v0.22.3
golang.org/x/crypto v0.0.0-20200427165652-729f1e841bcc
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
google.golang.org/api v0.20.0
google.golang.org/appengine v1.6.5 // indirect

15
go.sum
View file

@ -119,11 +119,15 @@ github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-jsonnet v0.15.0 h1:lEUXTDnVsHu+CLLzMeWAdWV4JpCgkJeDqdVNS8RtyuY=
github.com/google/go-jsonnet v0.15.0/go.mod h1:ex9QcU8vzXQUDeNe4gaN1uhGQbTYpOeZ6AbWdy6JbX4=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
@ -192,8 +196,11 @@ github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzR
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-runewidth v0.0.0-20181025052659-b20a3daf6a39 h1:0E3wlIAcvD6zt/8UJgTd4JMT6UQhsnYyjCIqllyVLbs=
github.com/mattn/go-runewidth v0.0.0-20181025052659-b20a3daf6a39/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
@ -221,6 +228,8 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.11.0 h1:JAKSXpt1YjtLA7YpPiqO9ss6sNXEsPfSGdwN0UHqzrw=
github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gocleanup v0.0.0-20140331211545-c1a5478700b5 h1:uuhPqmc+m7Nj7btxZEjdEUv+uFoBHNf2Tk/E7gGM+kY=
github.com/onsi/gocleanup v0.0.0-20140331211545-c1a5478700b5/go.mod h1:tHaogb+iP6wJXwCqVUlmxYuJb4XDyEKxxs3E4DvMBK0=
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.8.1 h1:C5Dqfs/LeauYDX0jJXIe2SWmwCbGzx9yF8C8xy3Lh34=
github.com/onsi/gomega v1.8.1/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
@ -297,6 +306,8 @@ github.com/rs/zerolog v1.18.0/go.mod h1:9nvC1axdVrAHcu/s9taAVfBuIdTZLVQmKQyvrUjF
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
@ -367,8 +378,6 @@ golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad h1:Jh8cai0fqIK+f6nG0UgPW5wFk8wmiMhM3AyciDBdtQg=
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200403201458-baeed622b8d8 h1:fpnn/HnJONpIu6hkXi1u/7rR0NzilgWr4T0JmWkEitk=
golang.org/x/crypto v0.0.0-20200403201458-baeed622b8d8/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200427165652-729f1e841bcc h1:ZGI/fILM2+ueot/UixBSoj9188jCAxVHEZEGhqq67I4=
golang.org/x/crypto v0.0.0-20200427165652-729f1e841bcc/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -437,11 +446,13 @@ golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

10
integration/README.md Normal file
View file

@ -0,0 +1,10 @@
# Integration Tests
These tests are full end-to-end integration tests using Pomerium in a kubernetes cluster.
## Usage
The following applications are needed:
* `kubectl`: to apply the manifests to kubernetes
* `mkcert`: to generate a root CA and wildcard certificates
The test suite will apply the manifests to your current Kubernetes context before running the tests.

View file

@ -0,0 +1,99 @@
package main
import (
"context"
"net/http"
"net/url"
"testing"
"time"
"github.com/pomerium/pomerium/integration/internal/flows"
"github.com/stretchr/testify/assert"
)
func TestAuthorization(t *testing.T) {
ctx, clearTimeout := context.WithTimeout(mainCtx, time.Second*30)
defer clearTimeout()
t.Run("public", func(t *testing.T) {
t.Skip() // pomerium doesn't currently handle unauthenticated public routes
client := testcluster.NewHTTPClient()
req, err := http.NewRequestWithContext(ctx, "GET", "https://httpdetails.localhost.pomerium.io", nil)
if err != nil {
t.Fatal(err)
}
res, err := client.Do(req)
if !assert.NoError(t, err, "unexpected http error") {
return
}
defer res.Body.Close()
assert.Equal(t, http.StatusOK, res.StatusCode, "unexpected status code, headers=%v", res.Header)
})
t.Run("domains", func(t *testing.T) {
t.Run("allowed", func(t *testing.T) {
client := testcluster.NewHTTPClient()
res, err := flows.Authenticate(ctx, client, mustParseURL("https://httpdetails.localhost.pomerium.io/by-domain"), "bob@dogs.test", []string{"user"})
if assert.NoError(t, err) {
assert.Equal(t, http.StatusOK, res.StatusCode, "expected OK for dogs.test")
}
})
t.Run("not allowed", func(t *testing.T) {
client := testcluster.NewHTTPClient()
res, err := flows.Authenticate(ctx, client, mustParseURL("https://httpdetails.localhost.pomerium.io/by-domain"), "joe@cats.test", []string{"user"})
if assert.NoError(t, err) {
assertDeniedAccess(t, res, "expected Forbidden for cats.test")
}
})
})
t.Run("users", func(t *testing.T) {
t.Run("allowed", func(t *testing.T) {
client := testcluster.NewHTTPClient()
res, err := flows.Authenticate(ctx, client, mustParseURL("https://httpdetails.localhost.pomerium.io/by-user"), "bob@dogs.test", []string{"user"})
if assert.NoError(t, err) {
assert.Equal(t, http.StatusOK, res.StatusCode, "expected OK for bob@dogs.test")
}
})
t.Run("not allowed", func(t *testing.T) {
client := testcluster.NewHTTPClient()
res, err := flows.Authenticate(ctx, client, mustParseURL("https://httpdetails.localhost.pomerium.io/by-user"), "joe@cats.test", []string{"user"})
if assert.NoError(t, err) {
assertDeniedAccess(t, res, "expected Forbidden for joe@cats.test")
}
})
})
t.Run("groups", func(t *testing.T) {
t.Run("allowed", func(t *testing.T) {
client := testcluster.NewHTTPClient()
res, err := flows.Authenticate(ctx, client, mustParseURL("https://httpdetails.localhost.pomerium.io/by-group"), "bob@dogs.test", []string{"admin", "user"})
if assert.NoError(t, err) {
assert.Equal(t, http.StatusOK, res.StatusCode, "expected OK for admin")
}
})
t.Run("not allowed", func(t *testing.T) {
client := testcluster.NewHTTPClient()
res, err := flows.Authenticate(ctx, client, mustParseURL("https://httpdetails.localhost.pomerium.io/by-group"), "joe@cats.test", []string{"user"})
if assert.NoError(t, err) {
assertDeniedAccess(t, res, "expected Forbidden for user")
}
})
})
}
func mustParseURL(str string) *url.URL {
u, err := url.Parse(str)
if err != nil {
panic(err)
}
return u
}
func assertDeniedAccess(t *testing.T, res *http.Response, msgAndArgs ...interface{}) bool {
return assert.Condition(t, func() bool {
return res.StatusCode == http.StatusForbidden || res.StatusCode == http.StatusUnauthorized
}, msgAndArgs...)
}

View file

@ -0,0 +1,29 @@
const http = require("http");
const requestListener = function (req, res) {
const {
pathname: path,
hostname: host,
port: port,
search: query,
hash: hash,
} = new URL(req.url, `http://${req.headers.host}`);
res.setHeader("Content-Type", "application/json");
res.writeHead(200);
res.end(
JSON.stringify({
headers: req.headers,
method: req.method,
host: host,
port: port,
path: path,
query: query,
hash: hash,
})
);
};
const server = http.createServer(requestListener);
console.log("starting http server on :8080");
server.listen(8080);

View file

@ -0,0 +1,34 @@
package main
import (
"context"
"net/http"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestDashboard(t *testing.T) {
ctx := mainCtx
ctx, clearTimeout := context.WithTimeout(ctx, time.Second*30)
defer clearTimeout()
t.Run("image asset", func(t *testing.T) {
client := testcluster.NewHTTPClient()
req, err := http.NewRequestWithContext(ctx, "GET", "https://httpdetails.localhost.pomerium.io/.pomerium/assets/img/pomerium.svg", nil)
if err != nil {
t.Fatal(err)
}
res, err := client.Do(req)
if !assert.NoError(t, err, "unexpected http error") {
return
}
defer res.Body.Close()
assert.Equal(t, http.StatusOK, res.StatusCode, "unexpected status code")
assert.Equal(t, "image/svg+xml", res.Header.Get("Content-Type"))
})
}

View file

@ -0,0 +1,65 @@
package cluster
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/google/uuid"
)
type TLSCerts struct {
CA string
Cert string
Key string
}
func bootstrapCerts(ctx context.Context) (*TLSCerts, error) {
err := run(ctx, "mkcert", withArgs("-install"))
if err != nil {
return nil, fmt.Errorf("error install root certificate: %w", err)
}
var buf bytes.Buffer
err = run(ctx, "mkcert", withArgs("-CAROOT"), withStdout(&buf))
if err != nil {
return nil, fmt.Errorf("error running mkcert")
}
caPath := strings.TrimSpace(buf.String())
ca, err := ioutil.ReadFile(filepath.Join(caPath, "rootCA.pem"))
if err != nil {
return nil, fmt.Errorf("error reading root ca: %w", err)
}
wd := filepath.Join(os.TempDir(), uuid.New().String())
err = os.MkdirAll(wd, 0755)
if err != nil {
return nil, fmt.Errorf("error creating temporary directory: %w", err)
}
err = run(ctx, "mkcert", withArgs("*.localhost.pomerium.io"), withWorkingDir(wd))
if err != nil {
return nil, fmt.Errorf("error generating certificates: %w", err)
}
cert, err := ioutil.ReadFile(filepath.Join(wd, "_wildcard.localhost.pomerium.io.pem"))
if err != nil {
return nil, fmt.Errorf("error reading certificate: %w", err)
}
key, err := ioutil.ReadFile(filepath.Join(wd, "_wildcard.localhost.pomerium.io-key.pem"))
if err != nil {
return nil, fmt.Errorf("error reading certificate key: %w", err)
}
return &TLSCerts{
CA: string(ca),
Cert: string(cert),
Key: string(key),
}, nil
}

View file

@ -0,0 +1,46 @@
package cluster
import (
"net/http"
"net/http/cookiejar"
"github.com/rs/zerolog/log"
"golang.org/x/net/publicsuffix"
)
type Cluster struct {
workingDir string
transport http.RoundTripper
certs *TLSCerts
}
func New(workingDir string) *Cluster {
return &Cluster{
workingDir: workingDir,
}
}
func (cluster *Cluster) NewHTTPClient() *http.Client {
jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
if err != nil {
panic(err)
}
return &http.Client{
Transport: &loggingRoundTripper{cluster.transport},
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
Jar: jar,
}
}
type loggingRoundTripper struct {
http.RoundTripper
}
func (rt *loggingRoundTripper) RoundTrip(req *http.Request) (res *http.Response, err error) {
res, err = rt.RoundTripper.RoundTrip(req)
log.Debug().Str("method", req.Method).Str("url", req.URL.String()).Msg("http request")
return res, err
}

View file

@ -0,0 +1,70 @@
package cluster
import (
"bufio"
"context"
"fmt"
"io"
"os/exec"
"github.com/rs/zerolog/log"
)
type cmdOption func(*exec.Cmd)
func withArgs(args ...string) cmdOption {
return func(cmd *exec.Cmd) {
cmd.Args = append([]string{"kubectl"}, args...)
}
}
func withStdin(rdr io.Reader) cmdOption {
return func(cmd *exec.Cmd) {
cmd.Stdin = rdr
}
}
func withStdout(w io.Writer) cmdOption {
return func(cmd *exec.Cmd) {
cmd.Stdout = w
}
}
func withWorkingDir(wd string) cmdOption {
return func(cmd *exec.Cmd) {
cmd.Dir = wd
}
}
func run(ctx context.Context, name string, options ...cmdOption) error {
cmd := commandContext(ctx, name)
for _, o := range options {
o(cmd)
}
if cmd.Stderr == nil {
stderr, err := cmd.StderrPipe()
if err != nil {
return fmt.Errorf("failed to create stderr pipe for %s: %w", name, err)
}
go cmdLogger(stderr)
defer stderr.Close()
}
if cmd.Stdout == nil {
stdout, err := cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("failed to create stdout pipe for %s: %w", name, err)
}
go cmdLogger(stdout)
defer stdout.Close()
}
log.Debug().Strs("args", cmd.Args).Msgf("running %s", name)
return cmd.Run()
}
func cmdLogger(rdr io.Reader) {
s := bufio.NewScanner(rdr)
for s.Scan() {
log.Debug().Msg(s.Text())
}
}

View file

@ -0,0 +1,24 @@
// +build linux
package cluster
import (
"context"
"os/exec"
"syscall"
"github.com/onsi/gocleanup"
)
func commandContext(ctx context.Context, name string, args ...string) *exec.Cmd {
cmd := exec.CommandContext(ctx, name, args...)
cmd.SysProcAttr = &syscall.SysProcAttr{
Pdeathsig: syscall.SIGTERM,
}
gocleanup.Register(func() {
if cmd.Process != nil {
_ = cmd.Process.Kill()
}
})
return cmd
}

View file

@ -0,0 +1,20 @@
// +build !linux
package cluster
import (
"context"
"os/exec"
"github.com/onsi/gocleanup"
)
func commandContext(ctx context.Context, name string, args ...string) *exec.Cmd {
cmd := exec.CommandContext(ctx, name, args...)
gocleanup.Register(func() {
if cmd.Process != nil {
_ = cmd.Process.Kill()
}
})
return cmd
}

View file

@ -0,0 +1,6 @@
package cluster
type Config struct {
WorkingDirectory string
HTTPSPort int
}

View file

@ -0,0 +1,222 @@
package cluster
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/google/go-jsonnet"
"github.com/pomerium/pomerium/integration/internal/httputil"
"github.com/rs/zerolog/log"
)
var requiredDeployments = []string{
"default/httpbin",
"default/httpecho",
"default/openid",
"default/pomerium-authenticate",
"default/pomerium-authorize",
"default/pomerium-proxy",
"ingress-nginx/nginx-ingress-controller",
}
// Setup configures the test cluster so that it is ready for the integration tests.
func (cluster *Cluster) Setup(ctx context.Context) error {
err := run(ctx, "kubectl", withArgs("cluster-info"))
if err != nil {
return fmt.Errorf("error running kubectl cluster-info: %w", err)
}
cluster.certs, err = bootstrapCerts(ctx)
if err != nil {
return err
}
jsonsrc, err := cluster.generateManifests(ctx)
if err != nil {
return err
}
err = applyManifests(ctx, jsonsrc)
if err != nil {
return err
}
hostport, err := cluster.getNodeHTTPSAddr(ctx)
if err != nil {
return err
}
cluster.transport = httputil.NewLocalRoundTripper(&http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}, map[string]string{
"443": hostport,
})
return nil
}
func (cluster *Cluster) getNodeHTTPSAddr(ctx context.Context) (hostport string, err error) {
var buf bytes.Buffer
args := []string{"get", "service", "--namespace", "ingress-nginx", "--output", "json",
"ingress-nginx-nodeport"}
err = run(ctx, "kubectl", withArgs(args...), withStdout(&buf))
if err != nil {
return "", fmt.Errorf("error getting service details with kubectl: %w", err)
}
var svcResult struct {
Spec struct {
Ports []struct {
Name string `json:"name"`
NodePort int `json:"nodePort"`
} `json:"ports"`
Selector map[string]string `json:"selector"`
} `json:"spec"`
}
err = json.Unmarshal(buf.Bytes(), &svcResult)
if err != nil {
return "", fmt.Errorf("error unmarshaling service details from kubectl: %w", err)
}
buf.Reset()
args = []string{"get", "pods", "--namespace", "ingress-nginx", "--output", "json"}
var sel []string
for k, v := range svcResult.Spec.Selector {
sel = append(sel, k+"="+v)
}
args = append(args, "--selector", strings.Join(sel, ","))
err = run(ctx, "kubectl", withArgs(args...), withStdout(&buf))
if err != nil {
return "", fmt.Errorf("error getting pod details with kubectl: %w", err)
}
var podsResult struct {
Items []struct {
Status struct {
HostIP string `json:"hostIP"`
} `json:"status"`
} `json:"items"`
}
err = json.Unmarshal(buf.Bytes(), &podsResult)
if err != nil {
return "", fmt.Errorf("error unmarshaling pod details from kubectl (json=%s): %w", buf.String(), err)
}
var port string
for _, p := range svcResult.Spec.Ports {
if p.Name == "https" {
port = strconv.Itoa(p.NodePort)
}
}
if port == "" {
return "", fmt.Errorf("failed to find https port in kubectl service results (result=%v)", svcResult)
}
var hostIP string
for _, item := range podsResult.Items {
hostIP = item.Status.HostIP
}
if hostIP == "" {
return "", fmt.Errorf("failed to find host ip in kubectl pod results: %w", err)
}
return net.JoinHostPort(hostIP, port), nil
}
func (cluster *Cluster) generateManifests(ctx context.Context) (string, error) {
src, err := ioutil.ReadFile(filepath.Join(cluster.workingDir, "manifests", "manifests.jsonnet"))
if err != nil {
return "", fmt.Errorf("error reading manifest jsonnet src: %w", err)
}
vm := jsonnet.MakeVM()
vm.ExtVar("tls-ca", cluster.certs.CA)
vm.ExtVar("tls-cert", cluster.certs.Cert)
vm.ExtVar("tls-key", cluster.certs.Key)
vm.Importer(&jsonnet.FileImporter{
JPaths: []string{filepath.Join(cluster.workingDir, "manifests")},
})
jsonsrc, err := vm.EvaluateSnippet("manifests.jsonnet", string(src))
if err != nil {
return "", fmt.Errorf("error evaluating jsonnet (filename=manifests.jsonnet): %w", err)
}
return jsonsrc, nil
}
func applyManifests(ctx context.Context, jsonsrc string) error {
err := run(ctx, "kubectl", withArgs("apply", "-f", "-"), withStdin(strings.NewReader(jsonsrc)))
if err != nil {
return fmt.Errorf("error applying manifests: %w", err)
}
log.Info().Msg("waiting for deployments to come up")
ctx, clearTimeout := context.WithTimeout(ctx, 5*time.Minute)
defer clearTimeout()
ticker := time.NewTicker(time.Second * 5)
defer ticker.Stop()
for {
var buf bytes.Buffer
err = run(ctx, "kubectl", withArgs("get", "deployments", "--all-namespaces", "--output", "json"),
withStdout(&buf))
if err != nil {
return fmt.Errorf("error polling for deployment status: %w", err)
}
var results struct {
Items []struct {
Metadata struct {
Namespace string `json:"namespace"`
Name string `json:"name"`
} `json:"metadata"`
Status struct {
AvailableReplicas int `json:"availableReplicas"`
} `json:"status"`
} `json:"items"`
}
err = json.Unmarshal(buf.Bytes(), &results)
if err != nil {
return fmt.Errorf("error unmarshaling kubectl results: %w", err)
}
byName := map[string]int{}
for _, item := range results.Items {
byName[item.Metadata.Namespace+"/"+item.Metadata.Name] = item.Status.AvailableReplicas
}
done := true
for _, dep := range requiredDeployments {
if byName[dep] < 1 {
done = false
log.Warn().Str("deployment", dep).Msg("deployment is not ready yet")
}
}
if done {
break
}
select {
case <-ticker.C:
case <-ctx.Done():
return ctx.Err()
}
<-ticker.C
}
log.Info().Msg("all deployments are ready")
return nil
}

View file

@ -0,0 +1,131 @@
package flows
import (
"context"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/pomerium/pomerium/integration/internal/forms"
)
const (
authenticateHostname = "authenticate.localhost.pomerium.io"
openidHostname = "openid.localhost.pomerium.io"
pomeriumCallbackPath = "/.pomerium/callback/"
)
// Authenticate submits a request to a URL, expects a redirect to authenticate and then openid and logs in.
// Finally it expects to redirect back to the original page.
func Authenticate(ctx context.Context, client *http.Client, url *url.URL, email string, groups []string) (*http.Response, error) {
originalHostname := url.Hostname()
req, err := http.NewRequestWithContext(ctx, "GET", url.String(), nil)
if err != nil {
return nil, err
}
var res *http.Response
// (1) redirect to authenticate
for req.URL.Hostname() == originalHostname {
res, err = client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
req, err = requestFromRedirectResponse(ctx, res, req)
if err != nil {
return nil, fmt.Errorf("expected redirect to %s: %w", authenticateHostname, err)
}
}
// (2) redirect to openid
for req.URL.Hostname() == authenticateHostname {
res, err = client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
req, err = requestFromRedirectResponse(ctx, res, req)
if err != nil {
return nil, fmt.Errorf("expected redirect to %s: %w", openidHostname, err)
}
}
// (3) submit the form
for req.URL.Hostname() == openidHostname {
res, err = client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
forms := forms.Parse(res.Body)
if len(forms) > 0 {
f := forms[0]
f.Inputs["email"] = email
f.Inputs["groups"] = strings.Join(groups, ",")
req, err = f.NewRequestWithContext(ctx, req.URL)
if err != nil {
return nil, err
}
} else {
req, err = requestFromRedirectResponse(ctx, res, req)
if err != nil {
return nil, fmt.Errorf("expected redirect to %s: %w", openidHostname, err)
}
}
}
// (4) back to authenticate
for req.URL.Hostname() == authenticateHostname {
res, err = client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
req, err = requestFromRedirectResponse(ctx, res, req)
if err != nil {
return nil, fmt.Errorf("expected redirect to %s: %w", originalHostname, err)
}
}
// (5) finally to callback
if req.URL.Path != pomeriumCallbackPath {
return nil, fmt.Errorf("expected to redirect back to %s, but got %s", pomeriumCallbackPath, req.URL.String())
}
res, err = client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
req, err = requestFromRedirectResponse(ctx, res, req)
if err != nil {
return nil, fmt.Errorf("expected redirect to %s: %w", originalHostname, err)
}
return client.Do(req)
}
func requestFromRedirectResponse(ctx context.Context, res *http.Response, req *http.Request) (*http.Request, error) {
if res.Header.Get("Location") == "" {
return nil, fmt.Errorf("no location header found in response headers")
}
location, err := url.Parse(res.Header.Get("Location"))
if err != nil {
return nil, err
}
location = req.URL.ResolveReference(location)
newreq, err := http.NewRequestWithContext(ctx, "GET", location.String(), nil)
if err != nil {
return nil, err
}
return newreq, nil
}

View file

@ -0,0 +1,91 @@
package forms
import (
"context"
"io"
"net/http"
"net/url"
"strings"
"golang.org/x/net/html"
)
// A Form represents an HTML form.
type Form struct {
Action string
Method string
Inputs map[string]string
}
// Parse parses all the forms in an HTML document.
func Parse(r io.Reader) []Form {
root, err := html.Parse(r)
if err != nil {
return nil
}
var forms []Form
var currentForm *Form
var visit func(*html.Node)
visit = func(node *html.Node) {
if node.Type == html.ElementNode && node.Data == "form" {
currentForm = &Form{Action: "", Method: "GET", Inputs: make(map[string]string)}
for _, attr := range node.Attr {
switch attr.Key {
case "action":
currentForm.Action = attr.Val
case "method":
currentForm.Method = strings.ToUpper(attr.Val)
}
}
}
if currentForm != nil && node.Type == html.ElementNode && node.Data == "input" {
var name, value string
for _, attr := range node.Attr {
switch attr.Key {
case "name":
name = attr.Val
case "value":
value = attr.Val
}
}
if name != "" {
currentForm.Inputs[name] = value
}
}
for c := node.FirstChild; c != nil; c = c.NextSibling {
visit(c)
}
if node.Type == html.ElementNode && node.Data == "form" {
if currentForm != nil {
forms = append(forms, *currentForm)
}
currentForm = nil
}
}
visit(root)
return forms
}
func (f *Form) NewRequestWithContext(ctx context.Context, baseURL *url.URL) (*http.Request, error) {
actionURL, err := url.Parse(f.Action)
if err != nil {
return nil, err
}
actionURL = baseURL.ResolveReference(actionURL)
vs := make(url.Values)
for k, v := range f.Inputs {
vs.Set(k, v)
}
req, err := http.NewRequestWithContext(ctx, f.Method, actionURL.String(), strings.NewReader(vs.Encode()))
if err != nil {
return nil, err
}
//TODO: handle multipart forms
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
return req, nil
}

View file

@ -0,0 +1,44 @@
package httputil
import (
"context"
"net"
"net/http"
)
type localRoundTripper struct {
underlying http.RoundTripper
portToAddr map[string]string
}
func NewLocalRoundTripper(underlying http.RoundTripper, portToAddr map[string]string) http.RoundTripper {
lrt := &localRoundTripper{underlying: underlying, portToAddr: portToAddr}
return lrt
}
func (lrt *localRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
req = req.Clone(req.Context())
req.URL.Host = lrt.remapHost(req.Context(), req.Host)
return lrt.underlying.RoundTrip(req)
}
func (lrt *localRoundTripper) remapHost(ctx context.Context, hostport string) string {
host, port, err := net.SplitHostPort(hostport)
if err != nil {
host = hostport
port = "443"
}
dst, ok := lrt.portToAddr[port]
if !ok {
return hostport
}
ips, err := net.DefaultResolver.LookupIPAddr(ctx, host)
if err != nil || len(ips) == 0 || ips[0].String() != "127.0.0.1" {
return hostport
}
return dst
}

52
integration/main_test.go Normal file
View file

@ -0,0 +1,52 @@
package main
import (
"context"
"flag"
"os"
"path/filepath"
"runtime"
"testing"
"time"
"github.com/onsi/gocleanup"
"github.com/pomerium/pomerium/integration/internal/cluster"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
const localHTTPSPort = 9443
var (
mainCtx context.Context
testcluster *cluster.Cluster
)
func TestMain(m *testing.M) {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
flag.Parse()
if testing.Verbose() {
log.Logger = log.Logger.Level(zerolog.DebugLevel)
} else {
log.Logger = log.Logger.Level(zerolog.InfoLevel)
}
mainCtx = context.Background()
var cancel func()
mainCtx, cancel = context.WithCancel(mainCtx)
var clearTimeout func()
mainCtx, clearTimeout = context.WithTimeout(mainCtx, time.Minute*10)
defer clearTimeout()
_, mainTestFilePath, _, _ := runtime.Caller(0)
testcluster = cluster.New(filepath.Dir(mainTestFilePath))
if err := testcluster.Setup(mainCtx); err != nil {
log.Fatal().Err(err).Send()
}
status := m.Run()
cancel()
gocleanup.Cleanup()
os.Exit(status)
}

View file

@ -0,0 +1,79 @@
{
apiVersion: 'v1',
kind: 'List',
items: [
{
apiVersion: 'v1',
kind: 'ConfigMap',
metadata: {
namespace: 'default',
name: 'httpdetails',
labels: {
app: 'httpdetails',
},
},
data: {
'index.js': importstr '../../backends/httpdetails/index.js',
},
},
{
apiVersion: 'v1',
kind: 'Service',
metadata: {
namespace: 'default',
name: 'httpdetails',
labels: { app: 'httpdetails' },
},
spec: {
selector: { app: 'httpdetails' },
ports: [{
name: 'http',
port: 80,
targetPort: 'http',
}],
},
},
{
apiVersion: 'apps/v1',
kind: 'Deployment',
metadata: {
namespace: 'default',
name: 'httpdetails',
},
spec: {
replicas: 1,
selector: { matchLabels: { app: 'httpdetails' } },
template: {
metadata: {
labels: { app: 'httpdetails' },
},
spec: {
containers: [{
name: 'httpbin',
image: 'node:14-stretch-slim',
imagePullPolicy: 'IfNotPresent',
args: [
'node',
'/app/index.js',
],
ports: [{
name: 'http',
containerPort: 8080,
}],
volumeMounts: [{
name: 'httpdetails',
mountPath: '/app',
}],
}],
volumes: [{
name: 'httpdetails',
configMap: {
name: 'httpdetails',
},
}],
},
},
},
},
],
}

View file

@ -0,0 +1,143 @@
{
apiVersion: 'v1',
kind: 'List',
items: [
{
apiVersion: 'v1',
kind: 'Namespace',
metadata: { labels: { 'app.kubernetes.io/name': 'ingress-nginx', 'app.kubernetes.io/part-of': 'ingress-nginx' }, name: 'ingress-nginx' },
},
{
apiVersion: 'v1',
kind: 'ConfigMap',
metadata: { labels: { 'app.kubernetes.io/name': 'ingress-nginx', 'app.kubernetes.io/part-of': 'ingress-nginx' }, name: 'nginx-configuration', namespace: 'ingress-nginx' },
},
{
apiVersion: 'v1',
kind: 'ConfigMap',
metadata: { labels: { 'app.kubernetes.io/name': 'ingress-nginx', 'app.kubernetes.io/part-of': 'ingress-nginx' }, name: 'tcp-services', namespace: 'ingress-nginx' },
},
{
apiVersion: 'v1',
kind: 'ConfigMap',
metadata: { labels: { 'app.kubernetes.io/name': 'ingress-nginx', 'app.kubernetes.io/part-of': 'ingress-nginx' }, name: 'udp-services', namespace: 'ingress-nginx' },
},
{
apiVersion: 'v1',
kind: 'ServiceAccount',
metadata: { labels: { 'app.kubernetes.io/name': 'ingress-nginx', 'app.kubernetes.io/part-of': 'ingress-nginx' }, name: 'nginx-ingress-serviceaccount', namespace: 'ingress-nginx' },
},
{
apiVersion: 'rbac.authorization.k8s.io/v1beta1',
kind: 'ClusterRole',
metadata: { labels: { 'app.kubernetes.io/name': 'ingress-nginx', 'app.kubernetes.io/part-of': 'ingress-nginx' }, name: 'nginx-ingress-clusterrole' },
rules: [{ apiGroups: [''], resources: ['configmaps', 'endpoints', 'nodes', 'pods', 'secrets'], verbs: ['list', 'watch'] }, { apiGroups: [''], resources: ['nodes'], verbs: ['get'] }, { apiGroups: [''], resources: ['services'], verbs: ['get', 'list', 'watch'] }, { apiGroups: [''], resources: ['events'], verbs: ['create', 'patch'] }, { apiGroups: ['extensions', 'networking.k8s.io'], resources: ['ingresses'], verbs: ['get', 'list', 'watch'] }, { apiGroups: ['extensions', 'networking.k8s.io'], resources: ['ingresses/status'], verbs: ['update'] }],
},
{
apiVersion: 'rbac.authorization.k8s.io/v1beta1',
kind: 'Role',
metadata: { labels: { 'app.kubernetes.io/name': 'ingress-nginx', 'app.kubernetes.io/part-of': 'ingress-nginx' }, name: 'nginx-ingress-role', namespace: 'ingress-nginx' },
rules: [{ apiGroups: [''], resources: ['configmaps', 'pods', 'secrets', 'namespaces'], verbs: ['get'] }, { apiGroups: [''], resourceNames: ['ingress-controller-leader-nginx'], resources: ['configmaps'], verbs: ['get', 'update'] }, { apiGroups: [''], resources: ['configmaps'], verbs: ['create'] }, { apiGroups: [''], resources: ['endpoints'], verbs: ['get'] }],
},
{
apiVersion: 'rbac.authorization.k8s.io/v1beta1',
kind: 'RoleBinding',
metadata: { labels: { 'app.kubernetes.io/name': 'ingress-nginx', 'app.kubernetes.io/part-of': 'ingress-nginx' }, name: 'nginx-ingress-role-nisa-binding', namespace: 'ingress-nginx' },
roleRef: { apiGroup: 'rbac.authorization.k8s.io', kind: 'Role', name: 'nginx-ingress-role' },
subjects: [{ kind: 'ServiceAccount', name: 'nginx-ingress-serviceaccount', namespace: 'ingress-nginx' }],
},
{
apiVersion: 'rbac.authorization.k8s.io/v1beta1',
kind: 'ClusterRoleBinding',
metadata: { labels: { 'app.kubernetes.io/name': 'ingress-nginx', 'app.kubernetes.io/part-of': 'ingress-nginx' }, name: 'nginx-ingress-clusterrole-nisa-binding' },
roleRef: { apiGroup: 'rbac.authorization.k8s.io', kind: 'ClusterRole', name: 'nginx-ingress-clusterrole' },
subjects: [{ kind: 'ServiceAccount', name: 'nginx-ingress-serviceaccount', namespace: 'ingress-nginx' }],
},
{
apiVersion: 'apps/v1',
kind: 'Deployment',
metadata: { labels: { 'app.kubernetes.io/name': 'ingress-nginx', 'app.kubernetes.io/part-of': 'ingress-nginx' }, name: 'nginx-ingress-controller', namespace: 'ingress-nginx' },
spec: {
replicas: 1,
selector: { matchLabels: { 'app.kubernetes.io/name': 'ingress-nginx', 'app.kubernetes.io/part-of': 'ingress-nginx' } },
template: {
metadata: { annotations: { 'prometheus.io/port': '10254', 'prometheus.io/scrape': 'true' }, labels: { 'app.kubernetes.io/name': 'ingress-nginx', 'app.kubernetes.io/part-of': 'ingress-nginx' } },
spec: {
containers: [{
name: 'nginx-ingress-controller',
image: 'quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.30.0',
args: [
'/nginx-ingress-controller',
'--configmap=$(POD_NAMESPACE)/nginx-configuration',
'--tcp-services-configmap=$(POD_NAMESPACE)/tcp-services',
'--udp-services-configmap=$(POD_NAMESPACE)/udp-services',
'--publish-service=$(POD_NAMESPACE)/ingress-nginx',
'--annotations-prefix=nginx.ingress.kubernetes.io',
'--v=2',
],
env: [
{ name: 'POD_NAME', valueFrom: { fieldRef: { fieldPath: 'metadata.name' } } },
{ name: 'POD_NAMESPACE', valueFrom: { fieldRef: { fieldPath: 'metadata.namespace' } } },
],
lifecycle: { preStop: { exec: { command: ['/wait-shutdown'] } } },
livenessProbe: { failureThreshold: 3, httpGet: { path: '/healthz', port: 10254, scheme: 'HTTP' }, initialDelaySeconds: 10, periodSeconds: 10, successThreshold: 1, timeoutSeconds: 10 },
ports: [{ containerPort: 80, name: 'http', protocol: 'TCP' }, { containerPort: 443, name: 'https', protocol: 'TCP' }],
readinessProbe: { failureThreshold: 3, httpGet: { path: '/healthz', port: 10254, scheme: 'HTTP' }, periodSeconds: 10, successThreshold: 1, timeoutSeconds: 10 },
securityContext: { allowPrivilegeEscalation: true, capabilities: { add: ['NET_BIND_SERVICE'], drop: ['ALL'] }, runAsUser: 101 },
}],
nodeSelector: { 'kubernetes.io/os': 'linux' },
serviceAccountName: 'nginx-ingress-serviceaccount',
terminationGracePeriodSeconds: 300,
},
},
},
},
{
apiVersion: 'v1',
kind: 'LimitRange',
metadata: { labels: { 'app.kubernetes.io/name': 'ingress-nginx', 'app.kubernetes.io/part-of': 'ingress-nginx' }, name: 'ingress-nginx', namespace: 'ingress-nginx' },
spec: { limits: [{ min: { cpu: '100m', memory: '90Mi' }, type: 'Container' }] },
},
{
apiVersion: 'v1',
kind: 'Service',
metadata: {
namespace: 'ingress-nginx',
name: 'ingress-nginx',
labels: {
'app.kubernetes.io/name': 'ingress-nginx',
'app.kubernetes.io/part-of': 'ingress-nginx',
},
},
spec: {
type: 'ClusterIP',
clusterIP: '10.96.1.1',
selector: { 'app.kubernetes.io/name': 'ingress-nginx', 'app.kubernetes.io/part-of': 'ingress-nginx' },
ports: [
{ name: 'http', port: 80, protocol: 'TCP', targetPort: 'http' },
{ name: 'https', port: 443, protocol: 'TCP', targetPort: 'https' },
],
},
},
{
apiVersion: 'v1',
kind: 'Service',
metadata: {
namespace: 'ingress-nginx',
name: 'ingress-nginx-nodeport',
labels: {
'app.kubernetes.io/name': 'ingress-nginx',
'app.kubernetes.io/part-of': 'ingress-nginx',
},
},
spec: {
type: 'NodePort',
selector: { 'app.kubernetes.io/name': 'ingress-nginx', 'app.kubernetes.io/part-of': 'ingress-nginx' },
ports: [
{ name: 'http', port: 80, protocol: 'TCP', targetPort: 'http', nodePort: 30080 },
{ name: 'https', port: 443, protocol: 'TCP', targetPort: 'https', nodePort: 30443 },
],
},
},
],
}

View file

@ -0,0 +1,337 @@
local tls = import './tls.libsonnet';
local PomeriumPolicy = function() [
{
from: 'http://httpdetails.localhost.pomerium.io',
prefix: '/by-domain',
to: 'http://httpdetails.default.svc.cluster.local',
allowed_domains: ['dogs.test'],
},
{
from: 'http://httpdetails.localhost.pomerium.io',
prefix: '/by-user',
to: 'http://httpdetails.default.svc.cluster.local',
allowed_users: ['bob@dogs.test'],
},
{
from: 'http://httpdetails.localhost.pomerium.io',
prefix: '/by-group',
to: 'http://httpdetails.default.svc.cluster.local',
allowed_groups: ['admin'],
},
{
from: 'http://httpdetails.localhost.pomerium.io',
to: 'http://httpdetails.default.svc.cluster.local',
allow_public_unauthenticated_access: true,
},
];
local PomeriumPolicyHash = std.base64(std.md5(std.manifestJsonEx(PomeriumPolicy(), '')));
local PomeriumTLSSecret = function() {
apiVersion: 'v1',
kind: 'Secret',
type: 'kubernetes.io/tls',
metadata: {
namespace: 'default',
name: 'pomerium-tls',
},
data: {
'tls.crt': std.base64(tls.cert),
'tls.key': std.base64(tls.key),
},
};
local PomeriumCAsConfigMap = function() {
apiVersion: 'v1',
kind: 'ConfigMap',
metadata: {
namespace: 'default',
name: 'pomerium-cas',
labels: {
'app.kubernetes.io/part-of': 'pomerium',
},
},
data: {
'pomerium.crt': tls.ca,
},
};
local PomeriumConfigMap = function() {
apiVersion: 'v1',
kind: 'ConfigMap',
metadata: {
namespace: 'default',
name: 'pomerium',
labels: {
'app.kubernetes.io/part-of': 'pomerium',
},
},
data: {
ADDRESS: ':443',
GRPC_ADDRESS: ':5080',
GRPC_INSECURE: 'true',
DEBUG: 'true',
LOG_LEVEL: 'debug',
AUTHENTICATE_SERVICE_URL: 'https://authenticate.localhost.pomerium.io',
AUTHENTICATE_CALLBACK_PATH: '/oauth2/callback',
AUTHORIZE_SERVICE_URL: 'http://authorize.default.svc.cluster.local:5080',
CACHE_SERVICE_URL: 'http://cache.default.svc.cluster.local:5080',
FORWARD_AUTH_URL: 'https://forward-authenticate.localhost.pomerium.io',
SHARED_SECRET: 'Wy+c0uSuIM0yGGXs82MBwTZwRiZ7Ki2T0LANnmzUtkI=',
COOKIE_SECRET: 'eZ91a/j9fhgki9zPDU5zHdQWX4io89pJanChMVa5OoM=',
CERTIFICATE: std.base64(tls.cert),
CERTIFICATE_KEY: std.base64(tls.key),
IDP_PROVIDER: 'oidc',
IDP_PROVIDER_URL: 'https://openid.localhost.pomerium.io',
IDP_CLIENT_ID: 'pomerium-authenticate',
IDP_CLIENT_SECRET: 'pomerium-authenticate-secret',
POLICY: std.base64(std.manifestYamlDoc(PomeriumPolicy())),
},
};
local PomeriumDeployment = function(svc) {
apiVersion: 'apps/v1',
kind: 'Deployment',
metadata: {
namespace: 'default',
name: 'pomerium-' + svc,
labels: {
app: 'pomerium-' + svc,
'app.kubernetes.io/part-of': 'pomerium',
},
},
spec: {
replicas: 1,
selector: {
matchLabels: {
app: 'pomerium-' + svc,
},
},
template: {
metadata: {
labels: {
app: 'pomerium-' + svc,
'app.kubernetes.io/part-of': 'pomerium',
},
annotations: {
'policy-version': PomeriumPolicyHash,
},
},
spec: {
hostAliases: [{
ip: '10.96.1.1',
hostnames: [
'openid.localhost.pomerium.io',
],
}],
initContainers: [{
name: 'pomerium-' + svc + '-certs',
image: 'buildpack-deps:buster-curl',
imagePullPolicy: 'Always',
command: ['sh', '-c', |||
cp /incoming-certs/* /usr/local/share/ca-certificates
update-ca-certificates
|||],
volumeMounts: [
{
name: 'incoming-certs',
mountPath: '/incoming-certs',
},
{
name: 'outgoing-certs',
mountPath: '/etc/ssl/certs',
},
],
}],
containers: [{
name: 'pomerium-' + svc,
image: 'pomerium/pomerium:dev',
imagePullPolicy: 'IfNotPresent',
envFrom: [{
configMapRef: { name: 'pomerium' },
}],
env: [{
name: 'SERVICES',
value: svc,
}],
ports: [
{ name: 'https', containerPort: 443 },
{ name: 'grpc', containerPort: 5080 },
],
volumeMounts: [
{
name: 'outgoing-certs',
mountPath: '/etc/ssl/certs',
},
],
}],
volumes: [
{
name: 'incoming-certs',
configMap: {
name: 'pomerium-cas',
},
},
{
name: 'outgoing-certs',
emptyDir: {},
},
],
},
},
},
};
local PomeriumService = function(svc) {
apiVersion: 'v1',
kind: 'Service',
metadata: {
namespace: 'default',
name: svc,
labels: {
app: 'pomerium-' + svc,
'app.kubernetes.io/part-of': 'pomerium',
},
},
spec: {
ports: [
{
name: 'https',
port: 443,
targetPort: 'https',
},
{
name: 'grpc',
port: 5080,
targetPort: 'grpc',
},
],
selector: {
app: 'pomerium-' + svc,
},
},
};
local PomeriumIngress = function() {
local proxyHosts = [
'forward-authenticate.localhost.pomerium.io',
'httpecho.localhost.pomerium.io',
'httpdetails.localhost.pomerium.io',
],
apiVersion: 'extensions/v1beta1',
kind: 'Ingress',
metadata: {
namespace: 'default',
name: 'pomerium',
annotations: {
'kubernetes.io/ingress.class': 'nginx',
'nginx.ingress.kubernetes.io/backend-protocol': 'HTTPS',
'nginx.ingress.kubernetes.io/proxy-buffer-size': '16k',
},
},
spec: {
tls: [
{
hosts: [
'authenticate.localhost.pomerium.io',
] + proxyHosts,
secretName: 'pomerium-tls',
},
],
rules: [
{
host: 'authenticate.localhost.pomerium.io',
http: {
paths: [
{
path: '/',
backend: {
serviceName: 'authenticate',
servicePort: 'https',
},
},
],
},
},
] + [{
host: host,
http: {
paths: [{
path: '/',
backend: {
serviceName: 'proxy',
servicePort: 'https',
},
}],
},
} for host in proxyHosts],
},
};
local PomeriumForwardAuthIngress = function() {
apiVersion: 'extensions/v1beta1',
kind: 'Ingress',
metadata: {
namespace: 'default',
name: 'pomerium-fa',
annotations: {
'kubernetes.io/ingress.class': 'nginx',
'nginx.ingress.kubernetes.io/auth-url': 'https://forward-authenticate.localhost.pomerium.io/verify?uri=$scheme://$host$request_uri',
'nginx.ingress.kubernetes.io/auth-signin': 'https://forward-authenticate.localhost.pomerium.io/?uri=$scheme://$host$request_uri',
'nginx.ingress.kubernetes.io/proxy-buffer-size': '16k',
},
},
spec: {
tls: [
{
hosts: [
'fa-httpecho.localhost.pomerium.io',
],
secretName: 'pomerium-tls',
},
],
rules: [
{
host: 'fa-httpecho.localhost.pomerium.io',
http: {
paths: [
{
path: '/',
backend: {
serviceName: 'httpecho',
servicePort: 'http',
},
},
],
},
},
],
},
};
{
apiVersion: 'v1',
kind: 'List',
items: [
PomeriumConfigMap(),
PomeriumCAsConfigMap(),
PomeriumTLSSecret(),
PomeriumService('authenticate'),
PomeriumDeployment('authenticate'),
PomeriumService('authorize'),
PomeriumDeployment('authorize'),
PomeriumService('cache'),
PomeriumDeployment('cache'),
PomeriumService('proxy'),
PomeriumDeployment('proxy'),
PomeriumIngress(),
PomeriumForwardAuthIngress(),
],
}

View file

@ -0,0 +1,106 @@
local Service = function() {
apiVersion: 'v1',
kind: 'Service',
metadata: {
namespace: 'default',
name: 'openid',
labels: {
app: 'openid',
'app.kubernetes.io/part-of': 'openid',
},
},
spec: {
selector: { app: 'openid' },
ports: [
{
name: 'http',
port: 80,
targetPort: 'http',
},
],
},
};
local Deployment = function() {
apiVersion: 'apps/v1',
kind: 'Deployment',
metadata: {
namespace: 'default',
name: 'openid',
labels: {
app: 'openid',
'app.kubernetes.io/part-of': 'openid',
},
},
spec: {
replicas: 1,
selector: { matchLabels: { app: 'openid' } },
template: {
metadata: {
labels: {
app: 'openid',
'app.kubernetes.io/part-of': 'openid',
},
},
spec: {
containers: [{
name: 'openid',
image: 'quay.io/calebdoxsey/reference-openid-provider:latest',
imagePullPolicy: 'IfNotPresent',
ports: [
{ name: 'http', containerPort: 6080 },
],
}],
},
},
},
};
local Ingress = function() {
apiVersion: 'extensions/v1beta1',
kind: 'Ingress',
metadata: {
namespace: 'default',
name: 'openid',
annotations: {
'kubernetes.io/ingress.class': 'nginx',
'nginx.ingress.kubernetes.io/backend-protocol': 'HTTP',
},
},
spec: {
tls: [
{
hosts: [
'openid.localhost.pomerium.io',
],
secretName: 'pomerium-tls',
},
],
rules: [
{
host: 'openid.localhost.pomerium.io',
http: {
paths: [
{
path: '/',
backend: {
serviceName: 'openid',
servicePort: 'http',
},
},
],
},
},
],
},
};
{
apiVersion: 'v1',
kind: 'List',
items: [
Service(),
Deployment(),
Ingress(),
],
}

View file

@ -0,0 +1,5 @@
{
cert: std.extVar('tls-cert'),
key: std.extVar('tls-key'),
ca: std.extVar('tls-ca'),
}

View file

@ -0,0 +1,10 @@
local httpdetails = import './lib/httpdetails.libsonnet';
local nginxIngressController = import './lib/nginx-ingress-controller.libsonnet';
local pomerium = import './lib/pomerium.libsonnet';
local openid = import './lib/reference-openid-provider.libsonnet';
{
apiVersion: 'v1',
kind: 'List',
items: nginxIngressController.items + pomerium.items + openid.items + httpdetails.items,
}