Implement very rough skeleton of the start of a supervisor server

- This is just stab at a starting place because it felt easier to
  put something down on paper than to keep staring at a blank page
This commit is contained in:
Ryan Richard 2020-10-05 17:28:19 -07:00
parent 7eed7ba19a
commit 76bd462cf8
8 changed files with 500 additions and 3 deletions

View File

@ -21,6 +21,7 @@ COPY hack ./hack
# Build the executable binary (CGO_ENABLED=0 means static linking) # Build the executable binary (CGO_ENABLED=0 means static linking)
RUN mkdir out \ RUN mkdir out \
&& CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(hack/get-ldflags.sh)" -o out ./cmd/pinniped-server/... \ && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(hack/get-ldflags.sh)" -o out ./cmd/pinniped-server/... \
&& CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(hack/get-ldflags.sh)" -o out ./cmd/pinniped-supervisor/... \
&& CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o out ./cmd/local-user-authenticator/... && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o out ./cmd/local-user-authenticator/...
# Use a runtime image based on Debian slim # Use a runtime image based on Debian slim
@ -28,6 +29,7 @@ FROM debian:10.5-slim
# Copy the binaries from the build-env stage # Copy the binaries from the build-env stage
COPY --from=build-env /work/out/pinniped-server /usr/local/bin/pinniped-server COPY --from=build-env /work/out/pinniped-server /usr/local/bin/pinniped-server
COPY --from=build-env /work/out/pinniped-supervisor /usr/local/bin/pinniped-supervisor
COPY --from=build-env /work/out/local-user-authenticator /usr/local/bin/local-user-authenticator COPY --from=build-env /work/out/local-user-authenticator /usr/local/bin/local-user-authenticator
# Document the port # Document the port

View File

@ -0,0 +1,186 @@
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package main
import (
"context"
"fmt"
"io/ioutil"
"net"
"net/http"
"os"
"os/signal"
"time"
kubeinformers "k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/pkg/version"
"k8s.io/client-go/rest"
restclient "k8s.io/client-go/rest"
"k8s.io/component-base/logs"
"k8s.io/klog/v2"
"sigs.k8s.io/yaml"
"go.pinniped.dev/internal/controller/supervisorconfig"
"go.pinniped.dev/internal/controllerlib"
"go.pinniped.dev/internal/downward"
)
const (
singletonWorker = 1
defaultResyncInterval = 3 * time.Minute
)
type helloWorld struct{}
func (w *helloWorld) start(ctx context.Context, l net.Listener) error {
server := http.Server{
Handler: w,
}
errCh := make(chan error)
go func() {
errCh <- server.Serve(l)
}()
go func() {
select {
case err := <-errCh:
klog.InfoS("server exited", "err", err)
case <-ctx.Done():
klog.InfoS("server context cancelled", "err", ctx.Err())
if err := server.Shutdown(context.Background()); err != nil {
klog.InfoS("server shutdown failed", "err", err)
}
}
}()
return nil
}
func (w *helloWorld) ServeHTTP(rsp http.ResponseWriter, req *http.Request) {
// TODO this is just a placeholder to allow manually testing that this is reachable; we don't want a hello world endpoint
defer req.Body.Close()
_, _ = fmt.Fprintf(rsp, "Hello, world!")
}
func waitForSignal() os.Signal {
signalCh := make(chan os.Signal, 1)
signal.Notify(signalCh, os.Interrupt)
return <-signalCh
}
func startControllers(
ctx context.Context,
kubeClient kubernetes.Interface,
kubeInformers kubeinformers.SharedInformerFactory,
serverInstallationNamespace string,
staticConfig StaticConfig,
) {
// Create controller manager.
controllerManager := controllerlib.
NewManager().
WithController(
supervisorconfig.NewDynamicConfigWatcherController(
serverInstallationNamespace,
staticConfig.NamesConfig.DynamicConfigMap,
kubeClient,
kubeInformers.Core().V1().ConfigMaps(),
controllerlib.WithInformer,
),
singletonWorker,
)
kubeInformers.Start(ctx.Done())
go controllerManager.Start(ctx)
}
func newK8sClient() (kubernetes.Interface, error) {
kubeConfig, err := restclient.InClusterConfig()
if err != nil {
return nil, fmt.Errorf("could not load in-cluster configuration: %w", err)
}
// Connect to the core Kubernetes API.
kubeClient, err := kubernetes.NewForConfig(kubeConfig)
if err != nil {
return nil, fmt.Errorf("could not load in-cluster configuration: %w", err)
}
return kubeClient, nil
}
func run(serverInstallationNamespace string, staticConfig StaticConfig) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
kubeClient, err := newK8sClient()
if err != nil {
return fmt.Errorf("cannot create k8s client: %w", err)
}
kubeInformers := kubeinformers.NewSharedInformerFactoryWithOptions(
kubeClient,
defaultResyncInterval,
kubeinformers.WithNamespace(serverInstallationNamespace),
)
startControllers(ctx, kubeClient, kubeInformers, serverInstallationNamespace, staticConfig)
//nolint: gosec // Intentionally binding to all network interfaces.
l, err := net.Listen("tcp", ":80")
if err != nil {
return fmt.Errorf("cannot create listener: %w", err)
}
defer l.Close()
helloHandler := &helloWorld{}
err = helloHandler.start(ctx, l)
if err != nil {
return fmt.Errorf("cannot start webhook: %w", err)
}
klog.InfoS("supervisor is ready", "address", l.Addr().String())
gotSignal := waitForSignal()
klog.InfoS("supervisor exiting", "signal", gotSignal)
return nil
}
type StaticConfig struct {
NamesConfig NamesConfigSpec `json:"names"`
}
type NamesConfigSpec struct {
DynamicConfigMap string `json:"dynamicConfigMap"`
}
func main() {
logs.InitLogs()
defer logs.FlushLogs()
klog.Infof("Running %s at %#v", rest.DefaultKubernetesUserAgent(), version.Get())
klog.Infof("Command-line arguments were: %s %s %s", os.Args[0], os.Args[1], os.Args[2])
// Discover in which namespace we are installed.
podInfo, err := downward.Load(os.Args[1])
if err != nil {
klog.Fatal(fmt.Errorf("could not read pod metadata: %w", err))
}
// Read static config.
data, err := ioutil.ReadFile(os.Args[2])
if err != nil {
klog.Fatal(fmt.Errorf("read file: %w", err))
}
var staticConfig StaticConfig
if err := yaml.Unmarshal(data, &staticConfig); err != nil {
klog.Fatal(fmt.Errorf("decode yaml: %w", err))
}
if err := run(podInfo.Namespace, staticConfig); err != nil {
klog.Fatal(err)
}
}

View File

@ -0,0 +1,41 @@
# Deploying the Pinniped Supervisor
## What is the Pinniped Supervisor?
The Pinniped Supervisor app is a component of the Pinniped OIDC and Cluster Federation solutions.
It can be deployed when those features are needed.
## Installing the Latest Version with Default Options
```bash
kubectl apply -f https://github.com/vmware-tanzu/pinniped/releases/latest/download/install-supervisor.yaml
```
## Installing an Older Version with Default Options
Choose your preferred [release](https://github.com/vmware-tanzu/pinniped/releases) version number
and use it to replace the version number in the URL below.
```bash
# Replace v0.3.0 with your preferred version in the URL below
kubectl apply -f https://github.com/vmware-tanzu/pinniped/releases/download/v0.3.0/install-supervisor.yaml
```
## Installing with Custom Options
Creating your own deployment YAML file requires `ytt` from [Carvel](https://carvel.dev/) to template the YAML files
in the [deploy-supervisor](../deploy-supervisor) directory.
Either [install `ytt`](https://get-ytt.io/) or use the [container image from Dockerhub](https://hub.docker.com/r/k14s/image/tags).
1. `git clone` this repo and `git checkout` the release version tag of the release that you would like to deploy.
1. The configuration options are in [deploy-supervisor/values.yml](values.yaml).
Fill in the values in that file, or override those values using additional `ytt` command-line options in
the command below. Use the release version tag as the `image_tag` value.
2. In a terminal, cd to this `deploy-supervisor` directory
3. To generate the final YAML files, run `ytt --file .`
4. Deploy the generated YAML using your preferred deployment tool, such as `kubectl` or [`kapp`](https://get-kapp.io/).
For example: `ytt --file . | kapp deploy --yes --app pinniped-supervisor --diff-changes --file -`
## Configuring After Installing
TODO: Provide some instructions here.

View File

@ -0,0 +1,146 @@
#! Copyright 2020 the Pinniped contributors. All Rights Reserved.
#! SPDX-License-Identifier: Apache-2.0
#@ load("@ytt:data", "data")
---
apiVersion: v1
kind: Namespace
metadata:
name: #@ data.values.namespace
labels:
name: #@ data.values.namespace
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: #@ data.values.app_name
namespace: #@ data.values.namespace
---
apiVersion: v1
kind: ConfigMap
metadata:
name: #@ data.values.app_name + "-static-config"
namespace: #@ data.values.namespace
labels:
app: #@ data.values.app_name
data:
#@yaml/text-templated-strings
pinniped.yaml: |
names:
dynamicConfigMap: (@= data.values.app_name + "-dynamic-config" @)
---
#@ if data.values.image_pull_dockerconfigjson and data.values.image_pull_dockerconfigjson != "":
apiVersion: v1
kind: Secret
metadata:
name: image-pull-secret
namespace: #@ data.values.namespace
labels:
app: #@ data.values.app_name
type: kubernetes.io/dockerconfigjson
data:
.dockerconfigjson: #@ data.values.image_pull_dockerconfigjson
#@ end
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: #@ data.values.app_name
namespace: #@ data.values.namespace
labels:
app: #@ data.values.app_name
spec:
replicas: #@ data.values.replicas
selector:
matchLabels:
app: #@ data.values.app_name
template:
metadata:
labels:
app: #@ data.values.app_name
spec:
serviceAccountName: #@ data.values.app_name
#@ if data.values.image_pull_dockerconfigjson and data.values.image_pull_dockerconfigjson != "":
imagePullSecrets:
- name: image-pull-secret
#@ end
containers:
- name: pinniped-supervisor
#@ if data.values.image_digest:
image: #@ data.values.image_repo + "@" + data.values.image_digest
#@ else:
image: #@ data.values.image_repo + ":" + data.values.image_tag
#@ end
imagePullPolicy: IfNotPresent
command: #! override the default entrypoint
- /usr/local/bin/pinniped-supervisor
args:
- /etc/podinfo #! TODO proper flag parsing instead of positional
- /etc/config/pinniped.yaml #! TODO proper flag parsing instead of positional
resources:
requests:
memory: "128Mi"
volumeMounts:
- name: config-volume
mountPath: /etc/config
- name: podinfo
mountPath: /etc/podinfo
#! livenessProbe:
#! httpGet:
#! path: /healthz
#! port: 443
#! scheme: HTTPS
#! initialDelaySeconds: 2
#! timeoutSeconds: 15
#! periodSeconds: 10
#! failureThreshold: 5
#! readinessProbe:
#! httpGet:
#! path: /healthz
#! port: 443
#! scheme: HTTPS
#! initialDelaySeconds: 2
#! timeoutSeconds: 3
#! periodSeconds: 10
#! failureThreshold: 3
volumes:
- name: config-volume
configMap:
name: #@ data.values.app_name + "-static-config"
- name: podinfo
downwardAPI:
items:
- path: "labels"
fieldRef:
fieldPath: metadata.labels
- path: "namespace"
fieldRef:
fieldPath: metadata.namespace
#! This will help make sure our multiple pods run on different nodes, making
#! our deployment "more" "HA".
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 50
podAffinityTerm:
labelSelector:
matchLabels:
app: #@ data.values.app_name
topologyKey: kubernetes.io/hostname
---
apiVersion: v1
kind: Service
metadata:
name: #@ data.values.app_name
namespace: #@ data.values.namespace
labels:
app: #@ data.values.app_name
spec:
type: ClusterIP
selector:
app: #@ data.values.app_name
ports:
- protocol: TCP
port: 80
targetPort: 80

View File

@ -0,0 +1,34 @@
#! Copyright 2020 the Pinniped contributors. All Rights Reserved.
#! SPDX-License-Identifier: Apache-2.0
#@ load("@ytt:data", "data")
#! Give permission to various objects within the app's own namespace
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: #@ data.values.app_name
namespace: #@ data.values.namespace
labels:
app: #@ data.values.app_name
rules:
- apiGroups: [""]
resources: [configmaps]
verbs: [get, list, watch]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: #@ data.values.app_name
namespace: #@ data.values.namespace
labels:
app: #@ data.values.app_name
subjects:
- kind: ServiceAccount
name: #@ data.values.app_name
namespace: #@ data.values.namespace
roleRef:
kind: Role
name: #@ data.values.app_name
apiGroup: rbac.authorization.k8s.io

View File

@ -0,0 +1,22 @@
#! Copyright 2020 the Pinniped contributors. All Rights Reserved.
#! SPDX-License-Identifier: Apache-2.0
#@data/values
---
app_name: pinniped-supervisor
namespace: pinniped-supervisor
#! Specify how many replicas of the Pinniped server to run.
replicas: 2
#! Specify either an image_digest or an image_tag. If both are given, only image_digest will be used.
image_repo: docker.io/getpinniped/pinniped-server
image_digest: #! e.g. sha256:f3c4fdfd3ef865d4b97a1fd295d94acc3f0c654c46b6f27ffad5cf80216903c8
image_tag: latest
#! Specifies a secret to be used when pulling the above container image.
#! Can be used when the above image_repo is a private registry.
#! Typically the value would be the output of: kubectl create secret docker-registry x --docker-server=https://example.io --docker-username="USERNAME" --docker-password="PASSWORD" --dry-run=client -o json | jq -r '.data[".dockerconfigjson"]'
#! Optional.
image_pull_dockerconfigjson: #! e.g. {"auths":{"https://registry.example.com":{"username":"USERNAME","password":"PASSWORD","auth":"BASE64_ENCODED_USERNAME_COLON_PASSWORD"}}}

View File

@ -174,15 +174,29 @@ kubectl create secret generic "$test_username" \
--output yaml | --output yaml |
kubectl apply -f - kubectl apply -f -
#
# Deploy the Pinniped Supervisor
#
pushd deploy-supervisor >/dev/null
log_note "Deploying the Pinniped Supervisor app to the cluster..."
ytt --file . \
--data-value "image_repo=$registry_repo" \
--data-value "image_tag=$tag" >"$manifest"
kapp deploy --yes --app "pinniped-supervisor" --diff-changes --file "$manifest"
popd >/dev/null
#
# Deploy Pinniped
#
app_name="pinniped" app_name="pinniped"
namespace="integration" namespace="integration"
webhook_url="https://local-user-authenticator.local-user-authenticator.svc/authenticate" webhook_url="https://local-user-authenticator.local-user-authenticator.svc/authenticate"
webhook_ca_bundle="$(kubectl get secret local-user-authenticator-tls-serving-certificate --namespace local-user-authenticator -o 'jsonpath={.data.caCertificate}')" webhook_ca_bundle="$(kubectl get secret local-user-authenticator-tls-serving-certificate --namespace local-user-authenticator -o 'jsonpath={.data.caCertificate}')"
discovery_url="$(TERM=dumb kubectl cluster-info | awk '/Kubernetes master/ {print $NF}')" discovery_url="$(TERM=dumb kubectl cluster-info | awk '/Kubernetes master/ {print $NF}')"
#
# Deploy Pinniped
#
pushd deploy >/dev/null pushd deploy >/dev/null
log_note "Deploying the Pinniped app to the cluster..." log_note "Deploying the Pinniped app to the cluster..."

View File

@ -0,0 +1,52 @@
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package supervisorconfig
import (
corev1informers "k8s.io/client-go/informers/core/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/klog/v2"
pinnipedcontroller "go.pinniped.dev/internal/controller"
"go.pinniped.dev/internal/controllerlib"
)
type dynamicConfigWatcherController struct {
k8sClient kubernetes.Interface
configMapInformer corev1informers.ConfigMapInformer
}
func NewDynamicConfigWatcherController(
serverInstallationNamespace string,
configMapName string,
k8sClient kubernetes.Interface,
configMapInformer corev1informers.ConfigMapInformer,
withInformer pinnipedcontroller.WithInformerOptionFunc,
) controllerlib.Controller {
return controllerlib.New(
controllerlib.Config{
Name: "DynamicConfigWatcherController",
Syncer: &dynamicConfigWatcherController{
k8sClient: k8sClient,
configMapInformer: configMapInformer,
},
},
withInformer(
configMapInformer,
pinnipedcontroller.NameAndNamespaceExactMatchFilterFactory(configMapName, serverInstallationNamespace),
controllerlib.InformerOption{},
),
)
}
// Sync implements controllerlib.Syncer.
func (c *dynamicConfigWatcherController) Sync(ctx controllerlib.Context) error {
// TODO Watch the configmap to find the issuer name, ingress url, etc.
// TODO Update some kind of in-memory representation of the configuration so the discovery endpoint can use it.
// TODO The discovery endpoint would return an error until all missing configuration options are filled in.
klog.InfoS("DynamicConfigWatcherController sync finished")
return nil
}