Repository: dexidp/dex
Pull request #1416 (2019-03-04 17:16:18)

Use http.FileSystem for web assets
This uses an `http.FileSystem` for web assets so that we can embed static assets into the dex binary. This makes it much easier to deploy. 

We use [packr](https://github.com/gobuffalo/packr), but there are many options available.

ericchiang: Can we add a flag for os.Open or template.Load operations to use an in memory lookup instead to reduce the diff? :)
pivotal-jwinters: Doesn't that mean we'd be introducing two separate code paths that need to be maintained? If it's necessary I don't mind doing it, just wondering what the reasoning is?
ericchiang: > Doesn't that mean we'd be introducing two separate code paths that need to be maintained? If it's necessary I don't mind doing it, just wondering what the reasoning is?

Sorry, this diff is just really hard to read. I agree that faking the filesystem is probably the best way going forward, but there's a lot of extra changes that are confusing (like modifying the `join` method or change to `server/server.go`).

As a start, can you just replace any interaction with the file system in `loadWebConfig` to use an interface? There shouldn't be any changes to the method signatures yet. The smaller the diff is, the easier it is for me to review :)

> We use packr, but there are many options available.

I don't think we need an external package for this. It's reasonable for dex to implement itself.
xtremerui: @bonifaido here is another legacy PR we have made. It has been updated recently to resolve conflicts and adapt changes.Is there a chance that you or one of the collaborator review this again so we could move forward?  

We know the [documentation of templates](https://github.com/dexidp/dex/blob/master/Documentation/templates.md) needs to be updated but just wanted to gather some early suggestions before that.

Thank you!
xtremerui: @sagikazarmark Is it possible to give some directions about moving the PR forward? I have synced it again with upstream. 

Originally we have another PR for https://github.com/dexidp/dex/pull/1416/commits/1265e8660e9727faf301cce5ef2454ee9f01f986 but since it is built on top off this file system PR, I just cherry-picked the changes of `hostURL` into this PR.

Any input is appreciated. Thx!
sagikazarmark: @xtremerui sorry for the delay. Just for you to know: this is on my radar, but I'm super busy lately, so I'm not sure when I get to reviewing this.

In general, I like the idea a lot, although using packr and embedded assets will essentially make it impossible to extend the docker image with a custom theme without recompiling the binary itself. I'd even say that it's a breaking change.

That being said, we could probably embed static resources from this repo and then look at the filesystem for other themes.

I realize this change does not actually include packr, just wanted to note the above for the future. Also, pkger seems to be the state of the art tool these days: https://github.com/markbates/pkger

I'll review this PR as soon as I can. Thanks for your patience!
tkleczek: Current version panics when run `./bin/dex serve examples/config-dev.yaml`. I believe Server.Web.Dir is never initialized.

```
[tkleczek@thinkpad dex-http-filesystem]$ ./bin/dex serve examples/config-dev.yaml 
time="2020-09-29T19:05:10Z" level=info msg="config issuer: http://127.0.0.1:5556/dex"
time="2020-09-29T19:05:11Z" level=info msg="config storage: sqlite3"
time="2020-09-29T19:05:11Z" level=info msg="config static client: Example App"
time="2020-09-29T19:05:11Z" level=info msg="config connector: mock"
time="2020-09-29T19:05:11Z" level=info msg="config connector: local passwords enabled"
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x18 pc=0xa51f87]

goroutine 1 [running]:
github.com/dexidp/dex/server.loadTemplate(0x0, 0x0, 0x101f6e9, 0xa, 0xc00035e9d8, 0xc000490720, 0x0, 0x0, 0x0)
        /home/tkleczek/Projects/dex-http-filesystem/server/templates.go:193 +0xb7
github.com/dexidp/dex/server.loadTemplates(0x0, 0x0, 0x1022a78, 0xe, 0x1018bdb, 0x3, 0x101b1f7, 0x6, 0x0, 0x0, ...)
        /home/tkleczek/Projects/dex-http-filesystem/server/templates.go:72 +0x596
github.com/dexidp/dex/server.newServer(0x1193f40, 0xc000130040, 0xc00021f2c0, 0x19, 0x11aff40, 0xc0004904e0, 0xc00048c440, 0x1, 0x1, 0x0, ...)
        /home/tkleczek/Projects/dex-http-filesystem/server/server.go:201 +0x192
github.com/dexidp/dex/server.NewServer(0x1193f40, 0xc000130040, 0xc00021f2c0, 0x19, 0x11aff40, 0xc0004904e0, 0x0, 0x0, 0x0, 0x0, ...)
        /home/tkleczek/Projects/dex-http-filesystem/server/server.go:172 +0xe4
main.serve(0xc0003c9180, 0xc0002d1eb0, 0x1, 0x1, 0x4, 0xcdbf70)
        /home/tkleczek/Projects/dex-http-filesystem/cmd/dex/serve.go:280 +0x1749
main.commandServe.func1(0xc0003c9180, 0xc0002d1eb0, 0x1, 0x1)
        /home/tkleczek/Projects/dex-http-filesystem/cmd/dex/serve.go:39 +0x4d
github.com/spf13/cobra.(*Command).execute(0xc0003c9180, 0xc0002d1e80, 0x1, 0x1, 0xc0003c9180, 0xc0002d1e80)
        /home/tkleczek/go/pkg/mod/github.com/spf13/cobra@v0.0.5/command.go:830 +0x29d
github.com/spf13/cobra.(*Command).ExecuteC(0xc0003c8f00, 0x5, 0xc00035ff78, 0x40d3df)
        /home/tkleczek/go/pkg/mod/github.com/spf13/cobra@v0.0.5/command.go:914 +0x2fb
github.com/spf13/cobra.(*Command).Execute(...)
        /home/tkleczek/go/pkg/mod/github.com/spf13/cobra@v0.0.5/command.go:864
main.main()
        /home/tkleczek/Projects/dex-http-filesystem/cmd/dex/main.go:24 +0x2b
```
xtremerui: I will fix the error and update the PR accordingly. Thx!
xtremerui: @sagikazarmark the PR is updated to use pkger for embedding static contents. What do you think? Thx.
sagikazarmark: I'm sorry this took so long. I suggest using Go 1.16 embedding for this. Let's keep this open for now, but chances are I'll rework it once Go 1.16 is out.
xtremerui: @sagikazarmark we are also targeting the go embeded feature so we could use it in Concourse itself. Will update this PR after 1.16 is released. 
sagikazarmark: Awesome! Thanks a lot! Sorry for the long delay again.
xtremerui: @sagikazarmark I have updated the PR to utilize go 1.16 new features. And also simplified the PR to keep only minimum changes that it needs. I have tested this PR in Concourse and the fs.FS works well. Please let me know. Thx!

Patch:
diff --git a/go.sum b/go.sum
index 69b3bcf4..3e624f53 100644
--- a/go.sum
+++ b/go.sum
@@ -1,6 +1,5 @@
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo=
 cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
 cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
 cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
@@ -15,7 +14,6 @@ cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiy
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 github.com/AppsFlyer/go-sundheit v0.3.1 h1:Zqnr3wV3WQmXonc234k9XZAoV2KHUHw3osR5k2iHQZE=
 github.com/AppsFlyer/go-sundheit v0.3.1/go.mod h1:iZ8zWMS7idcvmqewf5mEymWWgoOiG/0WD4+aeh+heX4=
-github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
@@ -42,9 +40,7 @@ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa h1:OaNxuTZr7kxeODyLWsRMC+OD03aFUH+mW6r2d+MWa5Y=
 github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
-github.com/coreos/bbolt v1.3.2 h1:wZwiHHUieZCquLkDL0B8UhzreNWsPHooDAG3q34zk0s=
 github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
-github.com/coreos/etcd v3.3.13+incompatible h1:8F3hqu9fGYLBifCmRCJsicFqDx/D68Rt3q1JMazcgBQ=
 github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
 github.com/coreos/go-oidc/v3 v3.0.0 h1:/mAA0XMgYJw2Uqm7WKGCsKnjitE/+A0FFbOmiRJm7LQ=
 github.com/coreos/go-oidc/v3 v3.0.0/go.mod h1:rEJ/idjfUyfkBit1eI1fvyr+64/g9dcKpAm8MJMesvo=
@@ -74,7 +70,6 @@ github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8S
 github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
 github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
-github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
@@ -87,7 +82,6 @@ github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gG
 github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
-github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
 github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
 github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
 github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
@@ -264,7 +258,6 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
 github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
-github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
 github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
 github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
@@ -280,7 +273,6 @@ github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M=
 github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo=
 github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
 github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
-github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
 github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
@@ -293,7 +285,6 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
-github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -339,7 +330,6 @@ golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTk
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
 golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
 golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
@@ -363,13 +353,11 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR
 golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200505041828-1ed23360d12c h1:zJ0mtu4jCalhKg6Oaukv6iIkb+cOvDrajDH9DH46Q4M=
 golang.org/x/net v0.0.0-20200505041828-1ed23360d12c/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/net v0.0.0-20201224014010-6772e930b67b h1:iFwSg7t5GZmB/Q5TjiEAsdoLDrdJRC1RiF2WhuV29Qw=
 golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -378,7 +366,6 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180117170059-2c42eef0765b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -408,7 +395,6 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20171227012246-e19ae1496984/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -456,7 +442,6 @@ google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRn
 google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
 google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
 google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE=
 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
 google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
 google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a h1:Ob5/580gVHBJZgXnff1cZDbG+xLtMVE5mDRTe+nIsX4=
@@ -501,7 +486,6 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
 sigs.k8s.io/testing_frameworks v0.1.2 h1:vK0+tvjF0BZ/RYFeZ1E6BYBwHJJXhjuZ3TdsEKH+UQM=
diff --git a/server/server.go b/server/server.go
index a79b7cfd..17d72af3 100644
--- a/server/server.go
+++ b/server/server.go
@@ -6,6 +6,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"io/fs"
 	"net/http"
 	"net/url"
 	"path"
@@ -100,7 +101,7 @@ type Config struct {
 
 // WebConfig holds the server's frontend templates and asset configuration.
 type WebConfig struct {
-	// A filepath to web static.
+	// A file path to web static. If set, WebFS will be ignored.
 	//
 	// It is expected to contain the following directories:
 	//
@@ -110,6 +111,12 @@ type WebConfig struct {
 	//
 	Dir string
 
+	// Alternative way to configure web static filesystem. Dir overrides this.
+	// It's expected to contain the same files and directories as mentioned
+	// above in Dir doc.
+	//
+	WebFS fs.FS
+
 	// Defaults to "( issuer URL )/theme/logo.png"
 	LogoURL string
 
@@ -205,6 +212,7 @@ func newServer(ctx context.Context, c Config, rotationStrategy rotationStrategy)
 
 	web := webConfig{
 		dir:       c.Web.Dir,
+		webFS:     c.Web.WebFS,
 		logoURL:   c.Web.LogoURL,
 		issuerURL: c.Issuer,
 		issuer:    c.Web.Issuer,
@@ -343,6 +351,7 @@ func newServer(ctx context.Context, c Config, rotationStrategy rotationStrategy)
 		}
 		fmt.Fprintf(w, "Health check passed")
 	}))
+
 	handlePrefix("/static", static)
 	handlePrefix("/theme", theme)
 	s.mux = r
diff --git a/server/templates.go b/server/templates.go
index bed1c6c8..ca5e4d24 100644
--- a/server/templates.go
+++ b/server/templates.go
@@ -4,7 +4,7 @@ import (
 	"fmt"
 	"html/template"
 	"io"
-	"io/ioutil"
+	"io/fs"
 	"net/http"
 	"net/url"
 	"os"
@@ -46,6 +46,7 @@ type templates struct {
 
 type webConfig struct {
 	dir       string
+	webFS     fs.FS
 	logoURL   string
 	issuer    string
 	theme     string
@@ -53,22 +54,9 @@ type webConfig struct {
 	extra     map[string]string
 }
 
-func dirExists(dir string) error {
-	stat, err := os.Stat(dir)
-	if err != nil {
-		if os.IsNotExist(err) {
-			return fmt.Errorf("directory %q does not exist", dir)
-		}
-		return fmt.Errorf("stat directory %q: %v", dir, err)
-	}
-	if !stat.IsDir() {
-		return fmt.Errorf("path %q is a file not a directory", dir)
-	}
-	return nil
-}
-
 // loadWebConfig returns static assets, theme assets, and templates used by the frontend by
-// reading the directory specified in the webConfig.
+// reading the dir specified in the webConfig. If directory is not specified it will
+// use the file system specified by webFS.
 //
 // The directory layout is expected to be:
 //
@@ -89,37 +77,34 @@ func loadWebConfig(c webConfig) (http.Handler, http.Handler, *templates, error)
 	if c.issuer == "" {
 		c.issuer = "dex"
 	}
-	if c.dir == "" {
-		c.dir = "./web"
+	if c.dir != "" {
+		c.webFS = os.DirFS(c.dir)
+	} else if c.webFS == nil {
+		c.webFS = os.DirFS("./web")
 	}
 	if c.logoURL == "" {
 		c.logoURL = "theme/logo.png"
 	}
 
-	if err := dirExists(c.dir); err != nil {
-		return nil, nil, nil, fmt.Errorf("load web dir: %v", err)
+	staticFiles, err := fs.Sub(c.webFS, "static")
+	if err != nil {
+		return nil, nil, nil, fmt.Errorf("read static dir: %v", err)
 	}
-
-	staticDir := filepath.Join(c.dir, "static")
-	templatesDir := filepath.Join(c.dir, "templates")
-	themeDir := filepath.Join(c.dir, "themes", c.theme)
-
-	for _, dir := range []string{staticDir, templatesDir, themeDir} {
-		if err := dirExists(dir); err != nil {
-			return nil, nil, nil, fmt.Errorf("load dir: %v", err)
-		}
+	themeFiles, err := fs.Sub(c.webFS, filepath.Join("themes", c.theme))
+	if err != nil {
+		return nil, nil, nil, fmt.Errorf("read themes dir: %v", err)
 	}
 
-	static := http.FileServer(http.Dir(staticDir))
-	theme := http.FileServer(http.Dir(themeDir))
+	static := http.FileServer(http.FS(staticFiles))
+	theme := http.FileServer(http.FS(themeFiles))
 
-	templates, err := loadTemplates(c, templatesDir)
+	templates, err := loadTemplates(c, "templates")
 	return static, theme, templates, err
 }
 
 // loadTemplates parses the expected templates from the provided directory.
 func loadTemplates(c webConfig, templatesDir string) (*templates, error) {
-	files, err := ioutil.ReadDir(templatesDir)
+	files, err := fs.ReadDir(c.webFS, templatesDir)
 	if err != nil {
 		return nil, fmt.Errorf("read dir: %v", err)
 	}
@@ -148,7 +133,7 @@ func loadTemplates(c webConfig, templatesDir string) (*templates, error) {
 		"extra":  func(k string) string { return c.extra[k] },
 	}
 
-	tmpls, err := template.New("").Funcs(funcs).ParseFiles(filenames...)
+	tmpls, err := template.New("").Funcs(funcs).ParseFS(c.webFS, filenames...)
 	if err != nil {
 		return nil, fmt.Errorf("parse files: %v", err)
 	}
diff --git a/web/templates/header.html b/web/templates/header.html
index 0d4fea0f..8cf744e5 100644
--- a/web/templates/header.html
+++ b/web/templates/header.html
@@ -18,4 +18,3 @@
     </div>
 
     <div class="dex-container">
-


Test patch:
diff --git a/server/handlers_test.go b/server/handlers_test.go
index 8ad59d94..d195af64 100644
--- a/server/handlers_test.go
+++ b/server/handlers_test.go
@@ -7,6 +7,7 @@ import (
 	"errors"
 	"net/http"
 	"net/http/httptest"
+	"os"
 	"testing"
 	"time"
 
@@ -135,7 +136,7 @@ func TestHandleInvalidSAMLCallbacks(t *testing.T) {
 func TestConnectorLoginDoesNotAllowToChangeConnectorForAuthRequest(t *testing.T) {
 	memStorage := memory.New(logger)
 
-	templates, err := loadTemplates(webConfig{}, "../web/templates")
+	templates, err := loadTemplates(webConfig{webFS: os.DirFS("../web")}, "templates")
 	if err != nil {
 		t.Fatal("failed to load templates")
 	}