Merge pull request #12 from jakkab/scaffold-controller

First controller version
This commit is contained in:
Tomasz Smelcerz 2019-08-29 13:09:02 +02:00 committed by GitHub
commit 0cecfbbe41
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 2673 additions and 38 deletions

View File

@ -1,9 +1,5 @@
version: 2
jobs:
aloha:
machine: true
steps:
- run: echo "Aloha! ;-)"
build:
docker:
- image: circleci/golang:1.12
@ -145,36 +141,31 @@ workflows:
version: 2
"test, build and release":
jobs:
- aloha:
- build:
filters:
tags:
only: /.*/
# ENABLE IT once controller with make target will be created
# - build:
# filters:
# tags:
# only: /.*/
# - test-integration:
# filters:
# tags:
# only: /.*/
# - test:
# filters:
# tags:
# only: /.*/
# - release:
# requires:
# - test
# filters:
# tags:
# only: /.*/
# branches:
# ignore: /.*/
# - release-changelog:
# requires:
# - release
# filters:
# tags:
# only: /.*/
# branches:
# ignore: /.*/
- test-integration:
filters:
tags:
only: /.*/
- test:
filters:
tags:
only: /.*/
- release:
requires:
- test
filters:
tags:
only: /.*/
branches:
ignore: /.*/
- release-changelog:
requires:
- release
filters:
tags:
only: /.*/
branches:
ignore: /.*/

26
.gitignore vendored Normal file
View File

@ -0,0 +1,26 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
bin
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Kubernetes Generated files - skip generated files, except for vendored files
!vendor/**/zz_generated.*
# editor and IDE paraphernalia
.idea
*.swp
*.swo
*~
config/default/manager_image_patch.yaml-e

26
Dockerfile Normal file
View File

@ -0,0 +1,26 @@
# Build the manager binary
FROM golang:1.12.5 as builder
WORKDIR /workspace
# Copy the Go Modules manifests
COPY go.mod go.mod
COPY go.sum go.sum
# cache deps before building and copying source so that we don't need to re-download as much
# and so that source changes don't invalidate our downloaded layer
RUN go mod download
# Copy the go source
COPY main.go main.go
COPY api/ api/
COPY controllers/ controllers/
COPY hydra/ hydra/
# Build
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -a -o manager main.go
# Use distroless as minimal base image to package the manager binary
# Refer to https://github.com/GoogleContainerTools/distroless for more details
FROM gcr.io/distroless/static:latest
WORKDIR /
COPY --from=builder /workspace/manager .
ENTRYPOINT ["/manager"]

69
Makefile Normal file
View File

@ -0,0 +1,69 @@
# Image URL to use all building/pushing image targets
IMG ?= controller:latest
# Produce CRDs that work back to Kubernetes 1.11 (no version conversion)
CRD_OPTIONS ?= "crd:trivialVersions=true"
all: manager
# Run tests
test: generate fmt vet manifests
go test ./api/... ./controllers/... ./hydra... -coverprofile cover.out
# Run integration tests on local KIND cluster
# TODO: modify once integration tests have been implemented
test-integration:
ginkgo -v ./controllers/...
# Build manager binary
manager: generate fmt vet
go build -o bin/manager main.go
# Run against the configured Kubernetes cluster in ~/.kube/config
run: generate fmt vet
go run ./main.go --hydra-url ${HYDRA_URL}
# Install CRDs into a cluster
install: manifests
kubectl apply -f config/crd/bases
# Deploy controller in the configured Kubernetes cluster in ~/.kube/config
deploy: manifests
kubectl apply -f config/crd/bases
kustomize build config/default | kubectl apply -f -
# Generate manifests e.g. CRD, RBAC etc.
manifests: controller-gen
$(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases
# Run go fmt against code
fmt:
go fmt ./...
# Run go vet against code
vet:
go vet ./...
# Generate code
generate: controller-gen
$(CONTROLLER_GEN) object:headerFile=./hack/boilerplate.go.txt paths=./api/...
# Build the docker image
docker-build: test
docker build . -t ${IMG}
@echo "updating kustomize image patch file for manager resource"
sed -i'' -e 's@image: .*@image: '"${IMG}"'@' ./config/default/manager_image_patch.yaml
# Push the docker image
docker-push:
docker push ${IMG}
# find or download controller-gen
# download controller-gen if necessary
controller-gen:
ifeq (, $(shell which controller-gen))
go get sigs.k8s.io/controller-tools/cmd/controller-gen@v0.2.0-beta.2
CONTROLLER_GEN=$(shell go env GOPATH)/bin/controller-gen
else
CONTROLLER_GEN=$(shell which controller-gen)
endif

7
PROJECT Normal file
View File

@ -0,0 +1,7 @@
version: "2"
domain: ory.sh
repo: github.com/ory/hydra-maester
resources:
- group: hydra
version: v1alpha1
kind: OAuth2Client

View File

@ -1,10 +1,31 @@
# hydra-maester
# Hydra-maester
This project contains a Kubernetes controller that uses Custom Resources to manage Hydra Oauth2 clients.
The project is based on [Kubebuilder](https://github.com/kubernetes-sigs/kubebuilder)
ORY Hydra Maester watches for instances of `oauth2clients.oathkeeper.ory.sh/v1alpha1` and creates, updates, or deletes corresponding OAuth2 clients by communicating with ORY Hydra API.
The project is based on [Kubebuilder](https://github.com/kubernetes-sigs/kubebuilder).
## Prerequisites
- recent version of Go language with support for modules (e.g: 1.12.6)
- make
- kubectl
- kustomize
- [ginkgo](https://onsi.github.io/ginkgo/) for local integration testing
- access to K8s environment: minikube or a remote K8s cluster
# Design
Take a look at [Design Readme](./docs/README.md)
## Design
Take a look at [Design Readme](./docs/README.md).
## How to use it
- `make test` to run tests
- `make test-integration` to run integration tests
- `make install` to generate CRD file from go sources and install it on the cluster
- `export HYDRA_URL={HYDRA_SERVICE_URL} && make run` to run the controller
To deploy the controller, edit the value of the ```--hydra-url``` argument in the [manager.yaml](config/manager/manager.yaml) file and run ```make deploy```.

View File

@ -0,0 +1,35 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package v1alpha1 contains API Schema definitions for the hydra v1alpha1 API group
// +kubebuilder:object:generate=true
// +groupName=hydra.ory.sh
package v1alpha1
import (
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/scheme"
)
var (
// GroupVersion is group version used to register these objects
GroupVersion = schema.GroupVersion{Group: "hydra.ory.sh", Version: "v1alpha1"}
// SchemeBuilder is used to add go types to the GroupVersionKind scheme
SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
// AddToScheme adds the types in this group-version to the given scheme.
AddToScheme = SchemeBuilder.AddToScheme
)

View File

@ -0,0 +1,114 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1alpha1
import (
"github.com/ory/hydra-maester/hydra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// OAuth2ClientSpec defines the desired state of OAuth2Client
type OAuth2ClientSpec struct {
// +kubebuilder:validation:MaxItems=4
// +kubebuilder:validation:MinItems=1
//
// GrantTypes is an array of grant types the client is allowed to use.
GrantTypes []GrantType `json:"grantTypes"`
// +kubebuilder:validation:MaxItems=3
// +kubebuilder:validation:MinItems=1
//
// ResponseTypes is an array of the OAuth 2.0 response type strings that the client can
// use at the authorization endpoint.
ResponseTypes []ResponseType `json:"responseTypes,omitempty"`
// +kubebuilder:validation:Pattern=([a-zA-Z0-9\.\*]+\s?)+
//
// Scope is a string containing a space-separated list of scope values (as
// described in Section 3.3 of OAuth 2.0 [RFC6749]) that the client
// can use when requesting access tokens.
Scope string `json:"scope"`
}
// +kubebuilder:validation:Enum=client_credentials;authorization_code;implicit;refresh_token
// GrantType represents an OAuth 2.0 grant type
type GrantType string
// +kubebuilder:validation:Enum=id_token;code;token
// ResponseType represents an OAuth 2.0 response type strings
type ResponseType string
// OAuth2ClientStatus defines the observed state of OAuth2Client
type OAuth2ClientStatus struct {
// Secret points to the K8s secret that contains this client's id and password
Secret *string `json:"secret,omitempty"`
// ClientID is the id for this client.
ClientID *string `json:"clientID,omitempty"`
// ObservedGeneration represents the most recent generation observed by the daemon set controller.
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// OAuth2Client is the Schema for the oauth2clients API
type OAuth2Client struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec OAuth2ClientSpec `json:"spec,omitempty"`
Status OAuth2ClientStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// OAuth2ClientList contains a list of OAuth2Client
type OAuth2ClientList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []OAuth2Client `json:"items"`
}
func init() {
SchemeBuilder.Register(&OAuth2Client{}, &OAuth2ClientList{})
}
// ToOAuth2ClientJSON converts an OAuth2Client into a OAuth2ClientJSON object that represents an OAuth2 client digestible by ORY Hydra
func (c *OAuth2Client) ToOAuth2ClientJSON() *hydra.OAuth2ClientJSON {
return &hydra.OAuth2ClientJSON{
Name: c.Name,
ClientID: c.Status.ClientID,
GrantTypes: grantToStringSlice(c.Spec.GrantTypes),
ResponseTypes: responseToStringSlice(c.Spec.ResponseTypes),
Scope: c.Spec.Scope,
}
}
func responseToStringSlice(rt []ResponseType) []string {
var output = make([]string, len(rt))
for i, elem := range rt {
output[i] = string(elem)
}
return output
}
func grantToStringSlice(gt []GrantType) []string {
var output = make([]string, len(gt))
for i, elem := range gt {
output[i] = string(elem)
}
return output
}

View File

@ -0,0 +1,129 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1alpha1
import (
"fmt"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/net/context"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
)
var (
k8sClient client.Client
cfg *rest.Config
testEnv *envtest.Environment
key types.NamespacedName
created, fetched *OAuth2Client
createErr, getErr, deleteErr error
)
func TestCreateAPI(t *testing.T) {
runEnv(t)
defer stopEnv(t)
t.Run("should handle an object properly", func(t *testing.T) {
key = types.NamespacedName{
Name: "foo",
Namespace: "default",
}
t.Run("by creating an API object if it meets CRD requirements", func(t *testing.T) {
resetTestClient()
createErr = k8sClient.Create(context.TODO(), created)
require.NoError(t, createErr)
fetched = &OAuth2Client{}
getErr = k8sClient.Get(context.TODO(), key, fetched)
require.NoError(t, getErr)
assert.Equal(t, created, fetched)
deleteErr = k8sClient.Delete(context.TODO(), created)
require.NoError(t, deleteErr)
getErr = k8sClient.Get(context.TODO(), key, created)
require.Error(t, getErr)
})
t.Run("by failing if the requested object doesn't meet CRD requirements", func(t *testing.T) {
for desc, modifyClient := range map[string]func(){
"invalid grant type": func() { created.Spec.GrantTypes = []GrantType{"invalid"} },
"invalid response type": func() { created.Spec.ResponseTypes = []ResponseType{"invalid"} },
"invalid scope": func() { created.Spec.Scope = "" },
} {
t.Run(fmt.Sprintf("case=%s", desc), func(t *testing.T) {
resetTestClient()
modifyClient()
createErr = k8sClient.Create(context.TODO(), created)
require.Error(t, createErr)
})
}
})
})
}
func runEnv(t *testing.T) {
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")},
}
err := SchemeBuilder.AddToScheme(scheme.Scheme)
require.NoError(t, err)
cfg, err = testEnv.Start()
require.NoError(t, err)
require.NotNil(t, cfg)
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
require.NoError(t, err)
require.NotNil(t, k8sClient)
}
func stopEnv(t *testing.T) {
err := testEnv.Stop()
require.NoError(t, err)
}
func resetTestClient() {
created = &OAuth2Client{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
Namespace: "default",
},
Spec: OAuth2ClientSpec{
GrantTypes: []GrantType{"implicit", "client_credentials", "authorization_code", "refresh_token"},
ResponseTypes: []ResponseType{"id_token", "code", "token"},
Scope: "read,write",
},
}
}

View File

@ -0,0 +1,133 @@
// +build !ignore_autogenerated
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// autogenerated by controller-gen object, do not modify manually
package v1alpha1
import (
runtime "k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *OAuth2Client) DeepCopyInto(out *OAuth2Client) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OAuth2Client.
func (in *OAuth2Client) DeepCopy() *OAuth2Client {
if in == nil {
return nil
}
out := new(OAuth2Client)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *OAuth2Client) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *OAuth2ClientList) DeepCopyInto(out *OAuth2ClientList) {
*out = *in
out.TypeMeta = in.TypeMeta
out.ListMeta = in.ListMeta
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]OAuth2Client, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OAuth2ClientList.
func (in *OAuth2ClientList) DeepCopy() *OAuth2ClientList {
if in == nil {
return nil
}
out := new(OAuth2ClientList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *OAuth2ClientList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *OAuth2ClientSpec) DeepCopyInto(out *OAuth2ClientSpec) {
*out = *in
if in.GrantTypes != nil {
in, out := &in.GrantTypes, &out.GrantTypes
*out = make([]GrantType, len(*in))
copy(*out, *in)
}
if in.ResponseTypes != nil {
in, out := &in.ResponseTypes, &out.ResponseTypes
*out = make([]ResponseType, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OAuth2ClientSpec.
func (in *OAuth2ClientSpec) DeepCopy() *OAuth2ClientSpec {
if in == nil {
return nil
}
out := new(OAuth2ClientSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *OAuth2ClientStatus) DeepCopyInto(out *OAuth2ClientStatus) {
*out = *in
if in.Secret != nil {
in, out := &in.Secret, &out.Secret
*out = new(string)
**out = **in
}
if in.ClientID != nil {
in, out := &in.ClientID, &out.ClientID
*out = new(string)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OAuth2ClientStatus.
func (in *OAuth2ClientStatus) DeepCopy() *OAuth2ClientStatus {
if in == nil {
return nil
}
out := new(OAuth2ClientStatus)
in.DeepCopyInto(out)
return out
}

View File

@ -0,0 +1,24 @@
# The following manifests contain a self-signed issuer CR and a certificate CR.
# More document can be found at https://docs.cert-manager.io
apiVersion: certmanager.k8s.io/v1alpha1
kind: Issuer
metadata:
name: selfsigned-issuer
namespace: system
spec:
selfSigned: {}
---
apiVersion: certmanager.k8s.io/v1alpha1
kind: Certificate
metadata:
name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml
namespace: system
spec:
# $(SERVICENAME) and $(NAMESPACE) will be substituted by kustomize
commonName: $(SERVICENAME).$(NAMESPACE).svc
dnsNames:
- $(SERVICENAME).$(NAMESPACE).svc.cluster.local
issuerRef:
kind: Issuer
name: selfsigned-issuer
secretName: webhook-server-cert # this secret will not be prefixed, since it's not managed by kustomize

View File

@ -0,0 +1,26 @@
resources:
- certificate.yaml
# the following config is for teaching kustomize how to do var substitution
vars:
- name: NAMESPACE # namespace of the service and the certificate CR
objref:
kind: Service
version: v1
name: webhook-service
fieldref:
fieldpath: metadata.namespace
- name: CERTIFICATENAME
objref:
kind: Certificate
group: certmanager.k8s.io
version: v1alpha1
name: serving-cert # this name should match the one in certificate.yaml
- name: SERVICENAME
objref:
kind: Service
version: v1
name: webhook-service
configurations:
- kustomizeconfig.yaml

View File

@ -0,0 +1,16 @@
# This configuration is for teaching kustomize how to update name ref and var substitution
nameReference:
- kind: Issuer
group: certmanager.k8s.io
fieldSpecs:
- kind: Certificate
group: certmanager.k8s.io
path: spec/issuerRef/name
varReference:
- kind: Certificate
group: certmanager.k8s.io
path: spec/commonName
- kind: Certificate
group: certmanager.k8s.io
path: spec/dnsNames

View File

@ -0,0 +1,450 @@
---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
creationTimestamp: null
name: oauth2clients.hydra.ory.sh
spec:
group: hydra.ory.sh
names:
kind: OAuth2Client
plural: oauth2clients
scope: ""
subresources:
status: {}
validation:
openAPIV3Schema:
description: OAuth2Client is the Schema for the oauth2clients API
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds'
type: string
metadata:
properties:
annotations:
additionalProperties:
type: string
description: 'Annotations is an unstructured key value map stored with
a resource that may be set by external tools to store and retrieve
arbitrary metadata. They are not queryable and should be preserved
when modifying objects. More info: http://kubernetes.io/docs/user-guide/annotations'
type: object
clusterName:
description: The name of the cluster which the object belongs to. This
is used to distinguish resources with same name and namespace in different
clusters. This field is not set anywhere right now and apiserver is
going to ignore it if set in create or update request.
type: string
creationTimestamp:
description: "CreationTimestamp is a timestamp representing the server
time when this object was created. It is not guaranteed to be set
in happens-before order across separate operations. Clients may not
set this value. It is represented in RFC3339 form and is in UTC. \n
Populated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata"
format: date-time
type: string
deletionGracePeriodSeconds:
description: Number of seconds allowed for this object to gracefully
terminate before it will be removed from the system. Only set when
deletionTimestamp is also set. May only be shortened. Read-only.
format: int64
type: integer
deletionTimestamp:
description: "DeletionTimestamp is RFC 3339 date and time at which this
resource will be deleted. This field is set by the server when a graceful
deletion is requested by the user, and is not directly settable by
a client. The resource is expected to be deleted (no longer visible
from resource lists, and not reachable by name) after the time in
this field, once the finalizers list is empty. As long as the finalizers
list contains items, deletion is blocked. Once the deletionTimestamp
is set, this value may not be unset or be set further into the future,
although it may be shortened or the resource may be deleted prior
to this time. For example, a user may request that a pod is deleted
in 30 seconds. The Kubelet will react by sending a graceful termination
signal to the containers in the pod. After that 30 seconds, the Kubelet
will send a hard termination signal (SIGKILL) to the container and
after cleanup, remove the pod from the API. In the presence of network
partitions, this object may still exist after this timestamp, until
an administrator or automated process can determine the resource is
fully terminated. If not set, graceful deletion of the object has
not been requested. \n Populated by the system when a graceful deletion
is requested. Read-only. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata"
format: date-time
type: string
finalizers:
description: Must be empty before the object is deleted from the registry.
Each entry is an identifier for the responsible component that will
remove the entry from the list. If the deletionTimestamp of the object
is non-nil, entries in this list can only be removed.
items:
type: string
type: array
generateName:
description: "GenerateName is an optional prefix, used by the server,
to generate a unique name ONLY IF the Name field has not been provided.
If this field is used, the name returned to the client will be different
than the name passed. This value will also be combined with a unique
suffix. The provided value has the same validation rules as the Name
field, and may be truncated by the length of the suffix required to
make the value unique on the server. \n If this field is specified
and the generated name exists, the server will NOT return a 409 -
instead, it will either return 201 Created or 500 with Reason ServerTimeout
indicating a unique name could not be found in the time allotted,
and the client should retry (optionally after the time indicated in
the Retry-After header). \n Applied only if Name is not specified.
More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#idempotency"
type: string
generation:
description: A sequence number representing a specific generation of
the desired state. Populated by the system. Read-only.
format: int64
type: integer
initializers:
description: "An initializer is a controller which enforces some system
invariant at object creation time. This field is a list of initializers
that have not yet acted on this object. If nil or empty, this object
has been completely initialized. Otherwise, the object is considered
uninitialized and is hidden (in list/watch and get calls) from clients
that haven't explicitly asked to observe uninitialized objects. \n
When an object is created, the system will populate this list with
the current set of initializers. Only privileged users may set or
modify this list. Once it is empty, it may not be modified further
by any user. \n DEPRECATED - initializers are an alpha field and will
be removed in v1.15."
properties:
pending:
description: Pending is a list of initializers that must execute
in order before this object is visible. When the last pending
initializer is removed, and no failing result is set, the initializers
struct will be set to nil and the object is considered as initialized
and visible to all clients.
items:
properties:
name:
description: name of the process that is responsible for initializing
this object.
type: string
required:
- name
type: object
type: array
result:
description: If result is set with the Failure field, the object
will be persisted to storage and then deleted, ensuring that other
clients can observe the deletion.
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this
representation of an object. Servers should convert recognized
schemas to the latest internal value, and may reject unrecognized
values. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#resources'
type: string
code:
description: Suggested HTTP return code for this status, 0 if
not set.
format: int32
type: integer
details:
description: Extended data associated with the reason. Each
reason may define its own extended details. This field is
optional and the data returned is not guaranteed to conform
to any schema except that defined by the reason type.
properties:
causes:
description: The Causes array includes more details associated
with the StatusReason failure. Not all StatusReasons may
provide detailed causes.
items:
properties:
field:
description: "The field of the resource that has caused
this error, as named by its JSON serialization.
May include dot and postfix notation for nested
attributes. Arrays are zero-indexed. Fields may
appear more than once in an array of causes due
to fields having multiple errors. Optional. \n Examples:
\ \"name\" - the field \"name\" on the current
resource \"items[0].name\" - the field \"name\"
on the first array entry in \"items\""
type: string
message:
description: A human-readable description of the cause
of the error. This field may be presented as-is
to a reader.
type: string
reason:
description: A machine-readable description of the
cause of the error. If this value is empty there
is no information available.
type: string
type: object
type: array
group:
description: The group attribute of the resource associated
with the status StatusReason.
type: string
kind:
description: 'The kind attribute of the resource associated
with the status StatusReason. On some operations may differ
from the requested resource Kind. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds'
type: string
name:
description: The name attribute of the resource associated
with the status StatusReason (when there is a single name
which can be described).
type: string
retryAfterSeconds:
description: If specified, the time in seconds before the
operation should be retried. Some errors may indicate
the client must take an alternate action - for those errors
this field may indicate how long to wait before taking
the alternate action.
format: int32
type: integer
uid:
description: 'UID of the resource. (when there is a single
resource which can be described). More info: http://kubernetes.io/docs/user-guide/identifiers#uids'
type: string
type: object
kind:
description: 'Kind is a string value representing the REST resource
this object represents. Servers may infer this from the endpoint
the client submits requests to. Cannot be updated. In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds'
type: string
message:
description: A human-readable description of the status of this
operation.
type: string
metadata:
description: 'Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds'
properties:
continue:
description: continue may be set if the user set a limit
on the number of items returned, and indicates that the
server has more data available. The value is opaque and
may be used to issue another request to the endpoint that
served this list to retrieve the next set of available
objects. Continuing a consistent list may not be possible
if the server configuration has changed or more than a
few minutes have passed. The resourceVersion field returned
when using this continue value will be identical to the
value in the first response, unless you have received
this token from an error message.
type: string
resourceVersion:
description: 'String that identifies the server''s internal
version of this object that can be used by clients to
determine when objects have changed. Value must be treated
as opaque by clients and passed unmodified back to the
server. Populated by the system. Read-only. More info:
https://git.k8s.io/community/contributors/devel/api-conventions.md#concurrency-control-and-consistency'
type: string
selfLink:
description: selfLink is a URL representing this object.
Populated by the system. Read-only.
type: string
type: object
reason:
description: A machine-readable description of why this operation
is in the "Failure" status. If this value is empty there is
no information available. A Reason clarifies an HTTP status
code but does not override it.
type: string
status:
description: 'Status of the operation. One of: "Success" or
"Failure". More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#spec-and-status'
type: string
type: object
required:
- pending
type: object
labels:
additionalProperties:
type: string
description: 'Map of string keys and values that can be used to organize
and categorize (scope and select) objects. May match selectors of
replication controllers and services. More info: http://kubernetes.io/docs/user-guide/labels'
type: object
managedFields:
description: "ManagedFields maps workflow-id and version to the set
of fields that are managed by that workflow. This is mostly for internal
housekeeping, and users typically shouldn't need to set or understand
this field. A workflow can be the user's name, a controller's name,
or the name of a specific apply path like \"ci-cd\". The set of fields
is always in the version that the workflow used when modifying the
object. \n This field is alpha and can be changed or removed without
notice."
items:
properties:
apiVersion:
description: APIVersion defines the version of this resource that
this field set applies to. The format is "group/version" just
like the top-level APIVersion field. It is necessary to track
the version of a field set because it cannot be automatically
converted.
type: string
fields:
additionalProperties: true
description: Fields identifies a set of fields.
type: object
manager:
description: Manager is an identifier of the workflow managing
these fields.
type: string
operation:
description: Operation is the type of operation which lead to
this ManagedFieldsEntry being created. The only valid values
for this field are 'Apply' and 'Update'.
type: string
time:
description: Time is timestamp of when these fields were set.
It should always be empty if Operation is 'Apply'
format: date-time
type: string
type: object
type: array
name:
description: 'Name must be unique within a namespace. Is required when
creating resources, although some resources may allow a client to
request the generation of an appropriate name automatically. Name
is primarily intended for creation idempotence and configuration definition.
Cannot be updated. More info: http://kubernetes.io/docs/user-guide/identifiers#names'
type: string
namespace:
description: "Namespace defines the space within each name must be unique.
An empty namespace is equivalent to the \"default\" namespace, but
\"default\" is the canonical representation. Not all objects are required
to be scoped to a namespace - the value of this field for those objects
will be empty. \n Must be a DNS_LABEL. Cannot be updated. More info:
http://kubernetes.io/docs/user-guide/namespaces"
type: string
ownerReferences:
description: List of objects depended by this object. If ALL objects
in the list have been deleted, this object will be garbage collected.
If this object is managed by a controller, then an entry in this list
will point to this controller, with the controller field set to true.
There cannot be more than one managing controller.
items:
properties:
apiVersion:
description: API version of the referent.
type: string
blockOwnerDeletion:
description: If true, AND if the owner has the "foregroundDeletion"
finalizer, then the owner cannot be deleted from the key-value
store until this reference is removed. Defaults to false. To
set this field, a user needs "delete" permission of the owner,
otherwise 422 (Unprocessable Entity) will be returned.
type: boolean
controller:
description: If true, this reference points to the managing controller.
type: boolean
kind:
description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds'
type: string
name:
description: 'Name of the referent. More info: http://kubernetes.io/docs/user-guide/identifiers#names'
type: string
uid:
description: 'UID of the referent. More info: http://kubernetes.io/docs/user-guide/identifiers#uids'
type: string
required:
- apiVersion
- kind
- name
- uid
type: object
type: array
resourceVersion:
description: "An opaque value that represents the internal version of
this object that can be used by clients to determine when objects
have changed. May be used for optimistic concurrency, change detection,
and the watch operation on a resource or set of resources. Clients
must treat these values as opaque and passed unmodified back to the
server. They may only be valid for a particular resource or set of
resources. \n Populated by the system. Read-only. Value must be treated
as opaque by clients and . More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#concurrency-control-and-consistency"
type: string
selfLink:
description: SelfLink is a URL representing this object. Populated by
the system. Read-only.
type: string
uid:
description: "UID is the unique in time and space value for this object.
It is typically generated by the server on successful creation of
a resource and is not allowed to change on PUT operations. \n Populated
by the system. Read-only. More info: http://kubernetes.io/docs/user-guide/identifiers#uids"
type: string
type: object
spec:
properties:
grantTypes:
description: GrantTypes is an array of grant types the client is allowed
to use.
items:
enum:
- client_credentials
- authorization_code
- implicit
- refresh_token
type: string
maxItems: 4
minItems: 1
type: array
responseTypes:
description: ResponseTypes is an array of the OAuth 2.0 response type
strings that the client can use at the authorization endpoint.
items:
enum:
- id_token
- code
- token
type: string
maxItems: 3
minItems: 1
type: array
scope:
description: Scope is a string containing a space-separated list of
scope values (as described in Section 3.3 of OAuth 2.0 [RFC6749])
that the client can use when requesting access tokens.
pattern: ([a-zA-Z0-9\.\*]+\s?)+
type: string
required:
- grantTypes
- scope
type: object
status:
properties:
clientID:
description: ClientID is the id for this client.
type: string
observedGeneration:
description: ObservedGeneration represents the most recent generation
observed by the daemon set controller.
format: int64
type: integer
secret:
description: Secret points to the K8s secret that contains this client's
id and password
type: string
type: object
type: object
versions:
- name: v1alpha1
served: true
storage: true
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []

View File

@ -0,0 +1,19 @@
# This kustomization.yaml is not intended to be run by itself,
# since it depends on service name and namespace that are out of this kustomize package.
# It should be run by config/default
resources:
- bases/hydra.ory.sh_oauth2clients.yaml
# +kubebuilder:scaffold:crdkustomizeresource
patches:
# [WEBHOOK] patches here are for enabling the conversion webhook for each CRD
#- patches/webhook_in_oauth2clients.yaml
# +kubebuilder:scaffold:crdkustomizewebhookpatch
# [CAINJECTION] patches here are for enabling the CA injection for each CRD
#- patches/cainjection_in_oauth2clients.yaml
# +kubebuilder:scaffold:crdkustomizecainjectionpatch
# the following config is for teaching kustomize how to do kustomization for CRDs.
configurations:
- kustomizeconfig.yaml

View File

@ -0,0 +1,17 @@
# This file is for teaching kustomize how to substitute name and namespace reference in CRD
nameReference:
- kind: Service
version: v1
fieldSpecs:
- kind: CustomResourceDefinition
group: apiextensions.k8s.io
path: spec/conversion/webhookClientConfig/service/name
namespace:
- kind: CustomResourceDefinition
group: apiextensions.k8s.io
path: spec/conversion/webhookClientConfig/service/namespace
create: false
varReference:
- path: metadata/annotations

View File

@ -0,0 +1,8 @@
# The following patch adds a directive for certmanager to inject CA into the CRD
# CRD conversion requires k8s 1.13 or later.
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
annotations:
certmanager.k8s.io/inject-ca-from: $(NAMESPACE)/$(CERTIFICATENAME)
name: oauth2clients.hydra.ory.sh

View File

@ -0,0 +1,17 @@
# The following patch enables conversion webhook for CRD
# CRD conversion requires k8s 1.13 or later.
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: oauth2clients.hydra.ory.sh
spec:
conversion:
strategy: Webhook
webhookClientConfig:
# this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank,
# but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager)
caBundle: Cg==
service:
namespace: system
name: webhook-service
path: /convert

View File

@ -0,0 +1,43 @@
# Adds namespace to all resources.
namespace: hydra-maester-system
# Value of this field is prepended to the
# names of all resources, e.g. a deployment named
# "wordpress" becomes "alices-wordpress".
# Note that it should also match with the prefix (text before '-') of the namespace
# field above.
namePrefix: hydra-maester-
# Labels to add to all resources and selectors.
#commonLabels:
# someName: someValue
bases:
- ../crd
- ../rbac
- ../manager
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in crd/kustomization.yaml
#- ../webhook
# [CERTMANAGER] To enable cert-manager, uncomment next line. 'WEBHOOK' components are required.
#- ../certmanager
patches:
- manager_image_patch.yaml
# Protect the /metrics endpoint by putting it behind auth.
# Only one of manager_auth_proxy_patch.yaml and
# manager_prometheus_metrics_patch.yaml should be enabled.
- manager_auth_proxy_patch.yaml
# If you want your controller-manager to expose the /metrics
# endpoint w/o any authn/z, uncomment the following line and
# comment manager_auth_proxy_patch.yaml.
# Only one of manager_auth_proxy_patch.yaml and
# manager_prometheus_metrics_patch.yaml should be enabled.
#- manager_prometheus_metrics_patch.yaml
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in crd/kustomization.yaml
#- manager_webhook_patch.yaml
# [CAINJECTION] Uncomment next line to enable the CA injection in the admission webhooks.
# Uncomment 'CAINJECTION' in crd/kustomization.yaml to enable the CA injection in the admission webhooks.
# 'CERTMANAGER' needs to be enabled to use ca injection
#- webhookcainjection_patch.yaml

View File

@ -0,0 +1,24 @@
# This patch inject a sidecar container which is a HTTP proxy for the controller manager,
# it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews.
apiVersion: apps/v1
kind: Deployment
metadata:
name: controller-manager
namespace: system
spec:
template:
spec:
containers:
- name: kube-rbac-proxy
image: gcr.io/kubebuilder/kube-rbac-proxy:v0.4.0
args:
- "--secure-listen-address=0.0.0.0:8443"
- "--upstream=http://127.0.0.1:8080/"
- "--logtostderr=true"
- "--v=10"
ports:
- containerPort: 8443
name: https
- name: manager
args:
- "--metrics-addr=127.0.0.1:8080"

View File

@ -0,0 +1,12 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: controller-manager
namespace: system
spec:
template:
spec:
containers:
# Change the value of image field below to your controller image URL
- image: controller:latest
name: manager

View File

@ -0,0 +1,19 @@
# This patch enables Prometheus scraping for the manager pod.
apiVersion: apps/v1
kind: Deployment
metadata:
name: controller-manager
namespace: system
spec:
template:
metadata:
annotations:
prometheus.io/scrape: 'true'
spec:
containers:
# Expose the prometheus metrics on default port
- name: manager
ports:
- containerPort: 8080
name: metrics
protocol: TCP

View File

@ -0,0 +1,23 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: controller-manager
namespace: system
spec:
template:
spec:
containers:
- name: manager
ports:
- containerPort: 443
name: webhook-server
protocol: TCP
volumeMounts:
- mountPath: /tmp/k8s-webhook-server/serving-certs
name: cert
readOnly: true
volumes:
- name: cert
secret:
defaultMode: 420
secretName: webhook-server-cert

View File

@ -0,0 +1,15 @@
# This patch add annotation to admission webhook config and
# the variables $(NAMESPACE) and $(CERTIFICATENAME) will be substituted by kustomize.
apiVersion: admissionregistration.k8s.io/v1beta1
kind: MutatingWebhookConfiguration
metadata:
name: mutating-webhook-configuration
annotations:
certmanager.k8s.io/inject-ca-from: $(NAMESPACE)/$(CERTIFICATENAME)
---
apiVersion: admissionregistration.k8s.io/v1beta1
kind: ValidatingWebhookConfiguration
metadata:
name: validating-webhook-configuration
annotations:
certmanager.k8s.io/inject-ca-from: $(NAMESPACE)/$(CERTIFICATENAME)

View File

@ -0,0 +1,2 @@
resources:
- manager.yaml

View File

@ -0,0 +1,40 @@
apiVersion: v1
kind: Namespace
metadata:
labels:
control-plane: controller-manager
name: system
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: controller-manager
namespace: system
labels:
control-plane: controller-manager
spec:
selector:
matchLabels:
control-plane: controller-manager
replicas: 1
template:
metadata:
labels:
control-plane: controller-manager
spec:
containers:
- command:
- /manager
args:
- --enable-leader-election
- --hydra-url=http://use.actual.hydra.fqdn #change it to your ORY Hydra address
image: controller:latest
name: manager
resources:
limits:
cpu: 100m
memory: 30Mi
requests:
cpu: 100m
memory: 20Mi
terminationGracePeriodSeconds: 10

View File

@ -0,0 +1,13 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: proxy-role
rules:
- apiGroups: ["authentication.k8s.io"]
resources:
- tokenreviews
verbs: ["create"]
- apiGroups: ["authorization.k8s.io"]
resources:
- subjectaccessreviews
verbs: ["create"]

View File

@ -0,0 +1,12 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: proxy-rolebinding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: proxy-role
subjects:
- kind: ServiceAccount
name: default
namespace: system

View File

@ -0,0 +1,18 @@
apiVersion: v1
kind: Service
metadata:
annotations:
prometheus.io/port: "8443"
prometheus.io/scheme: https
prometheus.io/scrape: "true"
labels:
control-plane: controller-manager
name: controller-manager-metrics-service
namespace: system
spec:
ports:
- name: https
port: 8443
targetPort: https
selector:
control-plane: controller-manager

View File

@ -0,0 +1,11 @@
resources:
- role.yaml
- role_binding.yaml
- leader_election_role.yaml
- leader_election_role_binding.yaml
# Comment the following 3 lines if you want to disable
# the auth proxy (https://github.com/brancz/kube-rbac-proxy)
# which protects your /metrics endpoint.
- auth_proxy_service.yaml
- auth_proxy_role.yaml
- auth_proxy_role_binding.yaml

View File

@ -0,0 +1,26 @@
# permissions to do leader election.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: leader-election-role
rules:
- apiGroups:
- ""
resources:
- configmaps
verbs:
- get
- list
- watch
- create
- update
- patch
- delete
- apiGroups:
- ""
resources:
- configmaps/status
verbs:
- get
- update
- patch

View File

@ -0,0 +1,12 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: leader-election-rolebinding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: leader-election-role
subjects:
- kind: ServiceAccount
name: default
namespace: system

40
config/rbac/role.yaml Normal file
View File

@ -0,0 +1,40 @@
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
creationTimestamp: null
name: manager-role
rules:
- apiGroups:
- hydra.ory.sh
resources:
- oauth2clients
verbs:
- get
- list
- watch
- create
- update
- patch
- delete
- apiGroups:
- hydra.ory.sh
resources:
- oauth2clients/status
verbs:
- get
- update
- patch
- apiGroups:
- ""
resources:
- secrets
verbs:
- get
- list
- watch
- create
- update
- patch
- delete

View File

@ -0,0 +1,12 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: manager-rolebinding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: manager-role
subjects:
- kind: ServiceAccount
name: default
namespace: system

View File

@ -0,0 +1,7 @@
apiVersion: hydra.ory.sh/v1alpha1
kind: OAuth2Client
metadata:
name: oauth2client-sample
spec:
# Add fields here
foo: bar

View File

@ -0,0 +1,6 @@
resources:
- manifests.yaml
- service.yaml
configurations:
- kustomizeconfig.yaml

View File

@ -0,0 +1,25 @@
# the following config is for teaching kustomize where to look at when substituting vars.
# It requires kustomize v2.1.0 or newer to work properly.
nameReference:
- kind: Service
version: v1
fieldSpecs:
- kind: MutatingWebhookConfiguration
group: admissionregistration.k8s.io
path: webhooks/clientConfig/service/name
- kind: ValidatingWebhookConfiguration
group: admissionregistration.k8s.io
path: webhooks/clientConfig/service/name
namespace:
- kind: MutatingWebhookConfiguration
group: admissionregistration.k8s.io
path: webhooks/clientConfig/service/namespace
create: true
- kind: ValidatingWebhookConfiguration
group: admissionregistration.k8s.io
path: webhooks/clientConfig/service/namespace
create: true
varReference:
- path: metadata/annotations

View File

View File

@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: webhook-service
namespace: system
spec:
ports:
- port: 443
targetPort: 443
selector:
control-plane: controller-manager

View File

@ -0,0 +1,145 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controllers
import (
"context"
"fmt"
"k8s.io/apimachinery/pkg/types"
"github.com/go-logr/logr"
hydrav1alpha1 "github.com/ory/hydra-maester/api/v1alpha1"
"github.com/ory/hydra-maester/hydra"
apiv1 "k8s.io/api/core/v1"
apierrs "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
)
const (
clientIDKey = "client_id"
clientSecretKey = "client_secret"
)
type HydraClientInterface interface {
GetOAuth2Client(id string) (*hydra.OAuth2ClientJSON, bool, error)
PostOAuth2Client(o *hydra.OAuth2ClientJSON) (*hydra.OAuth2ClientJSON, error)
PutOAuth2Client(o *hydra.OAuth2ClientJSON) (*hydra.OAuth2ClientJSON, error)
DeleteOAuth2Client(id string) error
}
// OAuth2ClientReconciler reconciles a OAuth2Client object
type OAuth2ClientReconciler struct {
HydraClient HydraClientInterface
Log logr.Logger
client.Client
}
// +kubebuilder:rbac:groups=hydra.ory.sh,resources=oauth2clients,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=hydra.ory.sh,resources=oauth2clients/status,verbs=get;update;patch
// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete
func (r *OAuth2ClientReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
ctx := context.Background()
_ = r.Log.WithValues("oauth2client", req.NamespacedName)
var client hydrav1alpha1.OAuth2Client
if err := r.Get(ctx, req.NamespacedName, &client); err != nil {
if apierrs.IsNotFound(err) {
if err := r.unregisterOAuth2Client(ctx, req.NamespacedName); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
return ctrl.Result{}, err
}
if client.Generation != client.Status.ObservedGeneration {
var registered = false
var err error
if client.Status.ClientID != nil {
_, registered, err = r.HydraClient.GetOAuth2Client(*client.Status.ClientID)
if err != nil {
return ctrl.Result{}, err
}
}
if !registered {
return ctrl.Result{}, r.registerOAuth2Client(ctx, &client)
}
return ctrl.Result{}, r.updateRegisteredOAuth2Client(&client)
}
return ctrl.Result{}, nil
}
func (r *OAuth2ClientReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&hydrav1alpha1.OAuth2Client{}).
Complete(r)
}
func (r *OAuth2ClientReconciler) registerOAuth2Client(ctx context.Context, client *hydrav1alpha1.OAuth2Client) error {
created, err := r.HydraClient.PostOAuth2Client(client.ToOAuth2ClientJSON())
if err != nil {
return err
}
clientSecret := apiv1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: client.Name,
Namespace: client.Namespace,
},
Data: map[string][]byte{
clientSecretKey: []byte(*created.Secret),
clientIDKey: []byte(*created.ClientID),
},
}
err = r.Create(ctx, &clientSecret)
if err != nil {
return err
}
client.Status.Secret = &clientSecret.Name
client.Status.ClientID = created.ClientID
client.Status.ObservedGeneration = client.Generation
return r.Status().Update(ctx, client)
}
func (r *OAuth2ClientReconciler) unregisterOAuth2Client(ctx context.Context, namespacedName types.NamespacedName) error {
var sec apiv1.Secret
if err := r.Get(ctx, namespacedName, &sec); err != nil {
if apierrs.IsNotFound(err) {
r.Log.Info(fmt.Sprintf("unable to find secret corresponding with client %s/%s. Manual deletion recommended", namespacedName.Name, namespacedName.Namespace))
return nil
}
return err
}
return r.HydraClient.DeleteOAuth2Client(string(sec.Data[clientIDKey]))
}
func (r *OAuth2ClientReconciler) updateRegisteredOAuth2Client(client *hydrav1alpha1.OAuth2Client) error {
_, err := r.HydraClient.PutOAuth2Client(client.ToOAuth2ClientJSON())
return err
}

View File

@ -0,0 +1,197 @@
package controllers_test
import (
"context"
"fmt"
"time"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
hydrav1alpha1 "github.com/ory/hydra-maester/api/v1alpha1"
"github.com/ory/hydra-maester/controllers"
"github.com/ory/hydra-maester/hydra"
apiv1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes/scheme"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
)
const timeout = time.Second * 5
var _ = Describe("OAuth2Client Controller", func() {
Context("in a happy-path scenario", func() {
var tstName = "test"
var tstNamespace = "default"
var tstScopes = "a b c"
var tstClientID = "testClientID"
var tstSecret = "testSecret"
var expectedRequest = reconcile.Request{NamespacedName: types.NamespacedName{Name: tstName, Namespace: tstNamespace}}
It("should call create OAuth2 client in Hydra and a Secret", func() {
s := scheme.Scheme
err := hydrav1alpha1.AddToScheme(s)
Expect(err).NotTo(HaveOccurred())
// Setup the Manager and Controller. Wrap the Controller Reconcile function so it writes each request to a
// channel when it is finished.
mgr, err := manager.New(cfg, manager.Options{Scheme: s})
Expect(err).NotTo(HaveOccurred())
c := mgr.GetClient()
mch := (&mockHydraClient{}).
withSecret(tstSecret).
withClientID(tstClientID)
recFn, requests := SetupTestReconcile(getAPIReconciler(mgr, mch))
//_, requests := SetupTestReconcile(getApiReconciler(mgr))
Expect(add(mgr, recFn)).To(Succeed())
//Start the manager and the controller
stopMgr, mgrStopped := StartTestManager(mgr)
//Ensure manager is stopped properly
defer func() {
close(stopMgr)
mgrStopped.Wait()
}()
instance := testInstance(tstName, tstNamespace, tstScopes)
err = c.Create(context.TODO(), instance)
// The instance object may not be a valid object because it might be missing some required fields.
// Please modify the instance object by adding required fields and then remove the following if statement.
if apierrors.IsInvalid(err) {
Fail(fmt.Sprintf("failed to create object, got an invalid object error: %v", err))
return
}
Expect(err).NotTo(HaveOccurred())
defer c.Delete(context.TODO(), instance)
Eventually(requests, timeout).Should(Receive(Equal(expectedRequest)))
//Verify the created CR instance status
var retrieved hydrav1alpha1.OAuth2Client
ok := client.ObjectKey{Name: tstName, Namespace: tstNamespace}
err = c.Get(context.TODO(), ok, &retrieved)
Expect(err).NotTo(HaveOccurred())
Expect(*retrieved.Status.ClientID).To(Equal(tstClientID))
Expect(*retrieved.Status.Secret).To(Equal(tstName)) //Secret contents is not visible in the CR instance!
//Verify the created Secret
var createdSecret = apiv1.Secret{}
k8sClient.Get(context.TODO(), ok, &createdSecret)
Expect(err).NotTo(HaveOccurred())
Expect(createdSecret.Data["client_secret"]).To(Equal([]byte(tstSecret)))
})
})
})
// add adds a new Controller to mgr with r as the reconcile.Reconciler
func add(mgr manager.Manager, r reconcile.Reconciler) error {
// Create a new controller
c, err := controller.New("api-gateway-controller", mgr, controller.Options{Reconciler: r})
if err != nil {
return err
}
// Watch for changes to Api
err = c.Watch(&source.Kind{Type: &hydrav1alpha1.OAuth2Client{}}, &handler.EnqueueRequestForObject{})
if err != nil {
return err
}
// TODO(user): Modify this to be the types you create
// Uncomment watch a Deployment created by Guestbook - change this for objects you create
//err = c.Watch(&source.Kind{Type: &appsv1.Deployment{}}, &handler.EnqueueRequestForOwner{
// IsController: true,
// OwnerType: &webappv1.Guestbook{},
//})
//if err != nil {
// return err
//}
return nil
}
func getAPIReconciler(mgr ctrl.Manager, mock *mockHydraClient) reconcile.Reconciler {
return &controllers.OAuth2ClientReconciler{
Client: mgr.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("OAuth2Client"),
HydraClient: mock,
}
}
func testInstance(name, namespace, scopes string) *hydrav1alpha1.OAuth2Client {
return &hydrav1alpha1.OAuth2Client{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: hydrav1alpha1.OAuth2ClientSpec{
GrantTypes: []hydrav1alpha1.GrantType{"client_credentials"},
ResponseTypes: []hydrav1alpha1.ResponseType{"token"},
Scope: scopes,
}}
}
//TODO: Replace with full-fledged mocking framework (mockery/go-mock)
type mockHydraClient struct {
resSecret string
resClientID string
postedData *hydra.OAuth2ClientJSON
}
func (m *mockHydraClient) withSecret(secret string) *mockHydraClient {
m.resSecret = secret
return m
}
func (m *mockHydraClient) withClientID(clientID string) *mockHydraClient {
m.resClientID = clientID
return m
}
//Returns the data previously "stored" by PostOAuth2Client
func (m *mockHydraClient) GetOAuth2Client(id string) (*hydra.OAuth2ClientJSON, bool, error) {
res := &hydra.OAuth2ClientJSON{
ClientID: &m.resClientID,
Secret: &m.resSecret,
Name: m.postedData.Name,
GrantTypes: m.postedData.GrantTypes,
ResponseTypes: m.postedData.ResponseTypes,
Scope: m.postedData.Scope,
}
return res, true, nil
}
func (m *mockHydraClient) PostOAuth2Client(o *hydra.OAuth2ClientJSON) (*hydra.OAuth2ClientJSON, error) {
m.postedData = o
res := &hydra.OAuth2ClientJSON{
ClientID: &m.resClientID,
Secret: &m.resSecret,
Name: o.Name,
GrantTypes: o.GrantTypes,
ResponseTypes: o.ResponseTypes,
Scope: o.Scope,
}
return res, nil
}
func (m *mockHydraClient) DeleteOAuth2Client(id string) error {
panic("Should not be invoked!")
}
func (m *mockHydraClient) PutOAuth2Client(o *hydra.OAuth2ClientJSON) (*hydra.OAuth2ClientJSON, error) {
panic("Should not be invoked!")
}

102
controllers/suite_test.go Normal file
View File

@ -0,0 +1,102 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controllers_test
import (
"path/filepath"
"sync"
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
// +kubebuilder:scaffold:imports
)
// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
var cfg *rest.Config
var k8sClient client.Client
var testEnv *envtest.Environment
func TestAPIs(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecsWithDefaultAndCustomReporters(t,
"Controller Suite",
[]Reporter{envtest.NewlineReporter{}})
}
var _ = BeforeSuite(func(done Done) {
logf.SetLogger(zap.LoggerTo(GinkgoWriter, true))
By("bootstrapping test environment")
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")},
}
var err error
cfg, err = testEnv.Start()
Expect(err).ToNot(HaveOccurred())
Expect(cfg).ToNot(BeNil())
// +kubebuilder:scaffold:scheme
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
Expect(err).ToNot(HaveOccurred())
Expect(k8sClient).ToNot(BeNil())
close(done)
}, 60)
var _ = AfterSuite(func() {
By("tearing down the test environment")
err := testEnv.Stop()
Expect(err).ToNot(HaveOccurred())
})
// SetupTestReconcile returns a reconcile.Reconcile implementation that delegates to inner and
// writes the request to requests after Reconcile is finished.
func SetupTestReconcile(inner reconcile.Reconciler) (reconcile.Reconciler, chan reconcile.Request) {
requests := make(chan reconcile.Request)
fn := reconcile.Func(func(req reconcile.Request) (reconcile.Result, error) {
result, err := inner.Reconcile(req)
requests <- req
return result, err
})
return fn, requests
}
// StartTestManager adds recFn
func StartTestManager(mgr manager.Manager) (chan struct{}, *sync.WaitGroup) {
stop := make(chan struct{})
wg := &sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
Expect(mgr.Start(stop)).NotTo(HaveOccurred())
}()
return stop, wg
}

16
go.mod Normal file
View File

@ -0,0 +1,16 @@
module github.com/ory/hydra-maester
go 1.12
require (
github.com/go-logr/logr v0.1.0
github.com/onsi/ginkgo v1.6.0
github.com/onsi/gomega v1.4.2
github.com/stretchr/testify v1.3.0
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd
k8s.io/api v0.0.0-20190409021203-6e4e0e4f393b
k8s.io/apimachinery v0.0.0-20190404173353-6a84e37a896d
k8s.io/client-go v11.0.1-0.20190409021438-1a26190bd76a+incompatible
k8s.io/utils v0.0.0-20190506122338-8fab8cb257d5
sigs.k8s.io/controller-runtime v0.2.0-beta.2
)

126
go.sum Normal file
View File

@ -0,0 +1,126 @@
cloud.google.com/go v0.26.0 h1:e0WKqKTd5BnrG8aKH3J3h+QvEIQtSUcf2n5UZ5ZgLtQ=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/appscode/jsonpatch v0.0.0-20190108182946-7c0e3b262f30 h1:Kn3rqvbUFqSepE2OqVu0Pn1CbDw9IuMlONapol0zuwk=
github.com/appscode/jsonpatch v0.0.0-20190108182946-7c0e3b262f30/go.mod h1:4AJxUpXUhv4N+ziTvIcWWXgeorXpxPZOfk9HdEVr96M=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/evanphx/json-patch v4.0.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/evanphx/json-patch v4.1.0+incompatible h1:K1MDoo4AZ4wU0GIU/fPmtZg7VpzLjCxu+UwBD1FvwOc=
github.com/evanphx/json-patch v4.1.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/go-logr/logr v0.1.0 h1:M1Tv3VzNlEHg6uyACnRdtrploV2P7wZqH8BoQMtz0cg=
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
github.com/go-logr/zapr v0.1.0 h1:h+WVe9j6HAA01niTJPA/kKH0i7e0rLZBCwauQFcRE54=
github.com/go-logr/zapr v0.1.0/go.mod h1:tabnROwaDl0UNxkVeFRbY8bwB37GwRv0P8lg6aAiEnk=
github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/groupcache v0.0.0-20180513044358-24b0969c4cb7 h1:u4bArs140e9+AfE52mFHOXVFnOSBJBRlzTHrOPLOIhE=
github.com/golang/groupcache v0.0.0-20180513044358-24b0969c4cb7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeqr2z77+8R2RKyh8PG66dcu1V0ck=
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
github.com/googleapis/gnostic v0.2.0 h1:l6N3VoaVzTncYYW+9yOz2LJJammFZGBO13sqgEhpy9g=
github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
github.com/hashicorp/golang-lru v0.0.0-20180201235237-0fb14efe8c47 h1:UnszMmmmm5vLwWzDjTFVIkfhvWF1NdrmChl8L2NUDCw=
github.com/hashicorp/golang-lru v0.0.0-20180201235237-0fb14efe8c47/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE=
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.2 h1:3mYCb7aPxS/RU7TI1y4rkEn1oKmPRjNJLNEXgw7MH2I=
github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pborman/uuid v0.0.0-20170612153648-e790cca94e6c h1:MUyE44mTvnI5A0xrxIxaMqoWFzPfQvtE2IWUollMDMs=
github.com/pborman/uuid v0.0.0-20170612153648-e790cca94e6c/go.mod h1:VyrYX9gd7irzKovcSS6BIIEwPRkP2Wm2m9ufcdFSJ34=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.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/prometheus/client_golang v0.9.0 h1:tXuTFVHC03mW0D+Ua1Q2d1EAVqLTuggX50V0VLICCzY=
github.com/prometheus/client_golang v0.9.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e h1:n/3MEhJQjQxrOUCzh1Y3Re6aJUUWRp2M9+Oc3eVn/54=
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273 h1:agujYaXJSxSo18YNX3jzl+4G6Bstwt+kqv47GS12uL0=
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/pflag v1.0.2 h1:Fy0orTDgHdbnzHcsOgfCN4LtHf0ec3wwtiwJqwvf3Gc=
github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.9.1 h1:XCJQEf3W6eZaVwhRBof6ImoYGJSITeKWsyeh3HFu/5o=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac h1:7d7lG9fHOLdL6jZPtnV4LpI41SbohIJ1Atq7U991dMg=
golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 h1:+DCIGbF/swA92ohVg0//6X2IVY3KZs6p9mix0ziNYJM=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
google.golang.org/appengine v1.1.0 h1:igQkv0AAhEIvTEpD5LIpAfav2eeVO9HBTjvKHVJPRSs=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
k8s.io/api v0.0.0-20190409021203-6e4e0e4f393b h1:aBGgKJUM9Hk/3AE8WaZIApnTxG35kbuQba2w+SXqezo=
k8s.io/api v0.0.0-20190409021203-6e4e0e4f393b/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA=
k8s.io/apiextensions-apiserver v0.0.0-20190409022649-727a075fdec8 h1:q1Qvjzs/iEdXF6A1a8H3AKVFDzJNcJn3nXMs6R6qFtA=
k8s.io/apiextensions-apiserver v0.0.0-20190409022649-727a075fdec8/go.mod h1:IxkesAMoaCRoLrPJdZNZUQp9NfZnzqaVzLhb2VEQzXE=
k8s.io/apimachinery v0.0.0-20190404173353-6a84e37a896d h1:Jmdtdt1ZnoGfWWIIik61Z7nKYgO3J+swQJtPYsP9wHA=
k8s.io/apimachinery v0.0.0-20190404173353-6a84e37a896d/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0=
k8s.io/client-go v11.0.1-0.20190409021438-1a26190bd76a+incompatible h1:U5Bt+dab9K8qaUmXINrkXO135kA11/i5Kg1RUydgaMQ=
k8s.io/client-go v11.0.1-0.20190409021438-1a26190bd76a+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s=
k8s.io/klog v0.3.0 h1:0VPpR+sizsiivjIfIAQH/rl8tan6jvWkS7lU+0di3lE=
k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
k8s.io/kube-openapi v0.0.0-20180731170545-e3762e86a74c h1:3KSCztE7gPitlZmWbNwue/2U0YruD65DqX3INopDAQM=
k8s.io/kube-openapi v0.0.0-20180731170545-e3762e86a74c/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc=
k8s.io/utils v0.0.0-20190506122338-8fab8cb257d5 h1:VBM/0P5TWxwk+Nw6Z+lAw3DKgO76g90ETOiA6rfLV1Y=
k8s.io/utils v0.0.0-20190506122338-8fab8cb257d5/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
sigs.k8s.io/controller-runtime v0.2.0-beta.2 h1:hOWldx1qmGI9TsU+uUsq1xTgVmUV7AZo08VAYX0dwGI=
sigs.k8s.io/controller-runtime v0.2.0-beta.2/go.mod h1:TSH2R0nSz4WAlUUlNnOFcOR/VUhfwBLlmtq2X6AiQCA=
sigs.k8s.io/testing_frameworks v0.1.1 h1:cP2l8fkA3O9vekpy5Ks8mmA0NW/F7yBdXf8brkWhVrs=
sigs.k8s.io/testing_frameworks v0.1.1/go.mod h1:VVBKrHmJ6Ekkfz284YKhQePcdycOzNH9qL6ht1zEr/U=
sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=

14
hack/boilerplate.go.txt Normal file
View File

@ -0,0 +1,14 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

149
hydra/client.go Normal file
View File

@ -0,0 +1,149 @@
package hydra
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"path"
)
type Client struct {
HydraURL url.URL
HTTPClient *http.Client
}
func (c *Client) GetOAuth2Client(id string) (*OAuth2ClientJSON, bool, error) {
var jsonClient *OAuth2ClientJSON
req, err := c.newRequest(http.MethodGet, id, nil)
if err != nil {
return nil, false, err
}
resp, err := c.do(req, &jsonClient)
if err != nil {
return nil, false, err
}
switch resp.StatusCode {
case http.StatusOK:
return jsonClient, true, nil
case http.StatusNotFound:
return nil, false, nil
default:
return nil, false, fmt.Errorf("%s %s http request returned unexpected status code %s", req.Method, req.URL.String(), resp.Status)
}
}
func (c *Client) PostOAuth2Client(o *OAuth2ClientJSON) (*OAuth2ClientJSON, error) {
var jsonClient *OAuth2ClientJSON
req, err := c.newRequest(http.MethodPost, "", o)
if err != nil {
return nil, err
}
resp, err := c.do(req, &jsonClient)
if err != nil {
return nil, err
}
switch resp.StatusCode {
case http.StatusCreated:
return jsonClient, nil
case http.StatusConflict:
return nil, fmt.Errorf(" %s %s http request failed: requested ID already exists", req.Method, req.URL)
default:
return nil, fmt.Errorf("%s %s http request returned unexpected status code: %s", req.Method, req.URL, resp.Status)
}
}
func (c *Client) PutOAuth2Client(o *OAuth2ClientJSON) (*OAuth2ClientJSON, error) {
var jsonClient *OAuth2ClientJSON
req, err := c.newRequest(http.MethodPut, *o.ClientID, o)
if err != nil {
return nil, err
}
resp, err := c.do(req, &jsonClient)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%s %s http request returned unexpected status code: %s", req.Method, req.URL, resp.Status)
}
return jsonClient, nil
}
func (c *Client) DeleteOAuth2Client(id string) error {
req, err := c.newRequest(http.MethodDelete, id, nil)
if err != nil {
return err
}
resp, err := c.do(req, nil)
if err != nil {
return err
}
switch resp.StatusCode {
case http.StatusNoContent:
return nil
case http.StatusNotFound:
fmt.Printf("client with id %s does not exist", id)
return nil
default:
return fmt.Errorf("%s %s http request returned unexpected status code %s", req.Method, req.URL.String(), resp.Status)
}
}
func (c *Client) newRequest(method, relativePath string, body interface{}) (*http.Request, error) {
var buf io.ReadWriter
if body != nil {
buf = new(bytes.Buffer)
err := json.NewEncoder(buf).Encode(body)
if err != nil {
return nil, err
}
}
u := c.HydraURL
u.Path = path.Join(u.Path, relativePath)
req, err := http.NewRequest(method, u.String(), buf)
if err != nil {
return nil, err
}
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
req.Header.Set("Accept", "application/json")
return req, nil
}
func (c *Client) do(req *http.Request, v interface{}) (*http.Response, error) {
resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if v != nil {
err = json.NewDecoder(resp.Body).Decode(v)
}
return resp, err
}

269
hydra/client_test.go Normal file
View File

@ -0,0 +1,269 @@
package hydra_test
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"k8s.io/utils/pointer"
"github.com/ory/hydra-maester/hydra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
testID = "test-id"
schemeHTTP = "http"
testClient = `{"client_id":"test-id","client_name":"test-name","scope":"some,scopes","grant_types":["type1"]}`
testClientCreated = `{"client_id":"test-id-2", "client_secret": "TmGkvcY7k526","client_name":"test-name-2","scope":"some,other,scopes","grant_types":["type2"]}`
testClientUpdated = `{"client_id":"test-id-3", "client_secret": "xFoPPm654por","client_name":"test-name-3","scope":"yet,another,scope","grant_types":["type3"]}`
emptyBody = `{}`
clientsEndpoint = "/clients"
)
type server struct {
statusCode int
respBody string
err error
}
var testOAuthJSONPost = &hydra.OAuth2ClientJSON{
Name: "test-name-2",
Scope: "some,other,scopes",
GrantTypes: []string{"type2"},
}
var testOAuthJSONPut = &hydra.OAuth2ClientJSON{
ClientID: pointer.StringPtr("test-id-3"),
Name: "test-name-3",
Scope: "yet,another,scope",
GrantTypes: []string{"type3"},
}
func TestCRUD(t *testing.T) {
assert := assert.New(t)
c := hydra.Client{
HTTPClient: &http.Client{},
HydraURL: url.URL{Scheme: schemeHTTP},
}
t.Run("method=get", func(t *testing.T) {
for d, tc := range map[string]server{
"getting registered client": {
http.StatusOK,
testClient,
nil,
},
"getting unregistered client": {
http.StatusNotFound,
emptyBody,
nil,
},
"internal server error when requesting": {
http.StatusInternalServerError,
emptyBody,
errors.New("http request returned unexpected status code"),
},
} {
t.Run(fmt.Sprintf("case/%s", d), func(t *testing.T) {
//given
shouldFind := tc.statusCode == http.StatusOK
h := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
assert.Equal(fmt.Sprintf("%s/%s", c.HydraURL.String(), testID), fmt.Sprintf("%s://%s%s", schemeHTTP, req.Host, req.URL.Path))
assert.Equal(http.MethodGet, req.Method)
w.WriteHeader(tc.statusCode)
w.Write([]byte(tc.respBody))
if shouldFind {
w.Header().Set("Content-type", "application/json")
}
})
runServer(&c, h)
//when
o, found, err := c.GetOAuth2Client(testID)
//then
if tc.err == nil {
require.NoError(t, err)
} else {
require.Error(t, err)
assert.Contains(err.Error(), tc.err.Error())
}
assert.Equal(shouldFind, found)
if shouldFind {
require.NotNil(t, o)
var expected hydra.OAuth2ClientJSON
json.Unmarshal([]byte(testClient), &expected)
assert.Equal(&expected, o)
}
})
}
})
t.Run("method=post", func(t *testing.T) {
for d, tc := range map[string]server{
"with new client": {
http.StatusCreated,
testClientCreated,
nil,
},
"with existing client": {
http.StatusConflict,
emptyBody,
errors.New("requested ID already exists"),
},
"internal server error when requesting": {
http.StatusInternalServerError,
emptyBody,
errors.New("http request returned unexpected status code"),
},
} {
t.Run(fmt.Sprintf("case/%s", d), func(t *testing.T) {
//given
new := tc.statusCode == http.StatusCreated
h := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
assert.Equal(c.HydraURL.String(), fmt.Sprintf("%s://%s%s", schemeHTTP, req.Host, req.URL.Path))
assert.Equal(http.MethodPost, req.Method)
w.WriteHeader(tc.statusCode)
w.Write([]byte(tc.respBody))
if new {
w.Header().Set("Content-type", "application/json")
}
})
runServer(&c, h)
//when
o, err := c.PostOAuth2Client(testOAuthJSONPost)
//then
if tc.err == nil {
require.NoError(t, err)
} else {
require.Error(t, err)
assert.Contains(err.Error(), tc.err.Error())
}
if new {
require.NotNil(t, o)
assert.Equal(testOAuthJSONPost.Name, o.Name)
assert.Equal(testOAuthJSONPost.Scope, o.Scope)
assert.Equal(testOAuthJSONPost.GrantTypes, o.GrantTypes)
assert.NotNil(o.Secret)
assert.NotNil(o.ClientID)
}
})
}
})
t.Run("method=put", func(t *testing.T) {
for d, tc := range map[string]server{
"with registered client": {
http.StatusOK,
testClientUpdated,
nil,
},
"internal server error when requesting": {
http.StatusInternalServerError,
emptyBody,
errors.New("http request returned unexpected status code"),
},
} {
t.Run(fmt.Sprintf("case/%s", d), func(t *testing.T) {
ok := tc.statusCode == http.StatusOK
//given
h := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
assert.Equal(fmt.Sprintf("%s/%s", c.HydraURL.String(), *testOAuthJSONPut.ClientID), fmt.Sprintf("%s://%s%s", schemeHTTP, req.Host, req.URL.Path))
assert.Equal(http.MethodPut, req.Method)
w.WriteHeader(tc.statusCode)
w.Write([]byte(tc.respBody))
if ok {
w.Header().Set("Content-type", "application/json")
}
})
runServer(&c, h)
//when
o, err := c.PutOAuth2Client(testOAuthJSONPut)
//then
if tc.err == nil {
require.NoError(t, err)
} else {
require.Error(t, err)
assert.Contains(err.Error(), tc.err.Error())
}
if ok {
require.NotNil(t, o)
assert.Equal(testOAuthJSONPut.Name, o.Name)
assert.Equal(testOAuthJSONPut.Scope, o.Scope)
assert.Equal(testOAuthJSONPut.GrantTypes, o.GrantTypes)
assert.Equal(testOAuthJSONPut.ClientID, o.ClientID)
assert.NotNil(o.Secret)
}
})
}
})
t.Run("method=delete", func(t *testing.T) {
for d, tc := range map[string]server{
"with registered client": {
statusCode: http.StatusNoContent,
},
"with unregistered client": {
statusCode: http.StatusNotFound,
},
"internal server error when requesting": {
statusCode: http.StatusInternalServerError,
err: errors.New("http request returned unexpected status code"),
},
} {
t.Run(fmt.Sprintf("case/%s", d), func(t *testing.T) {
//given
h := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
assert.Equal(fmt.Sprintf("%s/%s", c.HydraURL.String(), testID), fmt.Sprintf("%s://%s%s", schemeHTTP, req.Host, req.URL.Path))
assert.Equal(http.MethodDelete, req.Method)
w.WriteHeader(tc.statusCode)
})
runServer(&c, h)
//when
err := c.DeleteOAuth2Client(testID)
//then
if tc.err == nil {
require.NoError(t, err)
} else {
require.Error(t, err)
assert.Contains(err.Error(), tc.err.Error())
}
})
}
})
}
func runServer(c *hydra.Client, h http.HandlerFunc) {
s := httptest.NewServer(h)
serverUrl, _ := url.Parse(s.URL)
c.HydraURL = *serverUrl.ResolveReference(&url.URL{Path: clientsEndpoint})
}

11
hydra/types.go Normal file
View File

@ -0,0 +1,11 @@
package hydra
// OAuth2ClientJSON represents an OAuth2 client digestible by ORY Hydra
type OAuth2ClientJSON struct {
ClientID *string `json:"client_id,omitempty"`
Name string `json:"client_name"`
GrantTypes []string `json:"grant_types"`
ResponseTypes []string `json:"response_types,omitempty"`
Scope string `json:"scope"`
Secret *string `json:"client_secret,omitempty"`
}

106
main.go Normal file
View File

@ -0,0 +1,106 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"flag"
"fmt"
"net/http"
"net/url"
"os"
"github.com/ory/hydra-maester/hydra"
hydrav1alpha1 "github.com/ory/hydra-maester/api/v1alpha1"
"github.com/ory/hydra-maester/controllers"
apiv1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
// +kubebuilder:scaffold:imports
)
var (
scheme = runtime.NewScheme()
setupLog = ctrl.Log.WithName("setup")
)
func init() {
apiv1.AddToScheme(scheme)
hydrav1alpha1.AddToScheme(scheme)
// +kubebuilder:scaffold:scheme
}
func main() {
var metricsAddr string
var hydraURL string
var port int
var endpoint string
var enableLeaderElection bool
flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.")
flag.StringVar(&hydraURL, "hydra-url", "", "The address of ORY Hydra")
flag.IntVar(&port, "port", 4445, "Port ORY Hydra is listening on")
flag.StringVar(&endpoint, "endpoint", "/clients", "ORY Hydra's client endpoint")
flag.BoolVar(&enableLeaderElection, "enable-leader-election", false,
"Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager.")
flag.Parse()
ctrl.SetLogger(zap.Logger(true))
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
MetricsBindAddress: metricsAddr,
LeaderElection: enableLeaderElection,
})
if err != nil {
setupLog.Error(err, "unable to start manager")
os.Exit(1)
}
if hydraURL == "" {
setupLog.Error(fmt.Errorf("hydra service address can't be empty"), "unable to create controller", "controller", "OAuth2Client")
os.Exit(1)
}
u, err := url.Parse(fmt.Sprintf("%s:%d", hydraURL, port))
if err != nil {
setupLog.Error(err, "unable to parse ORY Hydra's URL", "controller", "OAuth2Client")
os.Exit(1)
}
err = (&controllers.OAuth2ClientReconciler{
Client: mgr.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("OAuth2Client"),
HydraClient: &hydra.Client{
HydraURL: *u.ResolveReference(&url.URL{Path: endpoint}),
HTTPClient: &http.Client{},
},
}).SetupWithManager(mgr)
if err != nil {
setupLog.Error(err, "unable to create controller", "controller", "OAuth2Client")
os.Exit(1)
}
// +kubebuilder:scaffold:builder
setupLog.Info("starting manager")
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
setupLog.Error(err, "problem running manager")
os.Exit(1)
}
}