Use go-oidc-agent for authentication (#204)

* Use go-oidc-agent for authentication

Creates a "backend-for-frontend" using go-oidc-agent which now handles
authentication on the clients behalf. This avoids us having to populate
client-id/client-secret on every client since the backend now provides
this information to the frontend.

Signed-off-by: Dave Tucker <dave@dtucker.co.uk>

* Use go-oidc-agent for cli

Signed-off-by: Dave Tucker <dave@dtucker.co.uk>

* deploy: Kustomize the kube deployment

Signed-off-by: Dave Tucker <dave@dtucker.co.uk>

* Move integration tests to use kind

Signed-off-by: Dave Tucker <dave@dtucker.co.uk>

* Fix GH workflow

Signed-off-by: Dave Tucker <dave@dtucker.co.uk>

* tests: Remove old tests

Signed-off-by: Dave Tucker <dave@dtucker.co.uk>

* client: Fix missing id_token in Device Flow

Signed-off-by: Dave Tucker <dave@dtucker.co.uk>
This commit is contained in:
Dave Tucker
2022-12-02 17:10:14 +00:00
committed by GitHub
parent 68663c7468
commit 87b9a5db1e
93 changed files with 2180 additions and 4656 deletions
+1 -28
View File
@@ -1,30 +1,3 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Built binaries
/apex
apex-amd64-darwin
apex-amd64-linux
/controller
# Ops files
ops/
tests/
docs/
**/peer-inventory/*
**/endpoints.toml
**/default-ipam.json
ui/node_modules
docker-compose.yml
Makefile
+9 -14
View File
@@ -1,18 +1,13 @@
CONTROLLER_DB_USER=controller
CONTROLLER_DB_PASSWORD=floofykittens
CONTROLLER_DB_HOST=controller-db
APISERVER_DB_HOST=apiserver-db
APISERVER_DB_USER=apiserver
APISERVER_DB_PASSWORD=floofykittens
IPAM_DB_USER=ipam
IPAM_DB_PASSWORD=floofykittens
IPAM_DB_HOST=ipam-db
IPAM_ADDRESS=http://ipam:9090
KEYCLOAK_DB_USER=keycloak
KEYCLOAK_DB_PASSWORD=floofykittens
KEYCLOAK_DB_HOST=keycloak-db
KEYCLOAK_ADMIN_USER=admin
KEYCLOAK_ADMIN_PASSWORD=floofykittens
KEYCLOAK_URL=http://localhost:8080/auth
UI_URL=http://localhost:8080
VITE_KEYCLOAK_URL=/auth
VITE_KEYCLOAK_REALM=controller
VITE_KEYCLOAK_CLIENT_ID=front-controller
VITE_CONTROLLER_URL=/api
UI_URL=http://apex.local
API_URL=http://api.apex.local
APEX_OIDC_URL=http://auth.apex.local
APEX_OIDC_CLIENT_ID=apex-web
APEX_OIDC_CLIENT_SECRET=dhEN2dsqyUg5qmaDAdqi4CmH
APEX_OIDC_CLIENT_ID_CLI=apex-cli
+6 -34
View File
@@ -12,7 +12,6 @@ on:
- '**/*.md'
- '**/*.gitignore'
jobs:
lint:
runs-on: ubuntu-latest
@@ -88,13 +87,6 @@ jobs:
name: e2e-integration
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
deployment: ["wireguard", "fedora-wireguard"]
env:
JOB_NAME: "apex-e2e-${{ matrix.deployment }}"
DEPLOYMENT: ${{ matrix.deployment }}
steps:
- name: checkout
uses: actions/checkout@v2
@@ -104,36 +96,16 @@ jobs:
with:
go-version: 1.19
- name: Add hosts to /etc/hosts
run: |
echo "127.0.0.1 auth.apex.local api.apex.local apex.local" | sudo tee -a /etc/hosts
- name: Run e2e
run: |
make OS_IMAGE=quay.io/networkstatic/${{ matrix.DEPLOYMENT }} e2e
- name: Archive logs
uses: actions/upload-artifact@v3
if: always()
with:
name: ${{ matrix.deployment }}-logs
path: docker-compose.log
go-e2e:
needs: lint
name: go-e2e-integration
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.19
- name: Run go-e2e
run: |
make go-e2e
make e2e
deploy:
needs: [ "build", "e2e", "go-e2e" ]
needs: [ "build", "e2e" ]
permissions:
id-token: write
contents: read
+19 -88
View File
@@ -7,10 +7,8 @@ on:
env:
REGISTRY: quay.io
REPOSITORY: apex
CONTROLLER_IMAGE_NAME: controller
CONTROLLER_UI_IMAGE_NAME: controller-ui
APEX_IMAGE_NAME: apex
KEYCLOAK_IMAGE_NAME: keycloak
APISERVER_IMAGE_NAME: apiserver
APISERVER_UI_IMAGE_NAME: frontend
IMAGE_TAG: latest
jobs:
@@ -28,102 +26,35 @@ jobs:
username: ${{ secrets.QUAY_ROBOT_USERNAME }}
password: ${{ secrets.QUAY_ROBOT_PASSWORD }}
- name: Extract metadata (tags, labels) for apex controller image
id: meta-controller
uses: docker/metadata-action@v3.6.2
with:
images: ${{ env.REGISTRY }}/${{ env.REPOSITORY }}/${{ env.CONTROLLER_IMAGE_NAME }}
tags: |
type=raw,value=${{ env.IMAGE_TAG }}
- name: Build controller image
id: build-controller
- name: Build apiserver image
id: build-apiserver
uses: redhat-actions/buildah-build@v2
with:
image: controller
tags: ${{ steps.meta-controller.outputs.tags }}
labels: ${{ steps.meta-controller.outputs.labels }}
image: apiserver
tags: latest ${{ github.sha }} ${{github.ref_name}}
containerfiles: |
./Containerfile.controller
./Containerfile.apiserver
- name: Push controller to quay.io/
- name: Push apiserver to quay.io/
uses: redhat-actions/push-to-registry@v2
with:
image: ${{ steps.build-controller.outputs.image }}
tags: ${{ steps.build-controller.outputs.tags }}
image: ${{ steps.build-apiserver.outputs.image }}
tags: ${{ steps.build-apiserver.outputs.tags }}
registry: ${{ env.REGISTRY }}/${{ env.REPOSITORY }}
- name: Extract metadata (tags, labels) for controller-ui image
id: meta-controller-ui
uses: docker/metadata-action@v3.6.2
with:
images: ${{ env.REGISTRY }}/${{ env.REPOSITORY }}/${{ env.CONTROLLER_UI_IMAGE_NAME }}
tags: |
type=raw,value=${{ env.IMAGE_TAG }}
- name: Build controller-ui image
id: build-controller-ui
- name: Build frontend image
id: build-frontend
uses: redhat-actions/buildah-build@v2
with:
image: controller-ui
tags: ${{ steps.meta-controller-ui.outputs.tags }}
labels: ${{ steps.meta-controller-ui.outputs.labels }}
image: apiserver-ui
tags: latest ${{ github.sha }} ${{github.ref_name}}
labels: ${{ steps.meta-apiserver-ui.outputs.labels }}
containerfiles: |
./Containerfile.ui
./Containerfile.frontend
- name: Push controller-ui to quay.io
- name: Push frontend to quay.io
uses: redhat-actions/push-to-registry@v2
with:
image: ${{ steps.build-controller-ui.outputs.image }}
tags: ${{ steps.build-controller-ui.outputs.tags }}
registry: ${{ env.REGISTRY }}/${{ env.REPOSITORY }}
- name: Extract metadata (tags, labels) for apex agent image
id: meta-apex
uses: docker/metadata-action@v3.6.2
with:
images: ${{ env.REGISTRY }}/${{ env.REPOSITORY }}/${{ env.APEX_IMAGE_NAME }}
tags: |
type=raw,value=${{ env.IMAGE_TAG }}
- name: Build apex image
id: build-apex
uses: redhat-actions/buildah-build@v2
with:
image: apex
tags: ${{ steps.meta-apex.outputs.tags }}
labels: ${{ steps.meta-apex.outputs.labels }}
containerfiles: |
./Containerfile.apex
- name: Push apex to quay.io/
uses: redhat-actions/push-to-registry@v2
with:
image: ${{ steps.build-apex.outputs.image }}
tags: ${{ steps.build-apex.outputs.tags }}
registry: ${{ env.REGISTRY }}/${{ env.REPOSITORY }}
- name: Extract metadata (tags, labels) for keycloak image
id: meta-keycloak
uses: docker/metadata-action@v3.6.2
with:
images: ${{ env.REGISTRY }}/${{ env.REPOSITORY }}/${{ env.KEYCLOAK_IMAGE_NAME }}
tags: |
type=raw,value=${{ env.IMAGE_TAG }}
- name: Build keycloak image
id: build-keycloak
uses: redhat-actions/buildah-build@v2
with:
image: keycloak
tags: ${{ steps.meta-keycloak.outputs.tags }}
labels: ${{ steps.meta-keycloak.outputs.labels }}
containerfiles: |
./Containerfile.keycloak
- name: Push keycloak to quay.io/
uses: redhat-actions/push-to-registry@v2
with:
image: ${{ steps.build-keycloak.outputs.image }}
tags: ${{ steps.build-keycloak.outputs.tags }}
image: ${{ steps.build-frontend.outputs.image }}
tags: ${{ steps.build-frontend.outputs.tags }}
registry: ${{ env.REGISTRY }}/${{ env.REPOSITORY }}
+1
View File
@@ -28,3 +28,4 @@ docker-compose.log
# config
.env.*
coredns.yaml
+1 -1
View File
@@ -13,4 +13,4 @@
]
regexes = [
# '''custom-regexes-to-ignore-here''',
]
]
-18
View File
@@ -1,18 +0,0 @@
FROM docker.io/library/golang:1.19-alpine as build
WORKDIR /src
COPY go.mod .
COPY go.sum .
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build \
-ldflags="-extldflags=-static" \
-o apex ./cmd/apex
FROM docker.io/library/alpine:3.16
RUN apk add --no-cache wireguard-tools iputils
COPY --from=build /src/apex /bin/apex
EXPOSE 8080
ENTRYPOINT [ "/apex" ]
@@ -8,10 +8,10 @@ RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build \
-ldflags="-extldflags=-static" \
-o controller ./cmd/apexcontroller
-o apiserver ./cmd/apiserver
FROM registry.access.redhat.com/ubi8/ubi
COPY --from=build /src/controller /controller
COPY --from=build /src/apiserver /apiserver
EXPOSE 8080
ENTRYPOINT [ "/controller" ]
ENTRYPOINT [ "/apiserver" ]
@@ -6,11 +6,6 @@ COPY ui/package.json .
COPY ui/yarn.lock .
RUN yarn install
ARG VITE_KEYCLOAK_URL=/auth
ARG VITE_KEYCLOAK_REALM=controller
ARG VITE_KEYCLOAK_CLIENT_ID=front-controller
ARG VITE_CONTROLLER_URL=/api
COPY ui .
RUN yarn build
-13
View File
@@ -1,13 +0,0 @@
FROM quay.io/keycloak/keycloak:latest as builder
ENV KC_HEALTH_ENABLED=true
ENV KC_METRICS_ENABLED=true
ENV KC_HTTP_RELATIVE_PATH=/auth
ENV KC_DB=postgres
RUN /opt/keycloak/bin/kc.sh build
FROM quay.io/keycloak/keycloak:latest
COPY --from=builder /opt/keycloak/ /opt/keycloak/
COPY ./hack/controller-realm.json /opt/keycloak/data/import/controller-realm.json
ENTRYPOINT ["/opt/keycloak/bin/kc.sh"]
+13 -28
View File
@@ -1,5 +1,5 @@
.PHONY: all
all: go-lint apex controller
all: go-lint apex apiserver
.PHONY: apex
apex: dist/apex dist/apex-linux-arm dist/apex-linux-amd64 dist/apex-darwin-amd64 dist/apex-darwin-arm64 dist/apex-windows-amd64
@@ -8,7 +8,7 @@ COMMON_DEPS=$(wildcard ./internal/**/*.go) go.sum go.mod
APEX_DEPS=$(COMMON_DEPS) $(wildcard cmd/apex/*.go)
CONTROLLER_DEPS=$(COMMON_DEPS) $(wildcard cmd/apexcontroller/*.go)
TAG=$(shell git rev-parse HEAD)
dist:
mkdir -p $@
@@ -35,23 +35,8 @@ dist/apex-windows-amd64: $(APEX_DEPS) | dist
clean:
rm -rf dist
.PHONY: controller
controller: dist/apexcontroller dist/apexcontroller-linux-amd64 dist/apexcontroller-darwin-amd64 dist/apexcontroller-darwin-arm64
dist/apexcontroller: $(CONTROLLER_DEPS) | dist
CGO_ENABLED=0 go build -o $@ ./cmd/apexcontroller
dist/apexcontroller-linux-amd64: $(CONTROLLER_DEPS) | dist
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o $@ ./cmd/apexcontroller
dist/apexcontroller-darwin-amd64: $(CONTROLLER_DEPS) | dist
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o $@ ./cmd/apexcontroller
dist/apexcontroller-darwin-arm64: $(CONTROLLER_DEPS) | dist
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o $@ ./cmd/apexcontroller
.PHONY: go-lint
go-lint: $(APEX_DEPS) $(CONTROLLER_DEPS)
go-lint: $(APEX_DEPS) $(APISERVER_DEPS)
@if ! which golangci-lint 2>&1 >/dev/null; then \
echo "Please install golangci-lint." ; \
echo "See: https://golangci-lint.run/usage/install/#local-installation" ; \
@@ -61,7 +46,7 @@ go-lint: $(APEX_DEPS) $(CONTROLLER_DEPS)
.PHONY: gen-docs
gen-docs:
swag init -g ./cmd/apexcontroller/main.go -o ./internal/docs
swag init -g ./cmd/apiserver/main.go -o ./internal/docs
.PHONY: test-images
test-images:
@@ -71,19 +56,19 @@ test-images:
OS_IMAGE?="quay.io/apex/test:fedora"
# Runs the CI e2e tests used in github actions
.PHONY: e2e
e2e: dist/apex
docker compose build
./tests/e2e-scripts/init-containers.sh -o $(OS_IMAGE)
.PHONY: e2e
go-e2e: dist/apex test-images
docker compose up --build -d
go test -v --tags=integration ./integration-tests/...
e2e: dist/apex test-images
./hack/run_e2e.sh
.PHONY: recompose
recompose: dist/apex
docker-compose down
docker-compose build
docker-compose up -d
.PHONY: images
images:
docker build -f Containerfile.apiserver -t quay.io/apex/apiserver:$(TAG) .
docker build -f Containerfile.frontend -t quay.io/apex/frontend:$(TAG) .
docker tag quay.io/apex/apiserver:$(TAG) quay.io/apex/apiserver:latest
docker tag quay.io/apex/frontend:$(TAG) quay.io/apex/frontend:latest
-118
View File
@@ -1,118 +0,0 @@
package main
import (
"net/http"
"os"
"os/signal"
"syscall"
"github.com/redhat-et/apex/internal/database"
"github.com/redhat-et/apex/internal/handlers"
"github.com/redhat-et/apex/internal/ipam"
"github.com/redhat-et/apex/internal/routers"
log "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
const (
acLogEnv = "APEX_CONTROLLER_LOGLEVEL"
)
// @title Apex API
// @version 1.0
// @description This is the APEX API Server.
// @contact.name The Apex Authors
// @contact.url https://github.com/redhat-et/apex/issues
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
// @securitydefinitions.oauth2.implicit OAuth2Implicit
// @authorizationurl /auth/realms/controller/protocol/openid-connect/auth
// @scope.admin Grants read and write access to administrative information
// @scope.user Grants read and write access to resources owned by this user
// @BasePath /api
func main() {
// set the log level
env := os.Getenv(acLogEnv)
if env == "debug" {
log.SetLevel(log.DebugLevel)
}
app := &cli.App{
Name: "apex-controller",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "keycloak-address",
Value: "keycloak.apex.svc.cluster.local",
Usage: "address of keycloak service",
Required: false,
},
&cli.StringFlag{
Name: "db-address",
Value: "",
Usage: "address of db",
Required: true,
},
&cli.StringFlag{
Name: "db-password",
Value: "",
Usage: "password of db",
Required: true,
},
&cli.StringFlag{
Name: "ipam-address",
Value: "",
Usage: "address of ipam grpc service",
Required: true,
},
},
Action: func(cCtx *cli.Context) error {
db, err := database.NewDatabase(
cCtx.String("db-address"),
"controller",
cCtx.String("db-password"),
"controller",
5432,
"disable",
)
if err != nil {
log.Fatal(err)
}
ipam := ipam.NewIPAM(cCtx.String("ipam-address"))
api, err := handlers.NewAPI(cCtx.Context, db, ipam)
if err != nil {
log.Fatal(err)
}
router, err := routers.NewRouter(api, cCtx.String("keycloak-address"))
if err != nil {
log.Fatal(err)
}
server := &http.Server{
Addr: "0.0.0.0:8080",
Handler: router,
}
go func() {
if err = server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}()
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGINT)
<-ch
return server.Close()
},
}
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}
+117
View File
@@ -0,0 +1,117 @@
package main
import (
"fmt"
"log"
"os"
"github.com/redhat-et/apex/internal/client"
"github.com/urfave/cli/v2"
)
func main() {
app := &cli.App{
Name: "apexctl",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "debug",
Value: false,
Usage: "enable debug logging",
EnvVars: []string{"APEX_DEBUG"},
},
&cli.StringFlag{
Name: "host",
Value: "http://api.apex.local",
Usage: "api server",
},
&cli.StringFlag{
Name: "username",
Required: true,
Usage: "username",
},
&cli.StringFlag{
Name: "password",
Required: true,
Usage: "password",
},
},
Commands: []*cli.Command{
{
Name: "zone",
Usage: "commands relating to zones",
Subcommands: []*cli.Command{
{
Name: "list",
Usage: "list zones",
Action: func(cCtx *cli.Context) error {
c, err := client.NewClient(cCtx.Context,
cCtx.String("host"),
client.WithPasswordGrant(
cCtx.String("username"),
cCtx.String("password"),
),
)
if err != nil {
log.Fatal(err)
}
res, err := c.ListZones()
if err != nil {
log.Fatal(err)
}
fmt.Printf("%+v", res)
return nil
},
},
{
Name: "create",
Usage: "create a zones",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "name",
Required: true,
},
&cli.StringFlag{
Name: "description",
Required: true,
},
&cli.StringFlag{
Name: "cidr",
Required: true,
},
&cli.BoolFlag{
Name: "hub-zone",
Value: false,
},
},
Action: func(cCtx *cli.Context) error {
c, err := client.NewClient(cCtx.Context,
cCtx.String("host"),
client.WithPasswordGrant(
cCtx.String("username"),
cCtx.String("password"),
),
)
if err != nil {
log.Fatal(err)
}
res, err := c.CreateZone(
cCtx.String("name"),
cCtx.String("description"),
cCtx.String("cidr"),
cCtx.Bool("hub-zone"),
)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%+v", res)
return nil
},
},
},
},
},
}
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}
+154
View File
@@ -0,0 +1,154 @@
package main
import (
"net/http"
"os"
"os/signal"
"syscall"
"github.com/redhat-et/apex/internal/database"
"github.com/redhat-et/apex/internal/handlers"
"github.com/redhat-et/apex/internal/ipam"
"github.com/redhat-et/apex/internal/routers"
log "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
// @title Apex API
// @version 1.0
// @description This is the APEX API Server.
// @contact.name The Apex Authors
// @contact.url https://github.com/redhat-et/apex/issues
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
// @securitydefinitions.oauth2.implicit OAuth2Implicit
// @scope.admin Grants read and write access to administrative information
// @scope.user Grants read and write access to resources owned by this user
// @BasePath /api
func main() {
app := &cli.App{
Name: "apex-controller",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "debug",
Value: false,
Usage: "enable debug logging",
EnvVars: []string{"APEX_DEBUG"},
},
&cli.StringFlag{
Name: "oidc-url",
Value: "http://keycloak:8080/realms/apex",
Usage: "address of oidc provider",
EnvVars: []string{"APEX_OIDC_URL"},
},
&cli.StringFlag{
Name: "oidc-client-id-web",
Value: "apex-web",
Usage: "OIDC client id for web",
EnvVars: []string{"APEX_OIDC_CLIENT_ID_WEB"},
},
&cli.StringFlag{
Name: "oidc-client-id-cli",
Value: "apex-web",
Usage: "OIDC client id for cli",
EnvVars: []string{"APEX_OIDC_CLIENT_ID_CLI"},
},
&cli.StringFlag{
Name: "db-host",
Value: "apiserver-db",
Usage: "db host",
EnvVars: []string{"APEX_DB_HOST"},
},
&cli.StringFlag{
Name: "db-port",
Value: "5432",
Usage: "db port",
EnvVars: []string{"APEX_DB_PORT"},
},
&cli.StringFlag{
Name: "db-user",
Value: "apiserver",
Usage: "db user",
EnvVars: []string{"APEX_DB_USER"},
},
&cli.StringFlag{
Name: "db-password",
Value: "secret",
Usage: "db password",
EnvVars: []string{"APEX_DB_PASSWORD"},
},
&cli.StringFlag{
Name: "db-name",
Value: "apiserver",
Usage: "db name",
EnvVars: []string{"APEX_DB_NAME"},
},
&cli.StringFlag{
Name: "ipam-address",
Value: "ipam:9090",
Usage: "address of ipam grpc service",
EnvVars: []string{"APEX_IPAM_URL"},
},
},
Action: func(cCtx *cli.Context) error {
// set the log level
if cCtx.Bool("debug") {
log.SetLevel(log.DebugLevel)
}
db, err := database.NewDatabase(
cCtx.String("db-host"),
cCtx.String("db-user"),
cCtx.String("db-password"),
cCtx.String("db-name"),
cCtx.String("db-port"),
"disable",
)
if err != nil {
log.Fatal(err)
}
ipam := ipam.NewIPAM(cCtx.String("ipam-address"))
api, err := handlers.NewAPI(cCtx.Context, db, ipam)
if err != nil {
log.Fatal(err)
}
router, err := routers.NewAPIRouter(
cCtx.Context,
api,
cCtx.String("oidc-client-id-web"),
cCtx.String("oidc-client-id-cli"),
cCtx.String("oidc-url"),
)
if err != nil {
log.Fatal(err)
}
server := &http.Server{
Addr: "0.0.0.0:8080",
Handler: router,
}
go func() {
if err = server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}()
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGINT)
<-ch
return server.Close()
},
}
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}
-31
View File
@@ -1,31 +0,0 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: controller
namespace: apex
spec:
rules:
- http:
paths:
- pathType: Prefix
path: "/api/"
backend:
service:
name: controller
port:
number: 8080
- pathType: Prefix
path: "/auth/"
backend:
service:
name: keycloak
port:
number: 8080
- pathType: Prefix
path: "/"
backend:
service:
name: frontend
port:
number: 3000
host: <HOST_DNS>
-410
View File
@@ -1,410 +0,0 @@
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: keycloak-postgres-pv-claim
namespace: apex
labels:
app.kubernetes.io/component: keycloak
app.kubernetes.io/instance: keycloak
app.kubernetes.io/name: keycloak
app.kubernetes.io/part-of: apex
spec:
accessModes:
- ReadWriteOnce # Sets read and write access
resources:
requests:
storage: 5Gi # Sets volume size
---
apiVersion: v1
kind: Service
metadata:
name: keycloak
namespace: apex
labels:
app.kubernetes.io/component: keycloak
app.kubernetes.io/instance: keycloak
app.kubernetes.io/name: keycloak
app.kubernetes.io/part-of: apex
spec:
selector:
app.kubernetes.io/instance: keycloak
app.kubernetes.io/name: keycloak
ports:
- name: http
port: 8080
targetPort: 8080
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: keycloak
namespace: apex
labels:
app.kubernetes.io/component: keycloak
app.kubernetes.io/instance: keycloak
app.kubernetes.io/name: keycloak
app.kubernetes.io/part-of: apex
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/component: keycloak
app.kubernetes.io/instance: keycloak
app.kubernetes.io/name: keycloak
updateStrategy:
type: RollingUpdate
template:
metadata:
labels:
app.kubernetes.io/component: keycloak
app.kubernetes.io/instance: keycloak
app.kubernetes.io/name: keycloak
spec:
containers:
- name: keycloak-db
image: quay.io/apex/postgres:latest
imagePullPolicy: IfNotPresent
env:
- name: POSTGRES_PASSWORD
value: floofykittens
- name: POSTGRES_USER
value: keycloak
resources:
requests:
cpu: 100m
memory: 200Mi
limits:
cpu: 100m
memory: 200Mi
volumeMounts:
- mountPath: /var/lib/postgresql
subPath: data
name: postgresdb
- name: keycloak
image: quay.io/apex/keycloak:latest
imagePullPolicy: Always
args:
- start
- --optimized
- --import-realm
env:
- name: KEYCLOAK_ADMIN
value: admin
- name: KEYCLOAK_ADMIN_PASSWORD
value: floofykittens
- name: KC_DB
value: postgres
- name: KC_DB_URL
value: jdbc:postgresql://localhost/keycloak
- name: KC_DB_USERNAME
value: keycloak
- name: KC_DB_PASSWORD
value: floofykittens
- name: UI_URL
value: UI_URL_VALUE
- name: KC_LOG_LEVEL
value: info
- name: KC_PROXY
value: edge
- name: KC_HOSTNAME_STRICT
value: 'false'
- name: KC_HOSTNAME_STRICT_BACKCHANNEL
value: 'true'
- name: KC_HOSTNAME_STRICT_HTTPS
value: 'false'
- name: KC_HTTP_ENABLED
value: 'true'
- name: PROXY_ADDRESS_FORWARDING
value: "true"
ports:
- name: http
containerPort: 8080
- name: https
containerPort: 8443
resources:
requests:
cpu: 100m
memory: 200Mi
restartPolicy: Always
volumes:
- name: postgresdb
persistentVolumeClaim:
claimName: keycloak-postgres-pv-claim
---
apiVersion: v1
kind: Service
metadata:
name: ipam
namespace: apex
labels:
app.kubernetes.io/component: ipam
app.kubernetes.io/instance: ipam
app.kubernetes.io/name: ipam
app.kubernetes.io/part-of: apex
spec:
selector:
app.kubernetes.io/component: ipam
app.kubernetes.io/instance: ipam
app.kubernetes.io/name: ipam
ports:
- port: 9090
targetPort: 9090
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: ipam-postgres-pv-claim
namespace: apex
labels:
app.kubernetes.io/component: ipam
app.kubernetes.io/instance: ipam
app.kubernetes.io/name: ipam
app.kubernetes.io/part-of: apex
spec:
accessModes:
- ReadWriteOnce # Sets read and write access
resources:
requests:
storage: 5Gi # Sets volume size
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: ipam
namespace: apex
labels:
app.kubernetes.io/component: ipam
app.kubernetes.io/instance: ipam
app.kubernetes.io/name: ipam
app.kubernetes.io/part-of: apex
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/component: ipam
app.kubernetes.io/instance: ipam
app.kubernetes.io/name: ipam
updateStrategy:
type: RollingUpdate
template:
metadata:
labels:
app.kubernetes.io/component: ipam
app.kubernetes.io/instance: ipam
app.kubernetes.io/name: ipam
spec:
containers:
- name: ipam-db
image: quay.io/apex/postgres:latest
imagePullPolicy: IfNotPresent
env:
- name: POSTGRES_PASSWORD
value: floofykittens
- name: POSTGRES_USER
value: ipam
resources:
requests:
cpu: 100m
memory: 200Mi
limits:
cpu: 100m
memory: 200Mi
volumeMounts:
- mountPath: /var/lib/postgresql
subPath: data
name: postgresdb
- name: ipam
image: ghcr.io/metal-stack/go-ipam
imagePullPolicy: Always
args:
- --grpc-server-endpoint=0.0.0.0:9090
- postgres
- --host=localhost
- --dbname=ipam
- --user=ipam
- --password=floofykittens
resources:
requests:
cpu: 100m
memory: 200Mi
limits:
cpu: 100m
memory: 200Mi
restartPolicy: Always
volumes:
- name: postgresdb
persistentVolumeClaim:
claimName: ipam-postgres-pv-claim
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: controller-postgres-pv-claim
namespace: apex
labels:
app.kubernetes.io/component: controller
app.kubernetes.io/instance: controller
app.kubernetes.io/name: controller
app.kubernetes.io/part-of: apex
spec:
accessModes:
- ReadWriteOnce # Sets read and write access
resources:
requests:
storage: 5Gi # Sets volume size
---
apiVersion: v1
kind: Service
metadata:
name: controller
namespace: apex
labels:
app.kubernetes.io/component: controller
app.kubernetes.io/instance: controller
app.kubernetes.io/name: controller
app.kubernetes.io/part-of: apex
spec:
selector:
app.kubernetes.io/component: controller
app.kubernetes.io/instance: controller
app.kubernetes.io/name: controller
ports:
- port: 8080
targetPort: 8080
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: controller
namespace: apex
labels:
app.kubernetes.io/component: controller
app.kubernetes.io/instance: controller
app.kubernetes.io/name: controller
app.kubernetes.io/part-of: apex
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/component: controller
app.kubernetes.io/instance: controller
app.kubernetes.io/name: controller
updateStrategy:
type: RollingUpdate
template:
metadata:
labels:
app.kubernetes.io/component: controller
app.kubernetes.io/instance: controller
app.kubernetes.io/name: controller
spec:
containers:
- name: controller-db
image: quay.io/apex/postgres:latest
imagePullPolicy: IfNotPresent
env:
- name: POSTGRES_PASSWORD
value: floofykittens
- name: POSTGRES_USER
value: controller
resources:
requests:
cpu: 100m
memory: 200Mi
limits:
cpu: 100m
memory: 200Mi
volumeMounts:
- mountPath: /var/lib/postgresql
subPath: data
name: postgresdb
- name: controller
image: quay.io/apex/controller:latest
imagePullPolicy: Always
args:
- --db-address=localhost
- --db-password=floofykittens
- --ipam-address=http://ipam.apex.svc.cluster.local:9090
env:
- name: APEX_CONTROLLER_LOGLEVEL
value: debug
resources:
requests:
cpu: 100m
memory: 200Mi
limits:
cpu: 100m
memory: 200Mi
restartPolicy: Always
volumes:
- name: postgresdb
persistentVolumeClaim:
claimName: controller-postgres-pv-claim
---
apiVersion: v1
kind: Service
metadata:
name: frontend
namespace: apex
labels:
app.kubernetes.io/component: frontend
app.kubernetes.io/instance: frontend
app.kubernetes.io/name: frontend
app.kubernetes.io/part-of: apex
spec:
selector:
app.kubernetes.io/component: frontend
app.kubernetes.io/instance: frontend
app.kubernetes.io/name: frontend
ports:
- port: 3000
targetPort: 3000
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
namespace: apex
labels:
app.kubernetes.io/component: frontend
app.kubernetes.io/instance: frontend
app.kubernetes.io/name: frontend
app.kubernetes.io/part-of: apex
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/component: frontend
app.kubernetes.io/instance: frontend
app.kubernetes.io/name: frontend
strategy:
type: Recreate
template:
metadata:
labels:
app.kubernetes.io/component: frontend
app.kubernetes.io/instance: frontend
app.kubernetes.io/name: frontend
spec:
containers:
- name: frontend
image: quay.io/apex/controller-ui:latest
imagePullPolicy: Always
env:
- name: VITE_KEYCLOAK_URL
value: /auth
- name: VITE_KEYCLOAK_REALM
value: controller
- name: VITE_KEYCLOAK_CLIENT_ID
value: front-controller
- name: VITE_CONTROLLER_URL
value: /api
resources:
requests:
cpu: 100m
memory: 200Mi
limits:
cpu: 100m
memory: 200Mi
restartPolicy: Always
---
+40
View File
@@ -0,0 +1,40 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: apiproxy
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/component: apiproxy
app.kubernetes.io/instance: apiproxy
app.kubernetes.io/name: apiproxy
strategy:
type: Recreate
template:
metadata:
labels:
app.kubernetes.io/component: apiproxy
app.kubernetes.io/instance: apiproxy
app.kubernetes.io/name: apiproxy
spec:
containers:
- name: caddy
image: docker.io/library/caddy:2.6-alpine
imagePullPolicy: IfNotPresent
resources:
requests:
cpu: 100m
memory: 200Mi
limits:
cpu: 100m
memory: 200Mi
volumeMounts:
- name: config
mountPath: /etc/caddy/Caddyfile
subPath: Caddyfile
restartPolicy: Always
volumes:
- name: config
configMap:
name: caddyfile
+16
View File
@@ -0,0 +1,16 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: apiproxy
spec:
rules:
- host: <API_HOST>
http:
paths:
- pathType: Prefix
path: "/"
backend:
service:
name: apiproxy
port:
number: 8080
+10
View File
@@ -0,0 +1,10 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- ingress.yaml
- service.yaml
commonLabels:
app.kubernetes.io/component: apiproxy
app.kubernetes.io/instance: apiproxy
app.kubernetes.io/name: apiproxy
+12
View File
@@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: apiproxy
spec:
selector:
app.kubernetes.io/component: apiproxy
app.kubernetes.io/instance: apiproxy
app.kubernetes.io/name: apiproxy
ports:
- port: 8080
targetPort: 8080
+10
View File
@@ -0,0 +1,10 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- persistentvolumeclaim.yaml
- service.yaml
- statefulset.yaml
commonLabels:
app.kubernetes.io/component: apiserver
app.kubernetes.io/instance: apiserver
app.kubernetes.io/name: apiserver
@@ -0,0 +1,10 @@
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: apiserver-postgres-pv-claim
spec:
accessModes:
- ReadWriteOnce # Sets read and write access
resources:
requests:
storage: 5Gi # Sets volume size
+12
View File
@@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: apiserver
spec:
selector:
app.kubernetes.io/component: apiserver
app.kubernetes.io/instance: apiserver
app.kubernetes.io/name: apiserver
ports:
- port: 8080
targetPort: 8080
+85
View File
@@ -0,0 +1,85 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: apiserver
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/component: apiserver
app.kubernetes.io/instance: apiserver
app.kubernetes.io/name: apiserver
serviceName: "apiserver"
updateStrategy:
type: RollingUpdate
template:
metadata:
labels:
app.kubernetes.io/component: apiserver
app.kubernetes.io/instance: apiserver
app.kubernetes.io/name: apiserver
spec:
containers:
- name: apiserver-db
image: docker.io/library/postgres:15.1-alpine
imagePullPolicy: IfNotPresent
env:
- name: POSTGRES_PASSWORD
value: floofykittens
- name: POSTGRES_USER
value: apiserver
resources:
requests:
cpu: 100m
memory: 200Mi
limits:
cpu: 100m
memory: 200Mi
volumeMounts:
- mountPath: /var/lib/postgresql
subPath: data
name: postgresdb
- name: apiserver
image: quay.io/apex/apiserver:latest
imagePullPolicy: IfNotPresent
env:
- name: APEX_APISERVER_LOGLEVEL
value: debug
- name: APEX_DEBUG
value: "1"
- name: APEX_DB_HOST
value: localhost
- name: APEX_DB_NAME
value: apiserver
- name: APEX_DB_USER
value: apiserver
- name: APEX_DB_PASSWORD
value: floofykittens
- name: APEX_IPAM_URL
value: http://ipam:9090
- name: APEX_OIDC_URL
value: <AUTH_HOST>
- name: APEX_OIDC_CLIENT_ID_WEB
valueFrom:
secretKeyRef:
name: dex-secrets
key: web-client-id
optional: false
- name: APEX_OIDC_CLIENT_ID_CLI
valueFrom:
secretKeyRef:
name: dex-secrets
key: cli-client-id
optional: false
resources:
requests:
cpu: 100m
memory: 200Mi
limits:
cpu: 100m
memory: 200Mi
restartPolicy: Always
volumes:
- name: postgresdb
persistentVolumeClaim:
claimName: apiserver-postgres-pv-claim
+47
View File
@@ -0,0 +1,47 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend-cli
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/component: backend-cli
app.kubernetes.io/instance: backend-cli
app.kubernetes.io/name: backend-cli
strategy:
type: Recreate
template:
metadata:
labels:
app.kubernetes.io/component: backend-cli
app.kubernetes.io/instance: backend-cli
app.kubernetes.io/name: backend-cli
spec:
containers:
- name: backend-cli
image: quay.io/go-oidc-agent/go-oidc-agent:b98e1621e73404c5de21f47c7c5e6fab091e5d17
imagePullPolicy: IfNotPresent
env:
- name: DEBUG
value: "1"
- name: OIDC_PROVIDER
value: <AUTH_HOST>
- name: OIDC_CLIENT_ID
valueFrom:
secretKeyRef:
name: dex-secrets
key: cli-client-id
optional: false
- name: OIDC_FLOW
value: device
- name: BACKEND
value: http://apiserver:8080
resources:
requests:
cpu: 100m
memory: 200Mi
limits:
cpu: 100m
memory: 200Mi
restartPolicy: Always
@@ -0,0 +1,9 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
commonLabels:
app.kubernetes.io/component: backend-cli
app.kubernetes.io/instance: backend-cli
app.kubernetes.io/name: backend-cli
+16
View File
@@ -0,0 +1,16 @@
apiVersion: v1
kind: Service
metadata:
name: backend-cli
labels:
app.kubernetes.io/component: backend-cli
app.kubernetes.io/instance: backend-cli
app.kubernetes.io/name: backend-cli
spec:
selector:
app.kubernetes.io/component: backend-cli
app.kubernetes.io/instance: backend-cli
app.kubernetes.io/name: backend-cli
ports:
- port: 8080
targetPort: 8080
+57
View File
@@ -0,0 +1,57 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend-web
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/component: backend-web
app.kubernetes.io/instance: backend-web
app.kubernetes.io/name: backend-web
strategy:
type: Recreate
template:
metadata:
labels:
app.kubernetes.io/component: backend-web
app.kubernetes.io/instance: backend-web
app.kubernetes.io/name: backend-web
spec:
containers:
- name: backend-web
image: quay.io/go-oidc-agent/go-oidc-agent:b98e1621e73404c5de21f47c7c5e6fab091e5d17
imagePullPolicy: IfNotPresent
env:
- name: DEBUG
value: "1"
- name: OIDC_PROVIDER
value: <AUTH_HOST>
- name: OIDC_CLIENT_ID
valueFrom:
secretKeyRef:
name: dex-secrets
key: web-client-id
optional: false
- name: OIDC_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: dex-secrets
key: web-client-secret
optional: false
- name: REDIRECT_URL
value: <HOST>/#/login
- name: ORIGINS
value: <HOST>
- name: DOMAIN
value: <API_HOST>
- name: BACKEND
value: http://apiserver:8080
resources:
requests:
cpu: 100m
memory: 200Mi
limits:
cpu: 100m
memory: 200Mi
restartPolicy: Always
@@ -0,0 +1,9 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
commonLabels:
app.kubernetes.io/component: backend-web
app.kubernetes.io/instance: backend-web
app.kubernetes.io/name: backend-web
+16
View File
@@ -0,0 +1,16 @@
apiVersion: v1
kind: Service
metadata:
name: backend-web
labels:
app.kubernetes.io/component: backend-web
app.kubernetes.io/instance: backend-web
app.kubernetes.io/name: backend-web
spec:
selector:
app.kubernetes.io/component: backend-web
app.kubernetes.io/instance: backend-web
app.kubernetes.io/name: backend-web
ports:
- port: 8080
targetPort: 8080
+10
View File
@@ -0,0 +1,10 @@
:8080 {
@web {
header User-Agent *Mozilla*
}
@notweb {
not header User-Agent *Mozilla*
}
reverse_proxy @web backend-web:8080
reverse_proxy @notweb backend-cli:8080
}
+59
View File
@@ -0,0 +1,59 @@
issuer: <AUTH_HOST>
storage:
type: memory
web:
http: 0.0.0.0:8080
expiry:
deviceRequests: "5m"
signingKeys: "6h"
idTokens: "24h"
refreshTokens:
reuseInterval: "3s"
validIfNotUsedFor: "2160h" # 90 days
absoluteLifetime: "3960h" # 165 days
oauth2:
responseTypes: ["code", "token", "id_token"]
staticClients:
- idEnv: WEB_CLIENT_ID
redirectURIs:
- "<HOST>/#/login"
name: "Apex Web Frontend"
secretEnv: WEB_CLIENT_SECRET
- idEnv: CLI_CLIENT_ID
redirectURIs:
- "/device/callback"
name: "Apex CLI Frontend"
public: true
enablePasswordDB: true
staticPasswords:
- email: "admin@apex.local"
hash: "$2y$10$BdXJbB0M2dsCzZQSYZBkT.GNaClwAuqG2Tv/qJUW8S4cy6AIIc.5a"
username: "admin"
userID: "10a31cfa-4181-4815-9aa2-f74e122412ee"
- email: "kitteh1@apex.local"
hash: "$2y$10$BdXJbB0M2dsCzZQSYZBkT.GNaClwAuqG2Tv/qJUW8S4cy6AIIc.5a"
username: "kitteh1"
userID: "189d32dc-0d64-42c1-b34d-ae2daea0cc22"
- email: "kitteh2@apex.local"
hash: "$2y$10$BdXJbB0M2dsCzZQSYZBkT.GNaClwAuqG2Tv/qJUW8S4cy6AIIc.5a"
username: "kitteh2"
userID: "05e5fdff-ed73-48fd-ad10-b9d457f1f1bb"
- email: "kitteh3@apex.local"
hash: "$2y$10$BdXJbB0M2dsCzZQSYZBkT.GNaClwAuqG2Tv/qJUW8S4cy6AIIc.5a"
username: "kitteh3"
userID: "32b869d6-f633-41be-ac72-40efe86d55f7"
- email: "kitteh4@apex.local"
hash: "$2y$10$BdXJbB0M2dsCzZQSYZBkT.GNaClwAuqG2Tv/qJUW8S4cy6AIIc.5a"
username: "kitteh4"
userID: "885c1d57-8ff9-406c-a15a-388b77bf7409"
- email: "kitteh5@apex.local"
hash: "$2y$10$BdXJbB0M2dsCzZQSYZBkT.GNaClwAuqG2Tv/qJUW8S4cy6AIIc.5a"
username: "kitteh5"
userID: "306d0921-44bd-45e8-a7de-f90dfb32abf7"
+11
View File
@@ -0,0 +1,11 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
generatorOptions:
disableNameSuffixHash: true
configMapGenerator:
- name: dex
files:
- files/config.yaml
- name: caddyfile
files:
- files/Caddyfile
+54
View File
@@ -0,0 +1,54 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: dex
labels:
app.kubernetes.io/component: dex
app.kubernetes.io/instance: dex
app.kubernetes.io/name: dex
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/component: dex
app.kubernetes.io/instance: dex
app.kubernetes.io/name: dex
strategy:
type: Recreate
template:
metadata:
labels:
app.kubernetes.io/component: dex
app.kubernetes.io/instance: dex
app.kubernetes.io/name: dex
spec:
containers:
- name: dex
image: ghcr.io/dexidp/dex:v2.35.2
imagePullPolicy: IfNotPresent
resources:
requests:
cpu: 100m
memory: 200Mi
limits:
cpu: 100m
memory: 200Mi
env:
- name: WEB_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: dex-secrets
key: web-client-secret
optional: false
args:
- dex
- serve
- /etc/dex/config.yaml
volumeMounts:
- name: config
mountPath: /etc/dex
restartPolicy: Always
volumes:
- name: config
configMap:
name: dex
+16
View File
@@ -0,0 +1,16 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: dex
spec:
rules:
- host: <AUTH_HOST>
http:
paths:
- pathType: Prefix
path: "/"
backend:
service:
name: dex
port:
number: 80
+18
View File
@@ -0,0 +1,18 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
generatorOptions:
disableNameSuffixHash: true
resources:
- deployment.yaml
- ingress.yaml
- service.yaml
secretGenerator:
- name: dex-secrets
literals:
- web-client-id=apex-web
- web-client-secret=dhEN2dsqyUg5qmaDAdqi4CmH
- cli-client-id=apex-cli
commonLabels:
app.kubernetes.io/component: dex
app.kubernetes.io/instance: dex
app.kubernetes.io/name: dex
+12
View File
@@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: dex
spec:
selector:
app.kubernetes.io/instance: dex
app.kubernetes.io/name: dex
ports:
- name: http
port: 80
targetPort: 8080
+32
View File
@@ -0,0 +1,32 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/component: frontend
app.kubernetes.io/instance: frontend
app.kubernetes.io/name: frontend
strategy:
type: Recreate
template:
metadata:
labels:
app.kubernetes.io/component: frontend
app.kubernetes.io/instance: frontend
app.kubernetes.io/name: frontend
spec:
containers:
- name: frontend
image: quay.io/apex/frontend:latest
imagePullPolicy: IfNotPresent
resources:
requests:
cpu: 100m
memory: 200Mi
limits:
cpu: 100m
memory: 200Mi
restartPolicy: Always
+16
View File
@@ -0,0 +1,16 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: frontend
spec:
rules:
- host: <HOST>
http:
paths:
- pathType: Prefix
path: "/"
backend:
service:
name: frontend
port:
number: 3000
+10
View File
@@ -0,0 +1,10 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- ingress.yaml
- service.yaml
commonLabels:
app.kubernetes.io/component: frontend
app.kubernetes.io/instance: frontend
app.kubernetes.io/name: frontend
+12
View File
@@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: frontend
spec:
selector:
app.kubernetes.io/component: frontend
app.kubernetes.io/instance: frontend
app.kubernetes.io/name: frontend
ports:
- port: 3000
targetPort: 3000
+10
View File
@@ -0,0 +1,10 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- persistentvolumeclaim.yaml
- service.yaml
- statefulset.yaml
commonLabels:
app.kubernetes.io/component: ipam
app.kubernetes.io/instance: ipam
app.kubernetes.io/name: ipam
@@ -0,0 +1,10 @@
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: ipam-postgres-pv-claim
spec:
accessModes:
- ReadWriteOnce # Sets read and write access
resources:
requests:
storage: 5Gi # Sets volume size
+12
View File
@@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: ipam
spec:
selector:
app.kubernetes.io/component: ipam
app.kubernetes.io/instance: ipam
app.kubernetes.io/name: ipam
ports:
- port: 9090
targetPort: 9090
+63
View File
@@ -0,0 +1,63 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: ipam
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/component: ipam
app.kubernetes.io/instance: ipam
app.kubernetes.io/name: ipam
serviceName: "ipam"
updateStrategy:
type: RollingUpdate
template:
metadata:
labels:
app.kubernetes.io/component: ipam
app.kubernetes.io/instance: ipam
app.kubernetes.io/name: ipam
spec:
containers:
- name: ipam-db
image: docker.io/library/postgres:15.1-alpine
imagePullPolicy: IfNotPresent
env:
- name: POSTGRES_PASSWORD
value: floofykittens
- name: POSTGRES_USER
value: ipam
resources:
requests:
cpu: 100m
memory: 200Mi
limits:
cpu: 100m
memory: 200Mi
volumeMounts:
- mountPath: /var/lib/postgresql
subPath: data
name: postgresdb
- name: ipam
image: ghcr.io/metal-stack/go-ipam:v1.11.3
imagePullPolicy: IfNotPresent
args:
- --grpc-server-endpoint=0.0.0.0:9090
- postgres
- --host=localhost
- --dbname=ipam
- --user=ipam
- --password=floofykittens
resources:
requests:
cpu: 100m
memory: 200Mi
limits:
cpu: 100m
memory: 200Mi
restartPolicy: Always
volumes:
- name: postgresdb
persistentVolumeClaim:
claimName: ipam-postgres-pv-claim
+15
View File
@@ -0,0 +1,15 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: apex
resources:
- apiproxy
- apiserver
- backend-cli
- backend-web
# configmaps must be implemented in overlays
# - configmaps
- dex
- frontend
- ipam
commonLabels:
app.kubernetes.io/part-of: apex
+16
View File
@@ -0,0 +1,16 @@
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: apex-dev
nodes:
- role: control-plane
image: kindest/node:v1.24.7@sha256:577c630ce8e509131eab1aea12c022190978dd2f745aac5eb1fe65c0807eb315
kubeadmConfigPatches:
- |
kind: InitConfiguration
nodeRegistration:
kubeletExtraArgs:
node-labels: "ingress-ready=true"
extraPortMappings:
- containerPort: 80
hostPort: 80
protocol: TCP
@@ -0,0 +1,12 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend-web
spec:
template:
spec:
containers:
- name: backend-web
env:
- name: DOMAIN
value: api.apex.local
@@ -0,0 +1,3 @@
- op: replace
path: /spec/rules/0/host
value: api.apex.local
@@ -0,0 +1,38 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: apiserver
spec:
template:
spec:
containers:
- name: apiserver
env:
- name: APEX_OIDC_URL
value: http://auth.apex.local
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend-cli
spec:
template:
spec:
containers:
- name: backend-cli
env:
- name: OIDC_PROVIDER
value: http://auth.apex.local
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend-web
spec:
template:
spec:
containers:
- name: backend-web
env:
- name: OIDC_PROVIDER
value: http://auth.apex.local
@@ -0,0 +1,3 @@
- op: replace
path: /spec/rules/0/host
value: auth.apex.local
@@ -0,0 +1,10 @@
:8080 {
@web {
header User-Agent *Mozilla*
}
@notweb {
not header User-Agent *Mozilla*
}
reverse_proxy @web backend-web:8080
reverse_proxy @notweb backend-cli:8080
}
@@ -0,0 +1,60 @@
issuer: http://auth.apex.local
storage:
type: memory
web:
http: 0.0.0.0:8080
expiry:
deviceRequests: "5m"
signingKeys: "6h"
idTokens: "24h"
refreshTokens:
reuseInterval: "3s"
validIfNotUsedFor: "2160h" # 90 days
absoluteLifetime: "3960h" # 165 days
oauth2:
responseTypes: ["code", "token", "id_token"]
passwordConnector: local
staticClients:
- id: apex-web
redirectURIs:
- 'http://apex.local/#/login'
name: 'Apex Web Frontend'
secretEnv: WEB_CLIENT_SECRET
- id: apex-cli
redirectURIs:
- '/device/callback'
name: 'Apex CLI Frontend'
public: true
enablePasswordDB: true
staticPasswords:
- email: "admin@apex.local"
hash: "$2y$10$BdXJbB0M2dsCzZQSYZBkT.GNaClwAuqG2Tv/qJUW8S4cy6AIIc.5a"
username: "admin"
userID: "10a31cfa-4181-4815-9aa2-f74e122412ee"
- email: "kitteh1@apex.local"
hash: "$2y$10$BdXJbB0M2dsCzZQSYZBkT.GNaClwAuqG2Tv/qJUW8S4cy6AIIc.5a"
username: "kitteh1"
userID: "189d32dc-0d64-42c1-b34d-ae2daea0cc22"
- email: "kitteh2@apex.local"
hash: "$2y$10$BdXJbB0M2dsCzZQSYZBkT.GNaClwAuqG2Tv/qJUW8S4cy6AIIc.5a"
username: "kitteh2"
userID: "05e5fdff-ed73-48fd-ad10-b9d457f1f1bb"
- email: "kitteh3@apex.local"
hash: "$2y$10$BdXJbB0M2dsCzZQSYZBkT.GNaClwAuqG2Tv/qJUW8S4cy6AIIc.5a"
username: "kitteh3"
userID: "32b869d6-f633-41be-ac72-40efe86d55f7"
- email: "kitteh4@apex.local"
hash: "$2y$10$BdXJbB0M2dsCzZQSYZBkT.GNaClwAuqG2Tv/qJUW8S4cy6AIIc.5a"
username: "kitteh4"
userID: "885c1d57-8ff9-406c-a15a-388b77bf7409"
- email: "kitteh5@apex.local"
hash: "$2y$10$BdXJbB0M2dsCzZQSYZBkT.GNaClwAuqG2Tv/qJUW8S4cy6AIIc.5a"
username: "kitteh5"
userID: "306d0921-44bd-45e8-a7de-f90dfb32abf7"
@@ -0,0 +1,12 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: apex
generatorOptions:
disableNameSuffixHash: true
configMapGenerator:
- name: dex
files:
- files/config.yaml
- name: caddyfile
files:
- files/Caddyfile
+14
View File
@@ -0,0 +1,14 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend-web
spec:
template:
spec:
containers:
- name: backend-web
env:
- name: REDIRECT_URL
value: http://apex.local/#/login
- name: ORIGINS
value: http://apex.local
@@ -0,0 +1,3 @@
- op: replace
path: /spec/rules/0/host
value: apex.local
+22
View File
@@ -0,0 +1,22 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
- configmaps
patchesStrategicMerge:
- api_host_env_patch.yaml
- auth_host_env_patch.yaml
- host_env_patch.yaml
patchesJson6902:
- target:
kind: Ingress
name: apiproxy
path: api_host_ingress_patch.yaml
- target:
kind: Ingress
name: dex
path: auth_host_ingress_patch.yaml
- target:
kind: Ingress
name: frontend
path: host_ingress_patch.yaml
-130
View File
@@ -1,130 +0,0 @@
version: "3.7"
services:
controller-db:
image: docker.io/library/postgres
environment:
- POSTGRES_USER=${CONTROLLER_DB_USER}
- POSTGRES_PASSWORD=${CONTROLLER_DB_PASSWORD}
ipam-db:
image: docker.io/library/postgres
environment:
- POSTGRES_USER=${IPAM_DB_USER}
- POSTGRES_PASSWORD=${IPAM_DB_PASSWORD}
ipam:
image: ghcr.io/metal-stack/go-ipam
depends_on:
- ipam-db
deploy:
restart_policy:
condition: on-failure
command:
- --grpc-server-endpoint=0.0.0.0:9090
- postgres
- --host=${IPAM_DB_HOST}
- --dbname=ipam
- --user=${IPAM_DB_USER}
- --password=${IPAM_DB_PASSWORD}
controller:
image: quay.io/apex/controller:latest
depends_on:
- controller-db
- ipam
- keycloak
build:
context: .
dockerfile: ./Containerfile.controller
deploy:
restart_policy:
condition: on-failure
command:
- "--keycloak-address=keycloak"
- "--db-address=${CONTROLLER_DB_HOST}"
- "--db-password=${CONTROLLER_DB_PASSWORD}"
- "--ipam-address=${IPAM_ADDRESS}"
environment:
APEX_CONTROLLER_LOGLEVEL: debug
labels:
- "traefik.enable=true"
- "traefik.http.routers.controller.rule=PathPrefix(`/api`)"
- "traefik.http.routers.controller.entrypoints=web"
- "traefik.http.services.my-controller.loadbalancer.server.port=8080"
ui:
image: quay.io/apex/controller-ui:latest
depends_on:
- controller
- keycloak
build:
context: .
dockerfile: ./Containerfile.ui
args:
- VITE_KEYCLOAK_URL=${VITE_KEYCLOAK_URL}
- VITE_KEYCLOAK_REALM=${VITE_KEYCLOAK_REALM}
- VITE_KEYCLOAK_CLIENT_ID=${VITE_KEYCLOAK_CLIENT_ID}
- VITE_CONTROLLER_URL=${VITE_CONTROLLER_URL}
labels:
- "traefik.enable=true"
- "traefik.http.routers.ui.rule=PathPrefix(`/`) && !PathPrefix(`/api`) && !PathPrefix(`/auth`)"
- "traefik.http.routers.ui.entrypoints=web"
- "traefik.http.services.ui.loadbalancer.server.port=3000"
keycloak-db:
image: docker.io/library/postgres
environment:
- POSTGRES_USER=${KEYCLOAK_DB_USER}
- POSTGRES_PASSWORD=${KEYCLOAK_DB_PASSWORD}
keycloak:
image: quay.io/apex/keycloak:latest
build:
context: .
dockerfile: ./Containerfile.keycloak
depends_on:
- keycloak-db
deploy:
restart_policy:
condition: on-failure
environment:
- KEYCLOAK_ADMIN=${KEYCLOAK_ADMIN_USER}
- KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN_PASSWORD}
- KC_DB=postgres
- KC_DB_URL=jdbc:postgresql://${KEYCLOAK_DB_HOST}/keycloak
- KC_DB_USERNAME=${KEYCLOAK_DB_USER}
- KC_DB_PASSWORD=${KEYCLOAK_DB_PASSWORD}
- UI_URL=${UI_URL}
- KC_PROXY=passthrough
- KC_HOSTNAME_STRICT=false
- KC_HOSTNAME_STRICT_BACKCHANNEL=true
- KC_HOSTNAME_STRICT_HTTPS=false
- KC_HTTP_ENABLED=true
command:
- start
- --optimized
- --import-realm
labels:
- "traefik.enable=true"
- "traefik.http.routers.keycloak.rule=PathPrefix(`/auth`)"
- "traefik.http.routers.keycloak.entrypoints=web"
- "traefik.http.services.keycloak.loadbalancer.server.port=8080"
proxy:
depends_on:
- controller
- keycloak
- ui
image: traefik:v2.9
command:
- --api.insecure=true
- --providers.docker
- --providers.docker.exposedbydefault=false
- --entrypoints.web.address=:8080
- --entrypoints.traefik.address=:8888
ports:
- "8080:8080"
- "8888:8888"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
+27 -36
View File
@@ -3,7 +3,6 @@
- [Documentation](#documentation)
- [Concepts](#concepts)
- [Deploying the Apex Controller](#deploying-the-apex-controller)
- [Using docker-compose](#using-docker-compose)
- [Run on Kubernetes](#run-on-kubernetes)
- [The Apex Agent](#the-apex-agent)
- [Installing the Agent](#installing-the-agent)
@@ -33,43 +32,35 @@
## Deploying the Apex Controller
### Using docker-compose
> **_NOTE:_** These instructions do not work with `podman-compose`. Instead of
> fixing `podman` compatibility, we will be moving exclusively to kubernetes as
> a local development platform using `kind` or `minikube`. Once those
> instructions are ready, `docker-compose` support will be removed.
For development and testing purposes, the quickest way to run the controller stack is by using `docker-compose`.
First, to build all required container images:
```sh
docker-compose build
```
To bring up the stack:
```sh
docker-compose up -d
```
To verify that everything has come up successfully:
```sh
$ curl http://localhost:8080/api/health
{"message":"ok"}
```
To tear everything back down:
```sh
docker-compose down
```
### Run on Kubernetes
Coming soon ...
#### Add required DNS entries
The development Apex stack requires 3 hostnames to be reachable:
- `auth.apex.local` - for the authentication service
- `api.apex.local` - for the backend apis
- `apex.local` - for the frontend
To add these on your own machine:
```console
echo "127.0.0.1 auth.apex.local api.apex.local apex.local" | sudo tee -a /etc/hosts
```
#### Deploy using KIND
You should first ensure that you have `kind` and `kubectl` installed.
If not, you can follow the instructions in the [KIND Quick Start](https://kind.sigs.k8s.io/docs/user/quick-start/).
```console
./hack/kind/kind.sh
```
This will install:
- `apex-dev` kind cluster
- `ingress-nginx` ingress controller
- a rewrite rule in coredns to allow `auth.apex.local` to resolve inside the k8s cluster
- the `apex` stack
## The Apex Agent
+37
View File
@@ -0,0 +1,37 @@
Authentication
==============
The Apex Stack uses OpenID Connect for authentication.
It allows whomever deploys the stack to chose any OpenID connect provider they wish in order to provide user authentication.
It also enables Apex to focus on its core, and to defer authentication to another service.
## Web Frontend Authentication
The web frontend authentication follows the Backend For Frontend (BFF) architecure. The apex stack has 2 components:
- go-oidc-agent
- apiserver
The [go-oidc-agent](https://github.com/redhat-et/go-oidc-agent) service is a dedicated backend for the web frontend that provides authentication services, and proxies API requests to the apiserver.
This not only helps simplify deployment, but also reduces risk of compromise of access tokens/refresh token compromise by keeping them out of the browser.
For more information on this flow see:
- https://github.com/redhat-et/go-oidc-agent
- https://auth0.com/blog/backend-for-frontend-pattern-with-auth0-and-dotnet/
- https://curity.io/resources/learn/the-token-handler-pattern/
The apiserver expects to see JWTs in the `Authorization` header, and will validate the JWT signature against the OpenID Providers JWKs.
## CLI Frontend Authentication
The cli frontend authentication follows the Backend For Frontend (BFF) architecure also. The apex stack has 2 components:
- go-oidc-agent
- apiserver
When `go-oidc-agent` is used in Device Flow mode, it simply sends:
1. The Device Authorization Endpoint of the Authenication Server
1. The Client ID to use
The Apex CLI is then responsible for acquiring and storing tokens.
In this case, the `go-oidc-agent` is there to simplfy deployment only - so no endpoints or client-ids need to be included in the client binary, or injected through config file or flags.
+22 -15
View File
@@ -3,25 +3,25 @@ module github.com/redhat-et/apex
go 1.19
require (
github.com/MicahParks/keyfunc v1.5.1
github.com/bufbuild/connect-go v1.0.0
github.com/gin-contrib/cors v1.4.0
github.com/coreos/go-oidc v2.2.1+incompatible
github.com/gin-gonic/gin v1.8.1
github.com/golang-jwt/jwt/v4 v4.4.2
github.com/google/uuid v1.3.0
github.com/metal-stack/go-ipam v1.11.2
github.com/ory/dockertest/v3 v3.9.1
github.com/pion/stun v0.3.5
github.com/redhat-et/go-oidc-agent v0.0.2-0.20221130160039-b98e1621e734
github.com/sirupsen/logrus v1.9.0
github.com/stretchr/testify v1.8.1
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a
github.com/swaggo/gin-swagger v1.5.3
github.com/swaggo/swag v1.8.7
github.com/urfave/cli/v2 v2.19.2
github.com/urfave/cli/v2 v2.23.5
github.com/vishvananda/netlink v1.1.0
go.uber.org/zap v1.23.0
golang.org/x/net v0.0.0-20220909164309-bea034e7d591
golang.org/x/sys v0.1.0
golang.org/x/net v0.2.0
golang.org/x/oauth2 v0.2.0
golang.org/x/sys v0.2.0
gopkg.in/ini.v1 v1.67.0
gorm.io/driver/postgres v1.4.4
gorm.io/driver/sqlite v1.4.3
@@ -37,22 +37,29 @@ require (
github.com/docker/docker v20.10.18+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/gin-contrib/cors v1.4.0 // indirect
github.com/gin-contrib/sessions v0.0.5 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect
github.com/gorilla/sessions v1.2.1 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/mitchellh/mapstructure v1.4.1 // indirect
github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect
github.com/opencontainers/runc v1.1.4 // indirect
github.com/pquerna/cachecontrol v0.1.0 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
)
require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/avast/retry-go/v4 v4.1.0 // indirect
github.com/cenkalti/backoff v2.2.1+incompatible
github.com/cenkalti/backoff/v4 v4.1.3
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/coreos/go-semver v0.3.0 // indirect
@@ -67,9 +74,9 @@ require (
github.com/go-openapi/swag v0.22.3 // indirect
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.10.0 // indirect
github.com/go-playground/validator/v10 v10.11.1 // indirect
github.com/go-redis/redis/v8 v8.11.5 // indirect
github.com/goccy/go-json v0.9.7 // indirect
github.com/goccy/go-json v0.10.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
@@ -88,14 +95,14 @@ require (
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.15.9 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/lib/pq v1.10.7 // indirect
github.com/lib/pq v1.10.7
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-sqlite3 v1.14.15 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/montanaflynn/stats v0.6.6 // indirect
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
@@ -113,9 +120,9 @@ require (
go.uber.org/atomic v1.10.0 // indirect
go.uber.org/multierr v1.8.0 // indirect
go4.org/netipx v0.0.0-20220812043211-3cc044ffd68d // indirect
golang.org/x/crypto v0.0.0-20221012134737-56aed061732a // indirect
golang.org/x/crypto v0.3.0 // indirect
golang.org/x/sync v0.0.0-20220907140024-f12130a52804 // indirect
golang.org/x/text v0.3.8 // indirect
golang.org/x/text v0.4.0 // indirect
golang.org/x/tools v0.1.12 // indirect
google.golang.org/genproto v0.0.0-20220909194730-69f6226f97e5 // indirect
google.golang.org/grpc v1.49.0 // indirect
+43 -22
View File
@@ -7,8 +7,6 @@ github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/MicahParks/keyfunc v1.5.1 h1:RlyyYgKQI/adkIw1yXYtPvTAOb7hBhSX42aH23d8N0Q=
github.com/MicahParks/keyfunc v1.5.1/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw=
github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/hcsshim v0.9.4 h1:mnUj0ivWy6UzbB1uLFqKR6F+ZyiDc7j4iGgHTpO+5+I=
@@ -31,8 +29,6 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bufbuild/connect-go v1.0.0 h1:htSflKUT8y1jxhoPhPYTZMrsY3ipUXjjrbcZR5O2cVo=
github.com/bufbuild/connect-go v1.0.0/go.mod h1:9iNvh/NOsfhNBUH5CtvXeVUskQO1xsrEviH7ZArwZ3I=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4=
github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -51,6 +47,8 @@ github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkX
github.com/containerd/containerd v1.6.8 h1:h4dOFDwzHmqFEP754PgfgTeVXFnLiRc6kiqC7tplDJs=
github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg=
github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM=
github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk=
github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
@@ -93,6 +91,8 @@ github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0
github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs=
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
github.com/gin-contrib/sessions v0.0.5 h1:CATtfHmLMQrMNpJRgzjWXD7worTh7g7ritsQfmF+0jE=
github.com/gin-contrib/sessions v0.0.5/go.mod h1:vYAuaUPqie3WUSsft6HUlCjlwwoJQs97miaG2+7neKY=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8=
@@ -122,15 +122,17 @@ github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0=
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ=
github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
@@ -138,8 +140,6 @@ github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRx
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs=
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
@@ -178,6 +178,12 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
@@ -284,11 +290,13 @@ github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope
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-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/metal-stack/go-ipam v1.11.2 h1:j6DH8puaTzSGOACzEqV8v0lrHkuNfPKeTol8MiexmG8=
github.com/metal-stack/go-ipam v1.11.2/go.mod h1:rIaCP9hkHKAZFuICswmdl0SYUv+X2RoHJYLJFQnB01Q=
@@ -333,8 +341,8 @@ github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg=
github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas=
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
github.com/pierrre/gotestcover v0.0.0-20160517101806-924dca7d15f0/go.mod h1:4xpMLz7RBWyB+ElzHu8Llua96TRCB3YwX+l5EP1wmHk=
github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg=
github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA=
@@ -345,6 +353,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/cachecontrol v0.1.0 h1:yJMy84ti9h/+OEWa752kBTKv4XC30OtVVHYv/8cTqKc=
github.com/pquerna/cachecontrol v0.1.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
@@ -360,6 +370,8 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/redhat-et/go-oidc-agent v0.0.2-0.20221130160039-b98e1621e734 h1:LbMeu6H1fLMrp4FQ1KbKSqTdZwGiNgDqnkncyJfGzz0=
github.com/redhat-et/go-oidc-agent v0.0.2-0.20221130160039-b98e1621e734/go.mod h1:BU0GliOTINM42cNrdkSk1e6PG+Lth5b30SIxyJDvCMM=
github.com/robertkrimen/godocdown v0.0.0-20130622164427-0bfa04905481/go.mod h1:C9WhFzY47SzYBIvzFqSvHIR6ROgDo4TtdTuRaOMjF/s=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
@@ -420,8 +432,8 @@ github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/urfave/cli/v2 v2.19.2 h1:eXu5089gqqiDQKSnFW+H/FhjrxRGztwSxlTsVK7IuqQ=
github.com/urfave/cli/v2 v2.19.2/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI=
github.com/urfave/cli/v2 v2.23.5 h1:xbrU7tAYviSpqeR3X4nEFWUdB/uDZ6DE+HxmRU7Xtyw=
github.com/urfave/cli/v2 v2.23.5/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df h1:OviZH7qLw/7ZovXvuNyL3XQl8UFofeikI1NW1Gypu7k=
@@ -491,10 +503,11 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20221012134737-56aed061732a h1:NmSIgad6KjE6VvHciPZuNRTKxGhlPfD6OA87W/PLkqg=
golang.org/x/crypto v0.0.0-20221012134737-56aed061732a/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@@ -514,6 +527,7 @@ golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
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=
@@ -528,11 +542,13 @@ golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220909164309-bea034e7d591 h1:D0B/7al0LLrVC8aWF4+oxpv/m8bc7ViFfVS8/gXGdqI=
golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
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-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.2.0 h1:GtQkldQ9m7yvzCL1V+LrYow3Khe0eJH0w7RbX/VbaIU=
golang.org/x/oauth2 v0.2.0/go.mod h1:Cwn6afJ8jrQwYMxQDTpISoXmXW9I6qF6vDeuuoX3Ibs=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -581,8 +597,9 @@ golang.org/x/sys v0.0.0-20211107104306-e0b2ad06fe42/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -593,8 +610,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -624,6 +641,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
@@ -665,6 +684,8 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
File diff suppressed because it is too large Load Diff
+20
View File
@@ -0,0 +1,20 @@
#!/bin/sh
set -e
source $(pwd)/.env
USERNAME="$1"
PASSWORD="$2"
token=$(curl -s -f -X POST \
$APEX_OIDC_URL/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d "username=$USERNAME" \
-d "password=$PASSWORD" \
-d "scope=openid profile email" \
-d "grant_type=password" \
-d "client_id=$APEX_OIDC_CLIENT_ID_CLI"
)
echo $token
-20
View File
@@ -1,20 +0,0 @@
#!/bin/sh
set -e
HOST="localhost:8080/auth"
REALM="controller"
USERNAME="$1"
PASSWORD="$2"
CLIENTID='api-clients'
CLIENTSECRET='cvXhCRXI2Vld244jjDcnABCMrTEq2rwE'
token=$(curl -sf -X POST \
http://$HOST/realms/$REALM/protocol/openid-connect/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d "username=$USERNAME" \
-d "password=$PASSWORD" \
-d "grant_type=password" \
-d "client_id=$CLIENTID" \
-d "client_secret=$CLIENTSECRET" | jq -r ".access_token")
echo $token
+51
View File
@@ -0,0 +1,51 @@
Deploy on Kind
==============
## Create Cluster
```console
kind create cluster ./deploy/kind.yaml
kubectl cluster-info --context kind-apex-dev
```
## Install Ingress Controller
```console
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml
kubectl rollout status deployment ingress-nginx-controller -n ingress-nginx --timeout=90s
```
## Fix internal name resolution
```console
kubectl edit configmap -n kube-system coredns
```
Add the following line in `:53{}`
```
rewrite name auth.apex.local dex.apex.svc.cluster.local
```
Then restart core-dns:
```console
kubectl rollout restart -n kube-system deployment/coredns
kubectl rollout status -n kube-system deployment coredns
```
## Load Images
```console
make images
kind load --name apex-dev docker-image quay.io/apex/apiserver:latest
kind load --name apex-dev docker-image quay.io/apex/frontend:latest
```
## Install Apex
```console
kubectl create namespace apex
kubectl apply -k ./deploy/overlays/dev
kubectl rollout status -n apex statefulset ipam
```
+58
View File
@@ -0,0 +1,58 @@
#!/bin/sh
set -e
up() {
kind create cluster --config ./deploy/kind.yaml
kubectl cluster-info --context kind-apex-dev
# Deploy Ingress Controller
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml
kubectl rollout status deployment ingress-nginx-controller -n ingress-nginx --timeout=90s
# Add rewrite to CoreDNS
kubectl get -n kube-system cm/coredns -o yaml > coredns.yaml
sed -i '22i \
rewrite name auth.apex.local dex.apex.svc.cluster.local' coredns.yaml
kubectl replace -n kube-system -f coredns.yaml
rm coredns.yaml
kubectl rollout restart -n kube-system deployment/coredns
kubectl rollout status -n kube-system deployment coredns --timeout=90s
# Build images and copy to kind
make images
kind load --name apex-dev docker-image quay.io/apex/apiserver:latest
kind load --name apex-dev docker-image quay.io/apex/frontend:latest
# Create namespace and deploy apex
kubectl create namespace apex
kubectl apply -k ./deploy/overlays/dev
kubectl rollout status -n apex deployment dex --timeout=90s
kubectl rollout status -n apex statefulset apiserver --timeout=90s
kubectl rollout status -n apex statefulset ipam --timeout=90s
kubectl rollout status -n apex deployment backend-web --timeout=90s
kubectl rollout status -n apex deployment backend-cli --timeout=90s
kubectl rollout status -n apex deployment apiproxy --timeout=90s
kubectl wait --for=condition=Ready pods --all -n apex --timeout=120s
# give k8s a little longer to come up
sleep 5
}
down() {
kind delete cluster --name apex-dev
}
case $1 in
"up")
up
;;
"down")
down
;;
*)
echo "command required. up or down"
exit 1
;;
esac
+1 -1
View File
@@ -101,4 +101,4 @@ while getopts "u:dh" opt; do
done
if [ $# -eq 0 ]; then
help;exit 0
fi
fi
+11
View File
@@ -0,0 +1,11 @@
#!/bin/sh
set -e
teardown() {
./hack/kind/kind.sh down
}
trap teardown EXIT
./hack/kind/kind.sh up
go test -v --tags=integration ./integration-tests/...
+62 -70
View File
@@ -10,7 +10,6 @@ import (
"testing"
"time"
"github.com/cenkalti/backoff/v4"
"github.com/ory/dockertest/v3"
"github.com/redhat-et/apex/internal/apex"
"github.com/stretchr/testify/require"
@@ -26,12 +25,6 @@ func (suite *ApexIntegrationSuite) SetupSuite() {
var err error
suite.pool, err = dockertest.NewPool("")
require.NoError(suite.T(), err)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
err = backoff.Retry(healthcheck, backoff.WithContext(backoff.NewExponentialBackOff(), ctx))
require.NoError(suite.T(), err)
}
func TestApexIntegrationSuite(t *testing.T) {
@@ -42,7 +35,10 @@ func (suite *ApexIntegrationSuite) TestBasicConnectivity() {
assert := suite.Assert()
require := suite.Require()
token, err := GetToken("admin", "floofykittens")
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
token, err := getToken(ctx, "admin@apex.local", "floofykittens")
require.NoError(err)
// create the nodes
@@ -51,15 +47,12 @@ func (suite *ApexIntegrationSuite) TestBasicConnectivity() {
node2 := suite.CreateNode("node2", "bridge", []string{})
defer node2.Close()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// start apex on the nodes
go func() {
_, err = containerExec(ctx, node1, []string{
"/bin/apex",
fmt.Sprintf("--with-token=%s", token),
"http://host.docker.internal:8080",
"http://apex.local",
})
}()
@@ -67,7 +60,7 @@ func (suite *ApexIntegrationSuite) TestBasicConnectivity() {
_, err = containerExec(ctx, node2, []string{
"/bin/apex",
fmt.Sprintf("--with-token=%s", token),
"http://host.docker.internal:8080",
"http://apex.local",
})
}()
@@ -78,11 +71,11 @@ func (suite *ApexIntegrationSuite) TestBasicConnectivity() {
suite.T().Logf("Pinging %s from node1", node2IP)
err = ping(ctx, node1, node2IP)
assert.NoError(err)
require.NoError(err)
suite.T().Logf("Pinging %s from node2", node1IP)
err = ping(ctx, node2, node1IP)
assert.NoError(err)
require.NoError(err)
//kill the apex process on both nodes
_, err = containerExec(ctx, node1, []string{"killall", "apex"})
@@ -102,7 +95,7 @@ func (suite *ApexIntegrationSuite) TestBasicConnectivity() {
_, err = containerExec(ctx, node1, []string{
"/bin/apex",
fmt.Sprintf("--with-token=%s", token),
"http://host.docker.internal:8080",
"http://apex.local",
})
}()
@@ -110,7 +103,7 @@ func (suite *ApexIntegrationSuite) TestBasicConnectivity() {
_, err = containerExec(ctx, node2, []string{
"/bin/apex",
fmt.Sprintf("--with-token=%s", token),
"http://host.docker.internal:8080",
"http://apex.local",
})
}()
@@ -138,7 +131,9 @@ func (suite *ApexIntegrationSuite) TestRequestIPDefaultZone() {
node1IP := "10.200.0.101"
node2IP := "10.200.0.102"
token, err := GetToken("admin", "floofykittens")
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
token, err := getToken(ctx, "admin@apex.local", "floofykittens")
require.NoError(err)
// create the nodes
@@ -147,16 +142,13 @@ func (suite *ApexIntegrationSuite) TestRequestIPDefaultZone() {
node2 := suite.CreateNode("node2", "bridge", []string{})
defer node2.Close()
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
// start apex on the nodes
go func() {
_, err = containerExec(ctx, node1, []string{
"/bin/apex",
fmt.Sprintf("--request-ip=%s", node1IP),
fmt.Sprintf("--with-token=%s", token),
"http://host.docker.internal:8080",
"http://apex.local",
})
}()
@@ -165,7 +157,7 @@ func (suite *ApexIntegrationSuite) TestRequestIPDefaultZone() {
"/bin/apex",
fmt.Sprintf("--request-ip=%s", node2IP),
fmt.Sprintf("--with-token=%s", token),
"http://host.docker.internal:8080",
"http://apex.local",
})
}()
@@ -183,18 +175,20 @@ func (suite *ApexIntegrationSuite) TestRequestIPDefaultZone() {
func (suite *ApexIntegrationSuite) TestRequestIPZone() {
assert := suite.Assert()
require := suite.Require()
token, err := GetToken("kitteh1", "floofykittens")
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
token, err := getToken(ctx, "kitteh1@apex.local", "floofykittens")
require.NoError(err)
c, err := newClient(token)
c, err := newClient(ctx, token)
require.NoError(err)
// create a new zone
zoneID, err := c.CreateZone("zone-blue", "zone full of blue things", "10.140.0.0/24", false)
assert.NoError(err)
require.NoError(err)
// patch the new user into the zone
_, err = c.MoveCurrentUserToZone(zoneID.ID)
assert.NoError(err)
require.NoError(err)
node1IP := "10.140.0.101"
node2IP := "10.140.0.102"
@@ -205,16 +199,13 @@ func (suite *ApexIntegrationSuite) TestRequestIPZone() {
node2 := suite.CreateNode("node2", "bridge", []string{})
defer node2.Close()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// start apex on the nodes
go func() {
_, err = containerExec(ctx, node1, []string{
"/bin/apex",
fmt.Sprintf("--request-ip=%s", node1IP),
fmt.Sprintf("--with-token=%s", token),
"http://host.docker.internal:8080",
"http://apex.local",
})
}()
@@ -223,7 +214,7 @@ func (suite *ApexIntegrationSuite) TestRequestIPZone() {
"/bin/apex",
fmt.Sprintf("--request-ip=%s", node2IP),
fmt.Sprintf("--with-token=%s", token),
"http://host.docker.internal:8080",
"http://apex.local",
})
}()
@@ -249,7 +240,7 @@ func (suite *ApexIntegrationSuite) TestRequestIPZone() {
"/bin/apex",
fmt.Sprintf("--request-ip=%s", node1IP),
fmt.Sprintf("--with-token=%s", token),
"http://host.docker.internal:8080",
"http://apex.local",
})
}()
@@ -258,7 +249,7 @@ func (suite *ApexIntegrationSuite) TestRequestIPZone() {
"/bin/apex",
fmt.Sprintf("--request-ip=%s", node2IP),
fmt.Sprintf("--with-token=%s", token),
"http://host.docker.internal:8080",
"http://apex.local",
})
}()
@@ -276,19 +267,21 @@ func (suite *ApexIntegrationSuite) TestRequestIPZone() {
func (suite *ApexIntegrationSuite) TestHubZone() {
assert := suite.Assert()
require := suite.Require()
token, err := GetToken("kitteh2", "floofykittens")
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
token, err := getToken(ctx, "kitteh2@apex.local", "floofykittens")
require.NoError(err)
c, err := newClient(token)
c, err := newClient(ctx, token)
require.NoError(err)
// create a new zone
zoneID, err := c.CreateZone("zone-relay", "zone with a relay hub", "10.162.0.0/24", true)
assert.NoError(err)
require.NoError(err)
// patch the new user into the zone
_, err = c.MoveCurrentUserToZone(zoneID.ID)
assert.NoError(err)
require.NoError(err)
// create the nodes
node1 := suite.CreateNode("node1", "bridge", []string{})
@@ -296,10 +289,7 @@ func (suite *ApexIntegrationSuite) TestHubZone() {
node2 := suite.CreateNode("node2", "bridge", []string{})
defer node2.Close()
node3 := suite.CreateNode("node3", "bridge", []string{})
defer node2.Close()
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
defer node3.Close()
// start apex on the nodes
go func() {
@@ -307,7 +297,7 @@ func (suite *ApexIntegrationSuite) TestHubZone() {
"/bin/apex",
"--hub-router",
fmt.Sprintf("--with-token=%s", token),
"http://host.docker.internal:8080",
"http://apex.local",
})
}()
@@ -317,7 +307,7 @@ func (suite *ApexIntegrationSuite) TestHubZone() {
_, err = containerExec(ctx, node2, []string{
"/bin/apex",
fmt.Sprintf("--with-token=%s", token),
"http://host.docker.internal:8080",
"http://apex.local",
})
}()
@@ -325,7 +315,7 @@ func (suite *ApexIntegrationSuite) TestHubZone() {
_, err = containerExec(ctx, node3, []string{
"/bin/apex",
fmt.Sprintf("--with-token=%s", token),
"http://host.docker.internal:8080",
"http://apex.local",
})
}()
@@ -357,19 +347,21 @@ func (suite *ApexIntegrationSuite) TestHubZone() {
func (suite *ApexIntegrationSuite) TestChildPrefix() {
assert := suite.Assert()
require := suite.Require()
token, err := GetToken("kitteh3", "floofykittens")
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
token, err := getToken(ctx, "kitteh3@apex.local", "floofykittens")
require.NoError(err)
c, err := newClient(token)
c, err := newClient(ctx, token)
require.NoError(err)
// create a new zone
zoneID, err := c.CreateZone("zone-child-prefix", "zone full of toddler prefixes", "100.64.100.0/24", false)
assert.NoError(err)
require.NoError(err)
// patch the new user into the zone
_, err = c.MoveCurrentUserToZone(zoneID.ID)
assert.NoError(err)
require.NoError(err)
node1LoopbackNet := "172.16.10.101/32"
node2LoopbackNet := "172.16.20.102/32"
@@ -382,16 +374,13 @@ func (suite *ApexIntegrationSuite) TestChildPrefix() {
node2 := suite.CreateNode("node2", "bridge", []string{})
defer node2.Close()
ctx, cancel := context.WithTimeout(context.Background(), 40*time.Second)
defer cancel()
// start apex on the nodes
go func() {
_, err = containerExec(ctx, node1, []string{
"/bin/apex",
fmt.Sprintf("--child-prefix=%s", node1ChildPrefix),
fmt.Sprintf("--with-token=%s", token),
"http://host.docker.internal:8080",
"http://apex.local",
})
}()
@@ -400,15 +389,15 @@ func (suite *ApexIntegrationSuite) TestChildPrefix() {
"/bin/apex",
fmt.Sprintf("--child-prefix=%s", node2ChildPrefix),
fmt.Sprintf("--with-token=%s", token),
"http://host.docker.internal:8080",
"http://apex.local",
})
}()
// add loopbacks to the containers that are contained in the node's child prefix
_, err = containerExec(ctx, node1, []string{"ip", "addr", "add", node1LoopbackNet, "dev", "lo"})
assert.NoError(err)
require.NoError(err)
_, err = containerExec(ctx, node2, []string{"ip", "addr", "add", node2LoopbackNet, "dev", "lo"})
assert.NoError(err)
require.NoError(err)
// parse the loopback ip from the loopback prefix
node1LoopbackIP, _, _ := net.ParseCIDR(node1LoopbackNet)
@@ -465,14 +454,17 @@ func (suite *ApexIntegrationSuite) TestRelayNAT() {
net2Spoke1Name := "net2-spoke1"
net1Spoke2Name := "net1-spoke2"
net2Spoke2Name := "net2-spoke2"
controllerURL := "http://172.17.0.1:8080"
controllerURL := "http://apex.local"
// launch a relay node in the default namespace that all spokes can reach
relayNode := suite.CreateNode(relayNodeName, defaultNSNet, []string{})
defer relayNode.Close()
_ = suite.CreateNetwork("net1", "100.64.11.0/24")
_ = suite.CreateNetwork("net2", "100.64.12.0/24")
dNet1 := suite.CreateNetwork(net1, "100.64.11.0/24")
defer dNet1.Close()
dNet2 := suite.CreateNetwork(net2, "100.64.12.0/24")
defer dNet2.Close()
// launch nat nodes
natNodeNet1 := suite.CreateNode("net1-nat", net1, []string{})
@@ -480,7 +472,7 @@ func (suite *ApexIntegrationSuite) TestRelayNAT() {
natNodeNet2 := suite.CreateNode("net2-nat", net2, []string{})
defer natNodeNet2.Close()
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
defer cancel()
// attach nat nodes to the spoke networks
@@ -503,13 +495,13 @@ func (suite *ApexIntegrationSuite) TestRelayNAT() {
// create spoke nodes
net1SpokeNode1 := suite.CreateNode(net1Spoke1Name, net1, []string{})
defer natNodeNet1.Close()
defer net1SpokeNode1.Close()
net2SpokeNode1 := suite.CreateNode(net2Spoke1Name, net2, []string{})
defer natNodeNet2.Close()
defer net2SpokeNode1.Close()
net1SpokeNode2 := suite.CreateNode(net1Spoke2Name, net1, []string{})
defer natNodeNet1.Close()
defer net1SpokeNode2.Close()
net2SpokeNode2 := suite.CreateNode(net2Spoke2Name, net2, []string{})
defer natNodeNet2.Close()
defer net2SpokeNode2.Close()
// delete the default route pointing to the nat gateway
_, err = containerExec(ctx, net1SpokeNode1, []string{"ip", "-4", "route", "del", "default"})
@@ -535,26 +527,26 @@ func (suite *ApexIntegrationSuite) TestRelayNAT() {
suite.T().Logf("Validate NAT Infra: Pinging %s from net2-spoke1", docker0)
err = ping(ctx, net2SpokeNode1, docker0)
assert.NoError(err)
suite.T().Logf("Validate NAT Infra: Pinging %s from net1-spoke1", docker0)
suite.T().Logf("Validate NAT Infra: Pinging %s from net1-spoke2", docker0)
err = ping(ctx, net1SpokeNode2, docker0)
assert.NoError(err)
suite.T().Logf("Validate NAT Infra: Pinging %s from net2-spoke1", docker0)
suite.T().Logf("Validate NAT Infra: Pinging %s from net2-spoke2", docker0)
err = ping(ctx, net2SpokeNode2, docker0)
assert.NoError(err)
token, err := GetToken("kitteh4", "floofykittens")
token, err := getToken(ctx, "kitteh4@apex.local", "floofykittens")
require.NoError(err)
c, err := newClient(token)
c, err := newClient(ctx, token)
require.NoError(err)
// create a new zone
zoneID, err := c.CreateZone("zone-nat-relay", "nat test zone", "10.29.0.0/24", true)
assert.NoError(err)
require.NoError(err)
// patch the new user into the zone
_, err = c.MoveCurrentUserToZone(zoneID.ID)
assert.NoError(err)
require.NoError(err)
// start apex on the nodes
go func() {
+45 -71
View File
@@ -11,43 +11,18 @@ import (
"fmt"
"io"
"net"
"net/http"
"net/url"
"strings"
"time"
"github.com/cenkalti/backoff/v4"
"github.com/coreos/go-oidc"
"github.com/ory/dockertest/v3"
"github.com/ory/dockertest/v3/docker"
"github.com/redhat-et/apex/internal/client"
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
)
const (
controller = "http://localhost:8080"
clientId = "api-clients"
clientSecret = "cvXhCRXI2Vld244jjDcnABCMrTEq2rwE"
)
func healthcheck() error {
res, err := http.Get(fmt.Sprintf("%s/api/health", controller))
if err != nil {
return err
}
if res.StatusCode != http.StatusOK {
return fmt.Errorf("got %d, wanted 200", res.StatusCode)
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
return err
}
if !strings.Contains(string(body), "ok") {
return fmt.Errorf("service is not healthy")
}
return nil
}
// CreateNode creates a container
func (suite *ApexIntegrationSuite) CreateNode(name, network string, args []string) *dockertest.Resource {
options := &dockertest.RunOptions{
@@ -63,11 +38,13 @@ func (suite *ApexIntegrationSuite) CreateNode(name, network string, args []strin
"NET_RAW",
},
ExtraHosts: []string{
"host.docker.internal:host-gateway",
"apex.local:host-gateway",
"api.apex.local:host-gateway",
"auth.apex.local:host-gateway",
},
}
hostConfig := func(config *docker.HostConfig) {
//config.AutoRemove = true
// config.AutoRemove = true
config.RestartPolicy = docker.RestartPolicy{
Name: "no",
}
@@ -79,47 +56,8 @@ func (suite *ApexIntegrationSuite) CreateNode(name, network string, args []strin
return node
}
// GetToken creates a new auth token
func GetToken(username, password string) (string, error) {
v := url.Values{}
v.Set("username", username)
v.Set("password", password)
v.Set("client_id", clientId)
v.Set("client_secret", clientSecret)
v.Set("grant_type", "password")
res, err := http.PostForm(fmt.Sprintf("%s/auth/realms/controller/protocol/openid-connect/token", controller), v)
if err != nil {
return "", err
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
return "", err
}
if res.StatusCode != http.StatusOK {
return "", err
}
var r map[string]interface{}
if err := json.Unmarshal(body, &r); err != nil {
return "", err
}
token, ok := r["access_token"]
if !ok {
return "", fmt.Errorf("no access token in reponse")
}
return token.(string), nil
}
func newClient(token string) (client.Client, error) {
auth := client.NewTokenAuthenticator(token)
client, err := client.NewClient(controller, auth)
if err != nil {
return client, err
}
return client, nil
func newClient(ctx context.Context, token string) (*client.Client, error) {
return client.NewClient(ctx, "http://api.apex.local", client.WithToken(token))
}
func getContainerIfaceIP(ctx context.Context, dev string, container *dockertest.Resource) (string, error) {
@@ -194,6 +132,43 @@ func containerExec(ctx context.Context, container *dockertest.Resource, cmd []st
return stdout.String(), err
}
func getToken(ctx context.Context, username, password string) (string, error) {
provider, err := oidc.NewProvider(ctx, "http://auth.apex.local")
if err != nil {
return "", err
}
config := oauth2.Config{
ClientID: "apex-cli",
//ClientSecret: "dhEN2dsqyUg5qmaDAdqi4CmH",
Endpoint: provider.Endpoint(),
Scopes: []string{"openid", "profile", "email"},
}
token, err := config.PasswordCredentialsToken(ctx, username, password)
if err != nil {
return "", err
}
data, err := json.Marshal(token)
if err != nil {
return "", err
}
var rawToken map[string]interface{}
if err := json.Unmarshal(data, &rawToken); err != nil {
return "", err
}
rawToken["id_token"] = token.Extra("id_token")
data, err = json.Marshal(rawToken)
if err != nil {
return "", err
}
return string(data), err
}
// CreateNetwork creates a docker network
func (suite *ApexIntegrationSuite) CreateNetwork(name, cidr string) *dockertest.Network {
net, err := suite.pool.CreateNetwork(name, func(config *docker.CreateNetworkOptions) {
@@ -208,7 +183,6 @@ func (suite *ApexIntegrationSuite) CreateNetwork(name, cidr string) *dockertest.
}
})
require.NoError(suite.T(), err)
return net
}
+9 -9
View File
@@ -40,7 +40,7 @@ type Apex struct {
hubRouterWgIP string
os string
wgConfig wgConfig
client client.Client
client *client.Client
controllerURL *url.URL
// caches peers by their UUID
peerCache map[uuid.UUID]models.Peer
@@ -83,19 +83,19 @@ func NewApex(ctx context.Context, cCtx *cli.Context) (*Apex, error) {
log.Fatalf("error: <controller-url> is not a valid URL: %s", err)
}
// Force controller URL be api.${DOMAIN}
controllerURL.Host = "api." + controllerURL.Host
controllerURL.Path = ""
withToken := cCtx.String("with-token")
var auth client.Authenticator
var option client.Option
if withToken == "" {
var err error
auth, err = client.NewDeviceFlowAuthenticator(ctx, controllerURL)
if err != nil {
log.Fatalf("authentication error: %+v", err)
}
option = client.WithDeviceFlow()
} else {
auth = client.NewTokenAuthenticator(withToken)
option = client.WithToken(withToken)
}
client, err := client.NewClient(controller, auth)
client, err := client.NewClient(ctx, controllerURL.String(), option)
if err != nil {
log.Fatalf("error creating client: %+v", err)
}
+88 -139
View File
@@ -9,21 +9,11 @@ import (
"net/url"
"time"
log "github.com/sirupsen/logrus"
agent "github.com/redhat-et/go-oidc-agent"
"golang.org/x/oauth2"
)
// TODO: These consts witll differ from installation to installation.
// Need to find a way to provide these dynamically (config file) etc...
const (
APEX_CLIENT_ID = "apex-cli"
APEX_CLIENT_SECRET = "QkskUDQenfXRxWx9UA0TeuwmOnHilHtQ"
LOGIN_URL = "/auth/realms/controller/protocol/openid-connect/auth/device"
VERIFICATION_URI = "/auth/realms/controller/device"
VERIFY_URL = "/auth/realms/controller/protocol/openid-connect/token"
GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code"
)
type TokenResponse struct {
type deviceFlowResponse struct {
DeviceCode string `json:"device_code"`
UserCode string `json:"user_code"`
VerificationURI string `json:"verification_uri"`
@@ -32,125 +22,67 @@ type TokenResponse struct {
Interval int `json:"interval"`
}
type Authenticator interface {
Token() (string, error)
}
type TokenAuthenticator struct {
accessToken string
}
func NewTokenAuthenticator(token string) Authenticator {
return &TokenAuthenticator{
accessToken: token,
}
}
func (a *TokenAuthenticator) Token() (string, error) {
return a.accessToken, nil
}
type DeviceFlowAuthenticator struct {
hostname *url.URL
accessToken string
refreshToken string
tokenExpiry time.Time
refreshExpiry time.Time
}
func NewDeviceFlowAuthenticator(ctx context.Context, hostname *url.URL) (*DeviceFlowAuthenticator, error) {
a := &DeviceFlowAuthenticator{
hostname: hostname,
}
func newDeviceFlowToken(ctx context.Context, deviceEndpoint, tokenEndpoint, clientID string) (*oauth2.Token, interface{}, error) {
requestTime := time.Now()
token, err := getToken(a.hostname)
d, err := startDeviceFlow(deviceEndpoint, clientID)
if err != nil {
return nil, err
return nil, nil, err
}
fmt.Println("Your device must be registered with Apex Controller.")
fmt.Printf("Your one-time code is: %s\n", token.UserCode)
fmt.Println("Please open the following URL in your browser and enter your one-time code:")
dest, err := url.JoinPath(a.hostname.String(), VERIFICATION_URI)
if err != nil {
return nil, err
}
fmt.Printf("%s\n", dest)
fmt.Println("Your device must be registered with Apex.")
fmt.Printf("Your one-time code is: %s\n", d.UserCode)
fmt.Println("Please open the following URL in your browser to sign in:")
fmt.Printf("%s\n", d.VerificationURIComplete)
var token *oauth2.Token
var idToken interface{}
c := make(chan error, 1)
ctx, cancel := context.WithTimeout(ctx, time.Duration(token.ExpiresIn)*time.Second)
ctx, cancel := context.WithTimeout(ctx, time.Duration(d.ExpiresIn)*time.Second)
defer cancel()
go func() {
c <- a.pollForResponse(ctx, token, requestTime)
token, idToken, err = pollForResponse(ctx, clientID, tokenEndpoint, d, requestTime)
c <- err
}()
err = <-c
if err != nil {
return nil, err
return nil, nil, err
}
fmt.Println("Authentication succeeded.")
return a, nil
return token, idToken, nil
}
func (a *DeviceFlowAuthenticator) Token() (string, error) {
if time.Now().After(a.tokenExpiry) {
log.Debugf("Access token has expired. Requesting a new one")
if time.Now().After(a.refreshExpiry) {
return "", fmt.Errorf("refresh token has expired")
}
if err := a.refreshTokens(); err != nil {
return "", err
}
}
if a.accessToken != "" {
return a.accessToken, nil
}
return "", fmt.Errorf("not authenticated")
}
func (a *DeviceFlowAuthenticator) refreshTokens() error {
v := url.Values{}
v.Set("client_id", APEX_CLIENT_ID)
v.Set("client_secret", APEX_CLIENT_SECRET)
v.Set("grant_type", "refresh_token")
v.Set("refresh_token", a.refreshToken)
dest, err := url.JoinPath(a.hostname.String(), VERIFY_URL)
func startLogin(hostname url.URL) (*agent.DeviceStartReponse, error) {
dest := hostname
dest.Path = "/login/start"
res, err := http.Post(dest.String(), "application/json", nil)
if err != nil {
return err
}
requestTime := time.Now()
res, err := http.PostForm(dest, v)
if err != nil {
return err
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
return err
return nil, err
}
if res.StatusCode != http.StatusOK {
return err
return nil, fmt.Errorf("request %s failed with %d", dest.String(), res.StatusCode)
}
var r tokenReponse
if err := json.Unmarshal(body, &r); err != nil {
return err
}
a.accessToken = r.AccessToken
a.tokenExpiry = requestTime.Add(time.Duration(r.ExpiresIn) * time.Second)
a.refreshToken = r.RefreshToken
a.refreshExpiry = requestTime.Add(time.Duration(r.RefreshExpiresIn) * time.Second)
return nil
}
defer res.Body.Close()
func getToken(hostname *url.URL) (*TokenResponse, error) {
v := url.Values{}
v.Set("client_id", APEX_CLIENT_ID)
v.Set("client_secret", APEX_CLIENT_SECRET)
dest, err := url.JoinPath(hostname.String(), LOGIN_URL)
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
res, err := http.PostForm(dest, v)
var resp agent.DeviceStartReponse
if err = json.Unmarshal(body, &resp); err != nil {
return nil, err
}
return &resp, nil
}
func startDeviceFlow(deviceEndpoint string, clientID string) (*deviceFlowResponse, error) {
v := url.Values{}
v.Set("client_id", clientID)
v.Set("scope", "openid profile email")
res, err := http.PostForm(deviceEndpoint, v)
if err != nil {
return nil, err
}
@@ -165,7 +97,7 @@ func getToken(hostname *url.URL) (*TokenResponse, error) {
return nil, fmt.Errorf("http error: %s", string(body))
}
var t TokenResponse
var t deviceFlowResponse
if err := json.Unmarshal(body, &t); err != nil {
return nil, err
}
@@ -173,57 +105,74 @@ func getToken(hostname *url.URL) (*TokenResponse, error) {
return &t, nil
}
type tokenReponse struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
RefreshToken string `json:"refresh_token"`
RefreshExpiresIn int `json:"refresh_expires_in"`
}
const (
errAuthorizationPending = "authorization_pending"
errSlowDown = "slow_down"
errAccessDenied = "access_denied"
errExpiredToken = "expired_token"
)
func (a *DeviceFlowAuthenticator) pollForResponse(ctx context.Context, t *TokenResponse, requestTime time.Time) error {
func pollForResponse(ctx context.Context, clientID string, tokenURL string, t *deviceFlowResponse, requestTime time.Time) (*oauth2.Token, interface{}, error) {
v := url.Values{}
v.Set("device_code", t.DeviceCode)
v.Set("client_id", APEX_CLIENT_ID)
v.Set("client_secret", APEX_CLIENT_SECRET)
v.Set("grant_type", GRANT_TYPE)
v.Set("client_id", clientID)
v.Set("grant_type", "urn:ietf:params:oauth:grant-type:device_code")
ticker := time.NewTicker(time.Duration(t.Interval) * time.Second)
var r tokenReponse
LOOP:
interval := t.Interval
if interval == 0 {
// Pick a reasonable default if none is set
interval = 5
}
ticker := time.NewTicker(time.Duration(interval) * time.Second)
var token oauth2.Token
for {
select {
case <-ctx.Done():
return ctx.Err()
return nil, nil, ctx.Err()
case <-ticker.C:
dest, err := url.JoinPath(a.hostname.String(), VERIFY_URL)
if err != nil {
continue
}
res, err := http.PostForm(dest, v)
res, err := http.PostForm(tokenURL, v)
if err != nil {
// possible transient connection error, continue retrying
continue
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
// possible transient connection error, continue retrying
continue
}
if res.StatusCode != http.StatusOK {
continue
type errorResponse struct {
Error string `json:"error"`
}
var r errorResponse
if err := json.Unmarshal(body, &r); err != nil {
return nil, "", err
}
if r.Error != "" {
if r.Error == errSlowDown {
// adjust interval and continue retrying
interval += 5
ticker.Reset(time.Duration(interval) * time.Second)
continue
} else if r.Error == errAccessDenied || r.Error == errExpiredToken {
return nil, nil, fmt.Errorf("failed to get token: %s", r.Error)
}
// error was either authorization_pending or something else
// continue to poll for a token
continue
}
}
if err := json.Unmarshal(body, &r); err != nil {
continue
if err := json.Unmarshal(body, &token); err != nil {
return nil, "", err
}
if r.AccessToken != "" {
break LOOP
var tokenRaw map[string]interface{}
if err = json.Unmarshal(body, &tokenRaw); err != nil {
return nil, "", err
}
idToken := tokenRaw["id_token"]
return &token, idToken, nil
}
}
a.accessToken = r.AccessToken
a.tokenExpiry = requestTime.Add(time.Duration(r.ExpiresIn) * time.Second)
a.refreshToken = r.RefreshToken
a.refreshExpiry = requestTime.Add(time.Duration(r.RefreshExpiresIn) * time.Second)
return nil
}
+76 -17
View File
@@ -1,34 +1,93 @@
package client
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"github.com/coreos/go-oidc"
"golang.org/x/oauth2"
)
type Client struct {
options *options
baseURL *url.URL
auth Authenticator
client *http.Client
}
func NewClient(addr string, auth Authenticator) (Client, error) {
baseURL, err := url.Parse(addr)
if err != nil {
return Client{}, err
}
return Client{
baseURL: baseURL,
auth: auth,
client: http.DefaultClient,
}, nil
}
func (c *Client) do(req *http.Request) (*http.Response, error) {
accessToken, err := c.auth.Token()
func NewClient(ctx context.Context, addr string, options ...Option) (*Client, error) {
opts, err := newOptions(options...)
if err != nil {
return nil, err
}
req.Header.Set("authorization", fmt.Sprintf("bearer %s", accessToken))
return c.client.Do(req)
baseURL, err := url.Parse(addr)
if err != nil {
return nil, err
}
c := Client{
options: opts,
baseURL: baseURL,
}
resp, err := startLogin(*baseURL)
if err != nil {
return nil, err
}
provider, err := oidc.NewProvider(ctx, resp.Issuer)
if err != nil {
return nil, err
}
oidcConfig := &oidc.Config{
ClientID: resp.ClientID,
}
verifier := provider.Verifier(oidcConfig)
config := &oauth2.Config{
ClientID: resp.ClientID,
ClientSecret: c.options.clientSecret,
Endpoint: provider.Endpoint(),
Scopes: []string{"openid", "profile", "email"},
}
var token *oauth2.Token
var rawIdToken interface{}
if c.options.deviceFlow {
token, rawIdToken, err = newDeviceFlowToken(ctx, resp.DeviceAuthURL, provider.Endpoint().TokenURL, resp.ClientID)
if err != nil {
return nil, err
}
} else if c.options.token == "" {
token, err = config.PasswordCredentialsToken(ctx, c.options.username, c.options.password)
if err != nil {
return nil, err
}
rawIdToken = token.Extra("id_token")
} else {
if err = json.Unmarshal([]byte(c.options.token), &token); err != nil {
return nil, err
}
var tokenRaw map[string]interface{}
if err = json.Unmarshal([]byte(c.options.token), &tokenRaw); err != nil {
return nil, err
}
rawIdToken = tokenRaw["id_token"]
}
if rawIdToken == nil {
return nil, fmt.Errorf("no id_token in response")
}
if _, err = verifier.Verify(ctx, rawIdToken.(string)); err != nil {
return nil, err
}
c.client = config.Client(ctx, token)
return &c, nil
}
+2 -2
View File
@@ -31,7 +31,7 @@ func (c *Client) CreateDevice(publicKey string, hostname string) (models.Device,
return models.Device{}, err
}
res, err := c.do(r)
res, err := c.client.Do(r)
if err != nil {
return models.Device{}, err
}
@@ -61,7 +61,7 @@ func (c *Client) GetDevice(deviceID uuid.UUID) (models.Device, error) {
return models.Device{}, err
}
res, err := c.do(r)
res, err := c.client.Do(r)
if err != nil {
return models.Device{}, err
}
+50
View File
@@ -0,0 +1,50 @@
package client
type options struct {
deviceFlow bool
clientSecret string
username string
password string
token string
}
func newOptions(opts ...Option) (*options, error) {
o := &options{}
for _, opt := range opts {
if err := opt(o); err != nil {
return nil, err
}
}
return o, nil
}
type Option func(o *options) error
func WithPasswordGrant(
username string,
password string,
) Option {
return func(o *options) error {
o.deviceFlow = false
o.username = username
o.password = password
return nil
}
}
func WithDeviceFlow() Option {
return func(o *options) error {
o.deviceFlow = true
return nil
}
}
func WithToken(
token string,
) Option {
return func(o *options) error {
o.deviceFlow = false
o.token = token
return nil
}
}
+2 -2
View File
@@ -37,7 +37,7 @@ func (c *Client) CreatePeerInZone(zoneID uuid.UUID, deviceID uuid.UUID, endpoint
return models.Peer{}, err
}
res, err := c.do(r)
res, err := c.client.Do(r)
if err != nil {
return models.Peer{}, err
}
@@ -67,7 +67,7 @@ func (c *Client) GetZonePeers(zoneID uuid.UUID) ([]models.Peer, error) {
return nil, err
}
res, err := c.do(req)
res, err := c.client.Do(req)
if err != nil {
return nil, err
}
+2 -2
View File
@@ -23,7 +23,7 @@ func (c *Client) GetCurrentUser() (models.User, error) {
return models.User{}, err
}
res, err := c.do(r)
res, err := c.client.Do(r)
if err != nil {
return models.User{}, err
}
@@ -60,7 +60,7 @@ func (c *Client) MoveCurrentUserToZone(zoneID uuid.UUID) (models.User, error) {
return models.User{}, err
}
res, err := c.do(req)
res, err := c.client.Do(req)
if err != nil {
return models.User{}, err
}
+34 -2
View File
@@ -30,7 +30,7 @@ func (c *Client) CreateZone(name, description, cidr string, hubZone bool) (model
return models.Zone{}, err
}
res, err := c.do(req)
res, err := c.client.Do(req)
if err != nil {
return models.Zone{}, err
}
@@ -42,7 +42,7 @@ func (c *Client) CreateZone(name, description, cidr string, hubZone bool) (model
}
if res.StatusCode != http.StatusCreated {
return models.Zone{}, fmt.Errorf("failed to create the zone")
return models.Zone{}, fmt.Errorf("failed to create the zone. %s", string(resBody))
}
var data models.Zone
@@ -52,3 +52,35 @@ func (c *Client) CreateZone(name, description, cidr string, hubZone bool) (model
return data, nil
}
// ListZone lists all zones
func (c *Client) ListZones() ([]models.Zone, error) {
dest := c.baseURL.JoinPath(ZONES).String()
req, err := http.NewRequest(http.MethodGet, dest, nil)
if err != nil {
return nil, err
}
res, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
resBody, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to create the zone: %d", res.StatusCode)
}
var data []models.Zone
if err := json.Unmarshal(resBody, &data); err != nil {
return nil, err
}
return data, nil
}
+2 -2
View File
@@ -14,10 +14,10 @@ func NewDatabase(
user string,
password string,
dbname string,
port int,
port string,
sslmode string,
) (*gorm.DB, error) {
dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=%s",
dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=%s",
host, user, password, dbname, port, sslmode)
var db *gorm.DB
connectDb := func() error {
+1 -2
View File
@@ -7,7 +7,6 @@ import (
"io"
"net/http"
"github.com/google/uuid"
"github.com/redhat-et/apex/internal/models"
)
@@ -37,7 +36,7 @@ func (suite *HandlerTestSuite) TestCreateGetDevice() {
assert.NoError(err)
assert.Equal(newDevice.PublicKey, actual.PublicKey)
assert.Equal(uuid.MustParse(TestUserID), actual.UserID)
assert.Equal(TestUserID, actual.UserID)
_, res, err = suite.ServeRequest(
http.MethodGet,
+2 -7
View File
@@ -16,12 +16,7 @@ const AuthUserName string = "_apex.UserName"
func (api *API) CreateUserIfNotExists() gin.HandlerFunc {
return func(c *gin.Context) {
userID := c.GetString(gin.AuthUserKey)
id, err := uuid.Parse(userID)
if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, fmt.Errorf("bad user id"))
return
}
id := c.GetString(gin.AuthUserKey)
userName := c.GetString(AuthUserName)
var user models.User
res := api.db.First(&user, "id = ?", id)
@@ -33,7 +28,7 @@ func (api *API) CreateUserIfNotExists() gin.HandlerFunc {
user.UserName = userName
api.db.Create(&user)
} else {
_ = c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("can't find record for user id %s", userID))
_ = c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("can't find record for user id %s", id))
return
}
}
+1 -1
View File
@@ -8,7 +8,7 @@ import (
// Device is a unique, end-user device.
type Device struct {
Base
UserID uuid.UUID `json:"user_id"`
UserID string `json:"user_id" example:"694aa002-5d19-495e-980b-3d8fd508ea10"`
PublicKey string `gorm:"uniqueIndex" json:"public_key"`
Peers []*Peer `json:"-"`
PeerList []uuid.UUID `gorm:"-" json:"peers" example:"97d5214a-8c51-4772-b492-53de034740c5"`
+8 -2
View File
@@ -1,13 +1,19 @@
package models
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
// User is the owner of a device, and a member of one Zone
type User struct {
Base
// Since the ID comes from the IDP, we have no control over the format...
ID string `gorm:"primary_key;" json:"id" example:"aa22666c-0f57-45cb-a449-16efecc04f2e"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
DeletedAt *time.Time `sql:"index" json:"-"`
Devices []*Device `json:"-"`
DeviceList []uuid.UUID `gorm:"-" json:"devices" example:"4902c991-3dd1-49a6-9f26-d82496c80aff"`
ZoneID uuid.UUID `json:"zone_id" example:"94deb404-c4eb-4097-b59d-76b024ff7867"`
@@ -21,7 +27,7 @@ func (u *User) BeforeCreate(tx *gorm.DB) error {
if u.DeviceList == nil {
u.DeviceList = make([]uuid.UUID, 0)
}
return u.Base.BeforeCreate(tx)
return nil
}
// PatchUser is used to update a user
+36 -62
View File
@@ -1,97 +1,71 @@
package routers
import (
"fmt"
"net/http"
"strings"
"time"
"github.com/MicahParks/keyfunc"
"github.com/coreos/go-oidc"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v4"
log "github.com/sirupsen/logrus"
)
// key for username in gin.Context
const AuthUserName string = "_apex.UserName"
type KeyCloakAuth struct {
jwks *keyfunc.JWKS
}
type Claims struct {
Scope string `json:"scope"`
FullName string `json:"name"`
UserName string `json:"preferred_username"`
GivenName string `json:"given_name"`
FamilyName string `json:"family_name"`
jwt.RegisteredClaims
Subject string `json:"sub"`
}
func NewKeyCloakAuth(url string) (*KeyCloakAuth, error) {
// Create the keyfunc options. Use an error handler that logs. Refresh the JWKS when a JWT signed by an unknown KID
// is found or at the specified interval. Rate limit these refreshes. Timeout the initial JWKS refresh request after
// 10 seconds. This timeout is also used to create the initial context.Context for keyfunc.Get.
options := keyfunc.Options{
RefreshErrorHandler: func(err error) {
log.Printf("There was an error with the jwt.Keyfunc\nError: %s", err.Error())
},
RefreshInterval: time.Hour,
RefreshRateLimit: time.Minute * 5,
RefreshTimeout: time.Second * 10,
RefreshUnknownKID: true,
}
jwks, err := keyfunc.Get(url, options)
if err != nil {
return nil, fmt.Errorf("Failed to create JWKS from resource at the given URL.\nError: %s", err.Error())
}
return &KeyCloakAuth{jwks: jwks}, nil
}
func (a *KeyCloakAuth) AuthFunc() gin.HandlerFunc {
// Naive JWS Key validation
func ValidateJWT(verifier *oidc.IDTokenVerifier, clientIdWeb string, clientIdCli string) func(*gin.Context) {
return func(c *gin.Context) {
header := c.Request.Header.Get("Authorization")
if header == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "no Authorization header present"})
return
}
jwtB64, ok := extractTokenFromAuthHeader(header)
if !ok {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "unable to get token from header"})
return
}
token, err := jwt.ParseWithClaims(jwtB64, &Claims{}, a.jwks.Keyfunc)
if err != nil {
log.Errorf("Failed to parse the JWT. %s", err.Error())
authz := c.Request.Header.Get("Authorization")
if authz == "" {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
if !token.Valid {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "token is not valid"})
parts := strings.Split(authz, " ")
if len(parts) != 2 {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
if claims, ok := token.Claims.(*Claims); ok {
c.Set(gin.AuthUserKey, claims.Subject)
c.Set(AuthUserName, claims.UserName)
// c.Set(AuthUserScope, claims.Scope)
} else {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "unable to extract user info from claims"})
if strings.ToLower(parts[0]) != "bearer" {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
log.Debug("verifying token")
token, err := verifier.Verify(c.Request.Context(), parts[1])
if err != nil {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
for _, audience := range token.Audience {
if audience != clientIdWeb && audience != clientIdCli {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
}
log.Debug("getting claims")
var claims Claims
if err := token.Claims(&claims); err != nil {
c.AbortWithStatus(http.StatusInternalServerError)
return
}
log.Debugf("claims: %+v", claims)
c.Set(gin.AuthUserKey, claims.Subject)
c.Set(AuthUserName, claims.UserName)
// c.Set(AuthUserScope, claims.Scope)
log.Debugf("user-id is %s", claims.Subject)
c.Next()
}
}
func extractTokenFromAuthHeader(val string) (token string, ok bool) {
authHeaderParts := strings.Split(val, " ")
if len(authHeaderParts) != 2 || !strings.EqualFold(authHeaderParts[0], "bearer") {
return "", false
}
return authHeaderParts[1], true
}
+22 -54
View File
@@ -1,74 +1,42 @@
package routers
import (
"encoding/json"
"fmt"
"io"
"context"
"net/http"
"github.com/cenkalti/backoff"
"github.com/gin-contrib/cors"
"github.com/coreos/go-oidc"
"github.com/gin-gonic/gin"
_ "github.com/redhat-et/apex/internal/docs"
"github.com/redhat-et/apex/internal/handlers"
log "github.com/sirupsen/logrus"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
)
func NewRouter(api *handlers.API, keycloakAddress string) (*gin.Engine, error) {
func NewAPIRouter(
ctx context.Context,
api *handlers.API,
clientIdWeb string,
clientIdCli string,
oidcURL string) (*gin.Engine, error) {
r := gin.Default()
log.Debug("Waiting for Keycloak")
connectKeycloak := func() error {
res, err := http.Get(fmt.Sprintf("http://%s:8080/auth/health/ready", keycloakAddress))
if err != nil {
return err
}
body, err := io.ReadAll(res.Body)
if err != nil {
return err
}
var response map[string]interface{}
if err := json.Unmarshal(body, &response); err != nil {
return err
}
if _, ok := response["status"]; !ok {
return fmt.Errorf("no status")
}
if response["status"] != "UP" {
return fmt.Errorf("not ready")
}
return nil
}
err := backoff.Retry(connectKeycloak, backoff.NewExponentialBackOff())
if err != nil {
return nil, err
}
jwksURL := fmt.Sprintf("http://%s:8080/auth/realms/controller/protocol/openid-connect/certs", keycloakAddress)
auth, err := NewKeyCloakAuth(jwksURL)
if err != nil {
return nil, err
}
corsConfig := cors.DefaultConfig()
corsConfig.AllowAllOrigins = true
r.Use(cors.New(corsConfig))
r.GET("/api/health", func(c *gin.Context) {
r.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "ok"})
})
private := r.Group("/api")
provider, err := oidc.NewProvider(ctx, oidcURL)
if err != nil {
return nil, err
}
config := &oidc.Config{
// Client ID checks are skipped since we perform these later
// in the ValidateJWT function
SkipClientIDCheck: true,
}
verifier := provider.Verifier(config)
private := r.Group("/")
{
private.Use(auth.AuthFunc())
private.Use(ValidateJWT(verifier, clientIdWeb, clientIdCli))
private.Use(api.CreateUserIfNotExists())
// Zones
private.GET("/zones", api.ListZones)
File diff suppressed because it is too large Load Diff
-4
View File
@@ -1,4 +0,0 @@
VITE_KEYCLOAK_URL=/auth
VITE_KEYCLOAK_REALM=controller
VITE_KEYCLOAK_CLIENT_ID=front-controller
VITE_CONTROLLER_URL=/api
+18 -50
View File
@@ -1,19 +1,10 @@
import { useState, useRef, useEffect } from 'react';
import {
Admin,
AuthProvider,
DataProvider,
Resource,
fetchUtils
} from 'react-admin';
import simpleRestProvider from 'ra-data-simple-rest';
// Auth
import Keycloak, {
KeycloakConfig,
KeycloakTokenParsed,
KeycloakInitOptions,
} from 'keycloak-js';
import { keycloakAuthProvider, httpClient } from 'ra-keycloak';
import { goOidcAgentAuthProvider } from './providers/AuthProvider';
// icons
import DeviceIcon from '@mui/icons-material/Devices';
@@ -26,56 +17,33 @@ import { PeerShow, PeerList } from "./pages/Peers";
import { UserShow, UserList } from "./pages/Users";
import { DeviceList, DeviceShow } from "./pages/Devices";
import Dashboard from './pages/Dashboard';
import LoginPage from './pages/Login';
import Layout from "./layout/Layout";
const config : KeycloakConfig = {
url: import.meta.env.VITE_KEYCLOAK_URL,
realm: import.meta.env.VITE_KEYCLOAK_REALM,
clientId: import.meta.env.VITE_KEYCLOAK_CLIENT_ID,
const fetchJson = (url: URL, options: any = {}) => {
// Includes the encrypted session cookie in requests to the API
options.credentials = "include";
return fetchUtils.fetchJson(url, options);
};
const initOptions: KeycloakInitOptions = { onLoad: 'login-required' };
const getPermissions = (decoded: KeycloakTokenParsed) => {
const roles = decoded?.realm_access?.roles;
if (!roles) {
return false;
}
if (roles.includes('admin')) return 'admin';
if (roles.includes('user')) return 'user';
return false;
};
const backend = `${window.location.protocol}//api.${window.location.host}`;
const authProvider = goOidcAgentAuthProvider(backend);
const dataProvider = simpleRestProvider(
`${backend}/api`,
fetchJson,
'X-Total-Count',
);
const App = () => {
const [keycloak, setKeycloak] = useState<Keycloak | undefined>(undefined);
const authProvider = useRef<AuthProvider | undefined>(undefined);
const dataProvider = useRef<DataProvider | undefined>(undefined);
useEffect(() => {
const initKeyCloakClient = async () => {
const keycloakClient = new Keycloak(config);
await keycloakClient.init(initOptions);
authProvider.current = keycloakAuthProvider(keycloakClient, {
onPermissions: getPermissions,
});
dataProvider.current = simpleRestProvider(import.meta.env.VITE_CONTROLLER_URL, httpClient(keycloakClient), 'X-Total-Count');
setKeycloak(keycloakClient);
};
if (!keycloak) {
initKeyCloakClient();
}
}, [keycloak]);
// hide the admin until the dataProvider and authProvider are ready
if (!keycloak) return <p>Loading...</p>;
return (
<Admin
dashboard={Dashboard}
authProvider={authProvider.current}
dataProvider={dataProvider.current}
authProvider={authProvider}
dataProvider={dataProvider}
title="Controller"
layout={Layout}
loginPage={LoginPage}
requireAuth
>
<Resource name="users" list={UserList} show={UserShow} icon={UserIcon} recordRepresentation={(record) => `${record.username}`} />
<Resource name="devices" list={DeviceList} show={DeviceShow} icon={DeviceIcon} recordRepresentation={(record) => `${record.hostname}`} />
-10
View File
@@ -1,10 +0,0 @@
interface ImportMetaEnv {
readonly VITE_KEYCLOAK_URL: string
readonly VITE_KEYCLOAK_REALM: string
readonly VITE_KEYCLOAK_CLIENT_ID: string
readonly VITE_CONTROLLER_URL: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
+84
View File
@@ -0,0 +1,84 @@
import { useState, useEffect } from "react";
import { styled } from "@mui/material/styles";
import { useLogin, Form, Login, LoginFormProps } from "react-admin";
import { CardContent, Button, CircularProgress } from "@mui/material";
const LoginForm = (props: LoginFormProps) => {
const { redirectTo, className } = props;
const [loading, setLoading] = useState(false);
const login = useLogin();
useEffect(() => {
const { searchParams } = new URL(window.location.href);
const code = searchParams.get("code");
const state = searchParams.get("state");
// If code is present, we came back from the provider
if (code && state) {
console.log("handling return from login");
setLoading(true);
login({ code, state });
}
}, [login]);
const handleLogin = () => {
console.log("login button pressed");
setLoading(true);
login({}); // Do not provide code, just trigger the redirection
};
return (
<StyledForm
onSubmit={handleLogin}
mode="onChange"
noValidate
className={className}
>
<CardContent className={LoginFormClasses.content}>
<Button
variant="contained"
type="submit"
color="primary"
disabled={loading}
fullWidth
className={LoginFormClasses.button}
>
{loading && <CircularProgress size={18} thickness={2} />}
Login
</Button>
</CardContent>
</StyledForm>
);
};
const PREFIX = "RaLoginForm";
export const LoginFormClasses = {
content: `${PREFIX}-content`,
button: `${PREFIX}-button`,
icon: `${PREFIX}-icon`,
};
const StyledForm = styled(Form, {
name: PREFIX,
overridesResolver: (props, styles) => styles.root,
})(({ theme }) => ({
[`& .${LoginFormClasses.content}`]: {
width: 300,
},
[`& .${LoginFormClasses.button}`]: {
marginTop: theme.spacing(2),
},
[`& .${LoginFormClasses.icon}`]: {
margin: theme.spacing(0.3),
},
}));
const LoginPage = () => (
<Login
backgroundImage="https://source.unsplash.com/9wg5jCEPBsw"
children={<LoginForm />}
/>
);
export default LoginPage;
+128
View File
@@ -0,0 +1,128 @@
import { AuthProvider, UserIdentity } from "react-admin";
const cleanup = () => {
// Remove the ?code&state from the URL
window.history.replaceState(
{},
window.document.title,
window.location.origin
);
};
export const goOidcAgentAuthProvider = (api: string): AuthProvider => ({
login: async (params = {}) => {
console.log("login");
// 1. Redirect to the issuer to ask authentication
if (!params.code || !params.state) {
const request = new Request(`${api}/login/start`, {
method: "POST",
credentials: "include",
});
try {
const response = await fetch(request);
const data = await response.json();
if (response && data) {
window.location.replace(data.authorization_request_url);
}
} catch (err: any) {
throw new Error("Network error");
}
}
// 2. We came back from the issuer with ?code infos in query params
const request = new Request(`${api}/login/end`, {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ request_url: window.location.href }),
});
try {
const response = await fetch(request);
const data = await response.json();
if (response && data) {
cleanup();
return data.handled && data.logged_in
? Promise.resolve()
: Promise.reject();
}
} catch (err: any) {
cleanup();
throw new Error(err.statusText);
}
},
logout: async () => {
console.log("logout");
const request = new Request(`${api}/logout`, {
method: "post",
credentials: "include",
});
try {
const response = await fetch(request);
if (response.status === 401) {
return Promise.resolve();
}
const data = await response.json();
if (response && data) {
window.location.replace(data.logout_url);
}
} catch (err: any) {
throw new Error(err.statusText);
}
},
checkError: async (error: any) => {
console.log("checkError");
const status = error.status;
if (status === 401) {
return Promise.reject();
}
return Promise.resolve();
},
checkAuth: async () => {
console.log("checkAuth");
console.log(document.cookie);
const request = new Request(`${api}/login/end`, {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ request_url: window.location.href }),
});
try {
const response = await fetch(request);
const data = await response.json();
if (response && data) {
return data.logged_in ? Promise.resolve() : Promise.reject();
}
} catch (err: any) {
throw new Error(err.statusText);
}
},
getPermissions: async () => {
console.log("getPermissions");
// TODO: Add a callback so people can decode the claims
return Promise.resolve();
},
getIdentity: async (): Promise<UserIdentity> => {
console.log("getIdentity");
const request = new Request(`${api}/user_info`, {
credentials: "include",
});
var id;
try {
const response = await fetch(request);
const data = await response.json();
if (response && data) {
id = {
id: data.subject,
fullName: data.preferred_username,
avatar: data.picture,
} as UserIdentity;
}
} catch (err: any) {
throw new Error(err.statusText);
}
if (id) {
return Promise.resolve(id);
}
return Promise.reject();
},
});
+12
View File
@@ -485,6 +485,11 @@
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.2.0.tgz#76467a94aa888aeb22aafa43eb6ff889df3a5a7f"
integrity sha512-rBevIsj2nclStJ7AxTdfsa3ovHb1H+qApwrxcTVo+NNdeJiB9V75hsKfrkG5AwNcRUNxrPPiScGYCNmLMoh8pg==
"@fortawesome/fontawesome-common-types@6.2.1":
version "6.2.1"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.2.1.tgz#411e02a820744d3f7e0d8d9df9d82b471beaa073"
integrity sha512-Sz07mnQrTekFWLz5BMjOzHl/+NooTdW8F8kDQxjWwbpOJcnoSg4vUDng8d/WR1wOxM0O+CY9Zw0nR054riNYtQ==
"@fortawesome/fontawesome-svg-core@~1 || ~6":
version "6.2.0"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.2.0.tgz#11856eaf4dd1d865c442ddea1eed8ee855186ba2"
@@ -499,6 +504,13 @@
dependencies:
"@fortawesome/fontawesome-common-types" "6.2.0"
"@fortawesome/free-solid-svg-icons@^6.2.0":
version "6.2.1"
resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.2.1.tgz#2290ea5adcf1537cbd0c43de6feb38af02141d27"
integrity sha512-oKuqrP5jbfEPJWTij4sM+/RvgX+RMFwx3QZCZcK9PrBDgxC35zuc7AOFsyMjMd/PIFPeB2JxyqDr5zs/DZFPPw==
dependencies:
"@fortawesome/fontawesome-common-types" "6.2.1"
"@fortawesome/react-fontawesome@^0.2.0":
version "0.2.0"
resolved "https://registry.yarnpkg.com/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.0.tgz#d90dd8a9211830b4e3c08e94b63a0ba7291ddcf4"