Add APIApp crd
[ZITADOPER-1]
This commit is contained in:
176
ops/chart/crds/apiapp-crd.yaml
Normal file
176
ops/chart/crds/apiapp-crd.yaml
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
apiVersion: apiextensions.k8s.io/v1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
controller-gen.kubebuilder.io/version: v0.11.1
|
||||||
|
creationTimestamp: null
|
||||||
|
name: apiapps.zitadel.topmanage.com
|
||||||
|
spec:
|
||||||
|
group: zitadel.topmanage.com
|
||||||
|
names:
|
||||||
|
kind: APIApp
|
||||||
|
listKind: APIAppList
|
||||||
|
plural: apiapps
|
||||||
|
singular: apiapp
|
||||||
|
scope: Namespaced
|
||||||
|
versions:
|
||||||
|
- name: v1alpha1
|
||||||
|
schema:
|
||||||
|
openAPIV3Schema:
|
||||||
|
description: APIApp is the Schema for the apiapps 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/sig-architecture/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/sig-architecture/api-conventions.md#types-kinds'
|
||||||
|
type: string
|
||||||
|
metadata:
|
||||||
|
type: object
|
||||||
|
spec:
|
||||||
|
description: APIAppSpec defines the desired state of APIApp
|
||||||
|
properties:
|
||||||
|
authMethodType:
|
||||||
|
enum:
|
||||||
|
- API_AUTH_METHOD_TYPE_BASIC
|
||||||
|
- API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT
|
||||||
|
type: string
|
||||||
|
projectRef:
|
||||||
|
description: 'INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
|
||||||
|
Important: Run "make" to regenerate code after modifying this file'
|
||||||
|
properties:
|
||||||
|
apiVersion:
|
||||||
|
description: API version of the referent.
|
||||||
|
type: string
|
||||||
|
fieldPath:
|
||||||
|
description: 'If referring to a piece of an object instead of
|
||||||
|
an entire object, this string should contain a valid JSON/Go
|
||||||
|
field access statement, such as desiredState.manifest.containers[2].
|
||||||
|
For example, if the object reference is to a container within
|
||||||
|
a pod, this would take on a value like: "spec.containers{name}"
|
||||||
|
(where "name" refers to the name of the container that triggered
|
||||||
|
the event) or if no container name is specified "spec.containers[2]"
|
||||||
|
(container with index 2 in this pod). This syntax is chosen
|
||||||
|
only to have some well-defined way of referencing a part of
|
||||||
|
an object. TODO: this design is not final and this field is
|
||||||
|
subject to change in the future.'
|
||||||
|
type: string
|
||||||
|
kind:
|
||||||
|
description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names'
|
||||||
|
type: string
|
||||||
|
namespace:
|
||||||
|
description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/'
|
||||||
|
type: string
|
||||||
|
resourceVersion:
|
||||||
|
description: 'Specific resourceVersion to which this reference
|
||||||
|
is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency'
|
||||||
|
type: string
|
||||||
|
uid:
|
||||||
|
description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids'
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
x-kubernetes-map-type: atomic
|
||||||
|
required:
|
||||||
|
- authMethodType
|
||||||
|
- projectRef
|
||||||
|
type: object
|
||||||
|
status:
|
||||||
|
description: APIAppStatus defines the observed state of APIApp
|
||||||
|
properties:
|
||||||
|
appId:
|
||||||
|
default: ""
|
||||||
|
type: string
|
||||||
|
clientId:
|
||||||
|
default: ""
|
||||||
|
type: string
|
||||||
|
conditions:
|
||||||
|
description: 'INSERT ADDITIONAL STATUS FIELD - define observed state
|
||||||
|
of cluster Important: Run "make" to regenerate code after modifying
|
||||||
|
this file'
|
||||||
|
items:
|
||||||
|
description: "Condition contains details for one aspect of the current
|
||||||
|
state of this API Resource. --- This struct is intended for direct
|
||||||
|
use as an array at the field path .status.conditions. For example,
|
||||||
|
\n type FooStatus struct{ // Represents the observations of a
|
||||||
|
foo's current state. // Known .status.conditions.type are: \"Available\",
|
||||||
|
\"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
|
||||||
|
// +listType=map // +listMapKey=type Conditions []metav1.Condition
|
||||||
|
`json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
|
||||||
|
protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
|
||||||
|
properties:
|
||||||
|
lastTransitionTime:
|
||||||
|
description: lastTransitionTime is the last time the condition
|
||||||
|
transitioned from one status to another. This should be when
|
||||||
|
the underlying condition changed. If that is not known, then
|
||||||
|
using the time when the API field changed is acceptable.
|
||||||
|
format: date-time
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
description: message is a human readable message indicating
|
||||||
|
details about the transition. This may be an empty string.
|
||||||
|
maxLength: 32768
|
||||||
|
type: string
|
||||||
|
observedGeneration:
|
||||||
|
description: observedGeneration represents the .metadata.generation
|
||||||
|
that the condition was set based upon. For instance, if .metadata.generation
|
||||||
|
is currently 12, but the .status.conditions[x].observedGeneration
|
||||||
|
is 9, the condition is out of date with respect to the current
|
||||||
|
state of the instance.
|
||||||
|
format: int64
|
||||||
|
minimum: 0
|
||||||
|
type: integer
|
||||||
|
reason:
|
||||||
|
description: reason contains a programmatic identifier indicating
|
||||||
|
the reason for the condition's last transition. Producers
|
||||||
|
of specific condition types may define expected values and
|
||||||
|
meanings for this field, and whether the values are considered
|
||||||
|
a guaranteed API. The value should be a CamelCase string.
|
||||||
|
This field may not be empty.
|
||||||
|
maxLength: 1024
|
||||||
|
minLength: 1
|
||||||
|
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
description: status of the condition, one of True, False, Unknown.
|
||||||
|
enum:
|
||||||
|
- "True"
|
||||||
|
- "False"
|
||||||
|
- Unknown
|
||||||
|
type: string
|
||||||
|
type:
|
||||||
|
description: type of condition in CamelCase or in foo.example.com/CamelCase.
|
||||||
|
--- Many .condition.type values are consistent across resources
|
||||||
|
like Available, but because arbitrary conditions can be useful
|
||||||
|
(see .node.status.conditions), the ability to deconflict is
|
||||||
|
important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
|
||||||
|
maxLength: 316
|
||||||
|
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- lastTransitionTime
|
||||||
|
- message
|
||||||
|
- reason
|
||||||
|
- status
|
||||||
|
- type
|
||||||
|
type: object
|
||||||
|
type: array
|
||||||
|
keyId:
|
||||||
|
default: ""
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- appId
|
||||||
|
- clientId
|
||||||
|
- keyId
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
|
served: true
|
||||||
|
storage: true
|
||||||
|
subresources:
|
||||||
|
status: {}
|
||||||
@@ -170,6 +170,32 @@ rules:
|
|||||||
- list
|
- list
|
||||||
- patch
|
- patch
|
||||||
- watch
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- zitadel.topmanage.com
|
||||||
|
resources:
|
||||||
|
- apiapps
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- zitadel.topmanage.com
|
||||||
|
resources:
|
||||||
|
- apiapps/finalizers
|
||||||
|
verbs:
|
||||||
|
- update
|
||||||
|
- apiGroups:
|
||||||
|
- zitadel.topmanage.com
|
||||||
|
resources:
|
||||||
|
- apiapps/status
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
- apiGroups:
|
- apiGroups:
|
||||||
- zitadel.topmanage.com
|
- zitadel.topmanage.com
|
||||||
resources:
|
resources:
|
||||||
|
|||||||
@@ -56,4 +56,13 @@ resources:
|
|||||||
kind: MachineUser
|
kind: MachineUser
|
||||||
path: bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/api/v1alpha1
|
path: bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/api/v1alpha1
|
||||||
version: v1alpha1
|
version: v1alpha1
|
||||||
|
- api:
|
||||||
|
crdVersion: v1
|
||||||
|
namespaced: true
|
||||||
|
controller: true
|
||||||
|
domain: topmanage.com
|
||||||
|
group: zitadel
|
||||||
|
kind: APIApp
|
||||||
|
path: bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/api/v1alpha1
|
||||||
|
version: v1alpha1
|
||||||
version: "3"
|
version: "3"
|
||||||
|
|||||||
126
src/api/v1alpha1/apiapp_types.go
Normal file
126
src/api/v1alpha1/apiapp_types.go
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024.
|
||||||
|
|
||||||
|
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 (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
|
||||||
|
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
|
||||||
|
|
||||||
|
// APIAppSpec defines the desired state of APIApp
|
||||||
|
type APIAppSpec struct {
|
||||||
|
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
|
||||||
|
// Important: Run "make" to regenerate code after modifying this file
|
||||||
|
ProjectRef ProjectRef `json:"projectRef"`
|
||||||
|
// +kubebuilder:validation:Enum=API_AUTH_METHOD_TYPE_BASIC;API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT
|
||||||
|
AuthMethodType string `json:"authMethodType"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// APIAppStatus defines the observed state of APIApp
|
||||||
|
type APIAppStatus struct {
|
||||||
|
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
|
||||||
|
// Important: Run "make" to regenerate code after modifying this file
|
||||||
|
// +optional
|
||||||
|
// +operator-sdk:csv:customresourcedefinitions:type=status,xDescriptors={"urn:alm:descriptor:io.kubernetes.conditions"}
|
||||||
|
Conditions []metav1.Condition `json:"conditions,omitempty"`
|
||||||
|
// +kubebuilder:default=""
|
||||||
|
AppId string `json:"appId"`
|
||||||
|
// +kubebuilder:default=""
|
||||||
|
KeyId string `json:"keyId"`
|
||||||
|
// +kubebuilder:default=""
|
||||||
|
ClientId string `json:"clientId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *APIAppStatus) SetCondition(condition metav1.Condition) {
|
||||||
|
if d.Conditions == nil {
|
||||||
|
d.Conditions = make([]metav1.Condition, 0)
|
||||||
|
}
|
||||||
|
meta.SetStatusCondition(&d.Conditions, condition)
|
||||||
|
}
|
||||||
|
|
||||||
|
//+kubebuilder:object:root=true
|
||||||
|
//+kubebuilder:subresource:status
|
||||||
|
|
||||||
|
// APIApp is the Schema for the apiapps API
|
||||||
|
type APIApp struct {
|
||||||
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||||
|
|
||||||
|
Spec APIAppSpec `json:"spec,omitempty"`
|
||||||
|
Status APIAppStatus `json:"status,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *APIApp) IsBeingDeleted() bool {
|
||||||
|
return !d.DeletionTimestamp.IsZero()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *APIApp) IsReady() bool {
|
||||||
|
return meta.IsStatusConditionTrue(d.Status.Conditions, ConditionTypeReady)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *APIApp) ZitadelClusterRef(ctx context.Context, refresolver *RefResolver) (*ZitadelClusterRef, error) {
|
||||||
|
project, err := refresolver.ProjectRef(ctx, &d.Spec.ProjectRef, d.Namespace)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
org, err := refresolver.OrganizationRef(ctx, &project.Spec.OrganizationRef, d.Namespace)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ref, err := org.ZitadelClusterRef(ctx, refresolver)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ref, nil
|
||||||
|
}
|
||||||
|
func (d *APIApp) Organization(ctx context.Context, refresolver *RefResolver) (*Organization, error) {
|
||||||
|
project, err := refresolver.ProjectRef(ctx, &d.Spec.ProjectRef, d.Namespace)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
org, err := refresolver.OrganizationRef(ctx, &project.Spec.OrganizationRef, d.Namespace)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return org, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *APIApp) Project(ctx context.Context, refresolver *RefResolver) (*Project, error) {
|
||||||
|
project, err := refresolver.ProjectRef(ctx, &d.Spec.ProjectRef, d.Namespace)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return project, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//+kubebuilder:object:root=true
|
||||||
|
|
||||||
|
// APIAppList contains a list of APIApp
|
||||||
|
type APIAppList struct {
|
||||||
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
metav1.ListMeta `json:"metadata,omitempty"`
|
||||||
|
Items []APIApp `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SchemeBuilder.Register(&APIApp{}, &APIAppList{})
|
||||||
|
}
|
||||||
@@ -26,6 +26,103 @@ import (
|
|||||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
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 *APIApp) DeepCopyInto(out *APIApp) {
|
||||||
|
*out = *in
|
||||||
|
out.TypeMeta = in.TypeMeta
|
||||||
|
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||||
|
out.Spec = in.Spec
|
||||||
|
in.Status.DeepCopyInto(&out.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIApp.
|
||||||
|
func (in *APIApp) DeepCopy() *APIApp {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(APIApp)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||||
|
func (in *APIApp) 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 *APIAppList) DeepCopyInto(out *APIAppList) {
|
||||||
|
*out = *in
|
||||||
|
out.TypeMeta = in.TypeMeta
|
||||||
|
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||||
|
if in.Items != nil {
|
||||||
|
in, out := &in.Items, &out.Items
|
||||||
|
*out = make([]APIApp, len(*in))
|
||||||
|
for i := range *in {
|
||||||
|
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIAppList.
|
||||||
|
func (in *APIAppList) DeepCopy() *APIAppList {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(APIAppList)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||||
|
func (in *APIAppList) 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 *APIAppSpec) DeepCopyInto(out *APIAppSpec) {
|
||||||
|
*out = *in
|
||||||
|
out.ProjectRef = in.ProjectRef
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIAppSpec.
|
||||||
|
func (in *APIAppSpec) DeepCopy() *APIAppSpec {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(APIAppSpec)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *APIAppStatus) DeepCopyInto(out *APIAppStatus) {
|
||||||
|
*out = *in
|
||||||
|
if in.Conditions != nil {
|
||||||
|
in, out := &in.Conditions, &out.Conditions
|
||||||
|
*out = make([]v1.Condition, len(*in))
|
||||||
|
for i := range *in {
|
||||||
|
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIAppStatus.
|
||||||
|
func (in *APIAppStatus) DeepCopy() *APIAppStatus {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(APIAppStatus)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *CrdbClusterRef) DeepCopyInto(out *CrdbClusterRef) {
|
func (in *CrdbClusterRef) DeepCopyInto(out *CrdbClusterRef) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
|||||||
@@ -133,6 +133,10 @@ func main() {
|
|||||||
setupLog.Error(err, "unable to create controller", "controller", "MachineUser")
|
setupLog.Error(err, "unable to create controller", "controller", "MachineUser")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
if err = controller.NewAPIAppReconciler(client, refResolver, builder, conditionReady, requeueZitadel).SetupWithManager(mgr); err != nil {
|
||||||
|
setupLog.Error(err, "unable to create controller", "controller", "APIApp")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
//+kubebuilder:scaffold:builder
|
//+kubebuilder:scaffold:builder
|
||||||
|
|
||||||
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
|
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
|
||||||
|
|||||||
177
src/config/crd/bases/zitadel.topmanage.com_apiapps.yaml
Normal file
177
src/config/crd/bases/zitadel.topmanage.com_apiapps.yaml
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
---
|
||||||
|
apiVersion: apiextensions.k8s.io/v1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
controller-gen.kubebuilder.io/version: v0.11.1
|
||||||
|
creationTimestamp: null
|
||||||
|
name: apiapps.zitadel.topmanage.com
|
||||||
|
spec:
|
||||||
|
group: zitadel.topmanage.com
|
||||||
|
names:
|
||||||
|
kind: APIApp
|
||||||
|
listKind: APIAppList
|
||||||
|
plural: apiapps
|
||||||
|
singular: apiapp
|
||||||
|
scope: Namespaced
|
||||||
|
versions:
|
||||||
|
- name: v1alpha1
|
||||||
|
schema:
|
||||||
|
openAPIV3Schema:
|
||||||
|
description: APIApp is the Schema for the apiapps 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/sig-architecture/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/sig-architecture/api-conventions.md#types-kinds'
|
||||||
|
type: string
|
||||||
|
metadata:
|
||||||
|
type: object
|
||||||
|
spec:
|
||||||
|
description: APIAppSpec defines the desired state of APIApp
|
||||||
|
properties:
|
||||||
|
authMethodType:
|
||||||
|
enum:
|
||||||
|
- API_AUTH_METHOD_TYPE_BASIC
|
||||||
|
- API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT
|
||||||
|
type: string
|
||||||
|
projectRef:
|
||||||
|
description: 'INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
|
||||||
|
Important: Run "make" to regenerate code after modifying this file'
|
||||||
|
properties:
|
||||||
|
apiVersion:
|
||||||
|
description: API version of the referent.
|
||||||
|
type: string
|
||||||
|
fieldPath:
|
||||||
|
description: 'If referring to a piece of an object instead of
|
||||||
|
an entire object, this string should contain a valid JSON/Go
|
||||||
|
field access statement, such as desiredState.manifest.containers[2].
|
||||||
|
For example, if the object reference is to a container within
|
||||||
|
a pod, this would take on a value like: "spec.containers{name}"
|
||||||
|
(where "name" refers to the name of the container that triggered
|
||||||
|
the event) or if no container name is specified "spec.containers[2]"
|
||||||
|
(container with index 2 in this pod). This syntax is chosen
|
||||||
|
only to have some well-defined way of referencing a part of
|
||||||
|
an object. TODO: this design is not final and this field is
|
||||||
|
subject to change in the future.'
|
||||||
|
type: string
|
||||||
|
kind:
|
||||||
|
description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names'
|
||||||
|
type: string
|
||||||
|
namespace:
|
||||||
|
description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/'
|
||||||
|
type: string
|
||||||
|
resourceVersion:
|
||||||
|
description: 'Specific resourceVersion to which this reference
|
||||||
|
is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency'
|
||||||
|
type: string
|
||||||
|
uid:
|
||||||
|
description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids'
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
x-kubernetes-map-type: atomic
|
||||||
|
required:
|
||||||
|
- authMethodType
|
||||||
|
- projectRef
|
||||||
|
type: object
|
||||||
|
status:
|
||||||
|
description: APIAppStatus defines the observed state of APIApp
|
||||||
|
properties:
|
||||||
|
appId:
|
||||||
|
default: ""
|
||||||
|
type: string
|
||||||
|
clientId:
|
||||||
|
default: ""
|
||||||
|
type: string
|
||||||
|
conditions:
|
||||||
|
description: 'INSERT ADDITIONAL STATUS FIELD - define observed state
|
||||||
|
of cluster Important: Run "make" to regenerate code after modifying
|
||||||
|
this file'
|
||||||
|
items:
|
||||||
|
description: "Condition contains details for one aspect of the current
|
||||||
|
state of this API Resource. --- This struct is intended for direct
|
||||||
|
use as an array at the field path .status.conditions. For example,
|
||||||
|
\n type FooStatus struct{ // Represents the observations of a
|
||||||
|
foo's current state. // Known .status.conditions.type are: \"Available\",
|
||||||
|
\"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
|
||||||
|
// +listType=map // +listMapKey=type Conditions []metav1.Condition
|
||||||
|
`json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
|
||||||
|
protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
|
||||||
|
properties:
|
||||||
|
lastTransitionTime:
|
||||||
|
description: lastTransitionTime is the last time the condition
|
||||||
|
transitioned from one status to another. This should be when
|
||||||
|
the underlying condition changed. If that is not known, then
|
||||||
|
using the time when the API field changed is acceptable.
|
||||||
|
format: date-time
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
description: message is a human readable message indicating
|
||||||
|
details about the transition. This may be an empty string.
|
||||||
|
maxLength: 32768
|
||||||
|
type: string
|
||||||
|
observedGeneration:
|
||||||
|
description: observedGeneration represents the .metadata.generation
|
||||||
|
that the condition was set based upon. For instance, if .metadata.generation
|
||||||
|
is currently 12, but the .status.conditions[x].observedGeneration
|
||||||
|
is 9, the condition is out of date with respect to the current
|
||||||
|
state of the instance.
|
||||||
|
format: int64
|
||||||
|
minimum: 0
|
||||||
|
type: integer
|
||||||
|
reason:
|
||||||
|
description: reason contains a programmatic identifier indicating
|
||||||
|
the reason for the condition's last transition. Producers
|
||||||
|
of specific condition types may define expected values and
|
||||||
|
meanings for this field, and whether the values are considered
|
||||||
|
a guaranteed API. The value should be a CamelCase string.
|
||||||
|
This field may not be empty.
|
||||||
|
maxLength: 1024
|
||||||
|
minLength: 1
|
||||||
|
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
description: status of the condition, one of True, False, Unknown.
|
||||||
|
enum:
|
||||||
|
- "True"
|
||||||
|
- "False"
|
||||||
|
- Unknown
|
||||||
|
type: string
|
||||||
|
type:
|
||||||
|
description: type of condition in CamelCase or in foo.example.com/CamelCase.
|
||||||
|
--- Many .condition.type values are consistent across resources
|
||||||
|
like Available, but because arbitrary conditions can be useful
|
||||||
|
(see .node.status.conditions), the ability to deconflict is
|
||||||
|
important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
|
||||||
|
maxLength: 316
|
||||||
|
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- lastTransitionTime
|
||||||
|
- message
|
||||||
|
- reason
|
||||||
|
- status
|
||||||
|
- type
|
||||||
|
type: object
|
||||||
|
type: array
|
||||||
|
keyId:
|
||||||
|
default: ""
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- appId
|
||||||
|
- clientId
|
||||||
|
- keyId
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
|
served: true
|
||||||
|
storage: true
|
||||||
|
subresources:
|
||||||
|
status: {}
|
||||||
@@ -7,6 +7,7 @@ resources:
|
|||||||
- bases/zitadel.topmanage.com_projects.yaml
|
- bases/zitadel.topmanage.com_projects.yaml
|
||||||
- bases/zitadel.topmanage.com_oidcapps.yaml
|
- bases/zitadel.topmanage.com_oidcapps.yaml
|
||||||
- bases/zitadel.topmanage.com_machineusers.yaml
|
- bases/zitadel.topmanage.com_machineusers.yaml
|
||||||
|
- bases/zitadel.topmanage.com_apiapps.yaml
|
||||||
#+kubebuilder:scaffold:crdkustomizeresource
|
#+kubebuilder:scaffold:crdkustomizeresource
|
||||||
|
|
||||||
patchesStrategicMerge:
|
patchesStrategicMerge:
|
||||||
@@ -17,6 +18,7 @@ patchesStrategicMerge:
|
|||||||
#- patches/webhook_in_projects.yaml
|
#- patches/webhook_in_projects.yaml
|
||||||
#- patches/webhook_in_oidcapps.yaml
|
#- patches/webhook_in_oidcapps.yaml
|
||||||
#- patches/webhook_in_machineusers.yaml
|
#- patches/webhook_in_machineusers.yaml
|
||||||
|
#- patches/webhook_in_apiapps.yaml
|
||||||
#+kubebuilder:scaffold:crdkustomizewebhookpatch
|
#+kubebuilder:scaffold:crdkustomizewebhookpatch
|
||||||
|
|
||||||
# [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix.
|
# [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix.
|
||||||
@@ -26,6 +28,7 @@ patchesStrategicMerge:
|
|||||||
#- patches/cainjection_in_projects.yaml
|
#- patches/cainjection_in_projects.yaml
|
||||||
#- patches/cainjection_in_oidcapps.yaml
|
#- patches/cainjection_in_oidcapps.yaml
|
||||||
#- patches/cainjection_in_machineusers.yaml
|
#- patches/cainjection_in_machineusers.yaml
|
||||||
|
#- patches/cainjection_in_apiapps.yaml
|
||||||
#+kubebuilder:scaffold:crdkustomizecainjectionpatch
|
#+kubebuilder:scaffold:crdkustomizecainjectionpatch
|
||||||
|
|
||||||
# the following config is for teaching kustomize how to do kustomization for CRDs.
|
# the following config is for teaching kustomize how to do kustomization for CRDs.
|
||||||
|
|||||||
7
src/config/crd/patches/cainjection_in_apiapps.yaml
Normal file
7
src/config/crd/patches/cainjection_in_apiapps.yaml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# The following patch adds a directive for certmanager to inject CA into the CRD
|
||||||
|
apiVersion: apiextensions.k8s.io/v1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME
|
||||||
|
name: apiapps.zitadel.topmanage.com
|
||||||
16
src/config/crd/patches/webhook_in_apiapps.yaml
Normal file
16
src/config/crd/patches/webhook_in_apiapps.yaml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# The following patch enables a conversion webhook for the CRD
|
||||||
|
apiVersion: apiextensions.k8s.io/v1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
name: apiapps.zitadel.topmanage.com
|
||||||
|
spec:
|
||||||
|
conversion:
|
||||||
|
strategy: Webhook
|
||||||
|
webhook:
|
||||||
|
clientConfig:
|
||||||
|
service:
|
||||||
|
namespace: system
|
||||||
|
name: webhook-service
|
||||||
|
path: /convert
|
||||||
|
conversionReviewVersions:
|
||||||
|
- v1
|
||||||
31
src/config/rbac/apiapp_editor_role.yaml
Normal file
31
src/config/rbac/apiapp_editor_role.yaml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# permissions for end users to edit apiapps.
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: clusterrole
|
||||||
|
app.kubernetes.io/instance: apiapp-editor-role
|
||||||
|
app.kubernetes.io/component: rbac
|
||||||
|
app.kubernetes.io/created-by: src
|
||||||
|
app.kubernetes.io/part-of: src
|
||||||
|
app.kubernetes.io/managed-by: kustomize
|
||||||
|
name: apiapp-editor-role
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- zitadel.topmanage.com
|
||||||
|
resources:
|
||||||
|
- apiapps
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- zitadel.topmanage.com
|
||||||
|
resources:
|
||||||
|
- apiapps/status
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
27
src/config/rbac/apiapp_viewer_role.yaml
Normal file
27
src/config/rbac/apiapp_viewer_role.yaml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# permissions for end users to view apiapps.
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: clusterrole
|
||||||
|
app.kubernetes.io/instance: apiapp-viewer-role
|
||||||
|
app.kubernetes.io/component: rbac
|
||||||
|
app.kubernetes.io/created-by: src
|
||||||
|
app.kubernetes.io/part-of: src
|
||||||
|
app.kubernetes.io/managed-by: kustomize
|
||||||
|
name: apiapp-viewer-role
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- zitadel.topmanage.com
|
||||||
|
resources:
|
||||||
|
- apiapps
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- zitadel.topmanage.com
|
||||||
|
resources:
|
||||||
|
- apiapps/status
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
@@ -170,6 +170,32 @@ rules:
|
|||||||
- list
|
- list
|
||||||
- patch
|
- patch
|
||||||
- watch
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- zitadel.topmanage.com
|
||||||
|
resources:
|
||||||
|
- apiapps
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- zitadel.topmanage.com
|
||||||
|
resources:
|
||||||
|
- apiapps/finalizers
|
||||||
|
verbs:
|
||||||
|
- update
|
||||||
|
- apiGroups:
|
||||||
|
- zitadel.topmanage.com
|
||||||
|
resources:
|
||||||
|
- apiapps/status
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
- apiGroups:
|
- apiGroups:
|
||||||
- zitadel.topmanage.com
|
- zitadel.topmanage.com
|
||||||
resources:
|
resources:
|
||||||
|
|||||||
@@ -5,4 +5,5 @@ resources:
|
|||||||
- zitadel_v1alpha1_project.yaml
|
- zitadel_v1alpha1_project.yaml
|
||||||
- zitadel_v1alpha1_oidcapp.yaml
|
- zitadel_v1alpha1_oidcapp.yaml
|
||||||
- zitadel_v1alpha1_machineuser.yaml
|
- zitadel_v1alpha1_machineuser.yaml
|
||||||
|
- zitadel_v1alpha1_apiapp.yaml
|
||||||
#+kubebuilder:scaffold:manifestskustomizesamples
|
#+kubebuilder:scaffold:manifestskustomizesamples
|
||||||
|
|||||||
12
src/config/samples/zitadel_v1alpha1_apiapp.yaml
Normal file
12
src/config/samples/zitadel_v1alpha1_apiapp.yaml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
apiVersion: zitadel.topmanage.com/v1alpha1
|
||||||
|
kind: APIApp
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: apiapp
|
||||||
|
app.kubernetes.io/instance: apiapp-sample
|
||||||
|
app.kubernetes.io/part-of: src
|
||||||
|
app.kubernetes.io/managed-by: kustomize
|
||||||
|
app.kubernetes.io/created-by: src
|
||||||
|
name: apiapp-sample
|
||||||
|
spec:
|
||||||
|
# TODO(user): Add fields here
|
||||||
@@ -13,6 +13,7 @@ require (
|
|||||||
github.com/sethvargo/go-password v0.2.0
|
github.com/sethvargo/go-password v0.2.0
|
||||||
github.com/zitadel/oidc v1.13.5
|
github.com/zitadel/oidc v1.13.5
|
||||||
github.com/zitadel/zitadel-go/v2 v2.1.10
|
github.com/zitadel/zitadel-go/v2 v2.1.10
|
||||||
|
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8
|
||||||
golang.org/x/oauth2 v0.18.0
|
golang.org/x/oauth2 v0.18.0
|
||||||
google.golang.org/grpc v1.62.1
|
google.golang.org/grpc v1.62.1
|
||||||
google.golang.org/protobuf v1.33.0
|
google.golang.org/protobuf v1.33.0
|
||||||
@@ -82,7 +83,6 @@ require (
|
|||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
go.uber.org/zap v1.25.0 // indirect
|
go.uber.org/zap v1.25.0 // indirect
|
||||||
golang.org/x/crypto v0.21.0 // indirect
|
golang.org/x/crypto v0.21.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect
|
|
||||||
golang.org/x/net v0.22.0 // indirect
|
golang.org/x/net v0.22.0 // indirect
|
||||||
golang.org/x/sys v0.18.0 // indirect
|
golang.org/x/sys v0.18.0 // indirect
|
||||||
golang.org/x/term v0.18.0 // indirect
|
golang.org/x/term v0.18.0 // indirect
|
||||||
|
|||||||
261
src/internal/controller/apiapp_controller.go
Normal file
261
src/internal/controller/apiapp_controller.go
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024.
|
||||||
|
|
||||||
|
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 controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
zitadelv1alpha1 "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/api/v1alpha1"
|
||||||
|
"bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/builder"
|
||||||
|
condition "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/condition"
|
||||||
|
"bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/controller/zitadel"
|
||||||
|
"github.com/zitadel/zitadel-go/v2/pkg/client/management"
|
||||||
|
"github.com/zitadel/zitadel-go/v2/pkg/client/middleware"
|
||||||
|
app "github.com/zitadel/zitadel-go/v2/pkg/client/zitadel/app"
|
||||||
|
"github.com/zitadel/zitadel-go/v2/pkg/client/zitadel/authn"
|
||||||
|
pb "github.com/zitadel/zitadel-go/v2/pkg/client/zitadel/management"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
"k8s.io/client-go/util/workqueue"
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
ctrlClient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/controller"
|
||||||
|
)
|
||||||
|
|
||||||
|
// APIAppReconciler reconciles a APIApp object
|
||||||
|
type APIAppReconciler struct {
|
||||||
|
client.Client
|
||||||
|
RefResolver *zitadelv1alpha1.RefResolver
|
||||||
|
ConditionReady *condition.Ready
|
||||||
|
RequeueInterval time.Duration
|
||||||
|
Builder *builder.Builder
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAPIAppReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, builder *builder.Builder, conditionReady *condition.Ready,
|
||||||
|
requeueInterval time.Duration) *APIAppReconciler {
|
||||||
|
return &APIAppReconciler{
|
||||||
|
Client: client,
|
||||||
|
RefResolver: refResolver,
|
||||||
|
ConditionReady: conditionReady,
|
||||||
|
RequeueInterval: requeueInterval,
|
||||||
|
Builder: builder,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=apiapps,verbs=get;list;watch;create;update;patch;delete
|
||||||
|
//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=apiapps/status,verbs=get;update;patch
|
||||||
|
//+kubebuilder:rbac:groups=zitadel.topmanage.com,resources=apiapps/finalizers,verbs=update
|
||||||
|
|
||||||
|
// Reconcile is part of the main kubernetes reconciliation loop which aims to
|
||||||
|
// move the current state of the cluster closer to the desired state.
|
||||||
|
func (r *APIAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||||
|
var APIApp zitadelv1alpha1.APIApp
|
||||||
|
if err := r.Get(ctx, req.NamespacedName, &APIApp); err != nil {
|
||||||
|
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||||
|
}
|
||||||
|
wr := newWrappedAPIAppReconciler(r.Client, r.RefResolver, r.Builder, &APIApp)
|
||||||
|
wf := newWrappedAPIAppFinalizer(r.Client, &APIApp, r.RefResolver)
|
||||||
|
tf := zitadel.NewZitadelFinalizer(r.Client, wf)
|
||||||
|
tr := zitadel.NewZitadelReconciler(r.Client, r.ConditionReady, wr, tf, r.RequeueInterval)
|
||||||
|
|
||||||
|
result, err := tr.Reconcile(ctx, &APIApp)
|
||||||
|
if err != nil {
|
||||||
|
return result, fmt.Errorf("error reconciling in APIAppReconciler: %v", err)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type wrappedAPIAppReconciler struct {
|
||||||
|
client.Client
|
||||||
|
refResolver *zitadelv1alpha1.RefResolver
|
||||||
|
APIApp *zitadelv1alpha1.APIApp
|
||||||
|
Builder *builder.Builder
|
||||||
|
}
|
||||||
|
|
||||||
|
func newWrappedAPIAppReconciler(client client.Client, refResolver *zitadelv1alpha1.RefResolver, builder *builder.Builder,
|
||||||
|
APIApp *zitadelv1alpha1.APIApp) zitadel.WrappedReconciler {
|
||||||
|
return &wrappedAPIAppReconciler{
|
||||||
|
Client: client,
|
||||||
|
refResolver: refResolver,
|
||||||
|
APIApp: APIApp,
|
||||||
|
Builder: builder,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type apiAppReoncilePhase struct {
|
||||||
|
Name string
|
||||||
|
Reconcile func(context.Context, *management.Client) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wr *wrappedAPIAppReconciler) Reconcile(ctx context.Context, ztdClient *management.Client) error {
|
||||||
|
phases := []projectReconcilePhase{
|
||||||
|
{
|
||||||
|
Name: "apiapp",
|
||||||
|
Reconcile: wr.reconcileApp,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "keys",
|
||||||
|
Reconcile: wr.reconcileKeys,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, p := range phases {
|
||||||
|
err := p.Reconcile(ctx, ztdClient)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wr *wrappedAPIAppReconciler) reconcileApp(ctx context.Context, ztdClient *management.Client) error {
|
||||||
|
org, err := wr.APIApp.Organization(ctx, wr.refResolver)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
project, err := wr.APIApp.Project(ctx, wr.refResolver)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if wr.APIApp.Status.AppId != "" {
|
||||||
|
appResp, err := ztdClient.GetAppByID(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.GetAppByIDRequest{
|
||||||
|
ProjectId: project.Status.ProjectId,
|
||||||
|
AppId: string(wr.APIApp.Status.AppId),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error getting APIApp: %v", err)
|
||||||
|
}
|
||||||
|
if appResp.App != nil {
|
||||||
|
_, err := ztdClient.UpdateAPIAppConfig(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.UpdateAPIAppConfigRequest{ProjectId: project.Status.ProjectId, AppId: wr.APIApp.Status.AppId,
|
||||||
|
AuthMethodType: app.APIAuthMethodType(app.APIAuthMethodType_value[wr.APIApp.Spec.AuthMethodType]),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if !strings.Contains(err.Error(), "No changes") {
|
||||||
|
return fmt.Errorf("Error updating APIApp: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := ztdClient.AddAPIApp(middleware.SetOrgID(ctx, org.Status.OrgId),
|
||||||
|
&pb.AddAPIAppRequest{
|
||||||
|
Name: wr.APIApp.Name,
|
||||||
|
ProjectId: project.Status.ProjectId,
|
||||||
|
AuthMethodType: app.APIAuthMethodType(app.APIAuthMethodType_value[wr.APIApp.Spec.AuthMethodType]),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "AlreadyExists") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("error creating APIApp in Zitadel: %v", err)
|
||||||
|
}
|
||||||
|
key := types.NamespacedName{
|
||||||
|
Name: wr.APIApp.Name + "-client-secret",
|
||||||
|
Namespace: wr.APIApp.Namespace,
|
||||||
|
}
|
||||||
|
|
||||||
|
secretData := map[string][]byte{"client-secret": []byte(resp.ClientSecret)}
|
||||||
|
secret, err := wr.Builder.BuildSecret(builder.SecretOpts{Immutable: true, Zitadel: nil, Key: key, Data: secretData}, wr.APIApp)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error building Secret: %v", err)
|
||||||
|
}
|
||||||
|
if err := wr.Create(ctx, secret); err != nil {
|
||||||
|
return fmt.Errorf("error creating Client-secret Secret: %v", err)
|
||||||
|
}
|
||||||
|
patch := ctrlClient.MergeFrom(wr.APIApp.DeepCopy())
|
||||||
|
wr.APIApp.Status.AppId = resp.AppId
|
||||||
|
wr.APIApp.Status.ClientId = resp.ClientId
|
||||||
|
return wr.Client.Status().Patch(ctx, wr.APIApp, patch)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wr *wrappedAPIAppReconciler) reconcileKeys(ctx context.Context, ztdClient *management.Client) error {
|
||||||
|
if wr.APIApp.Spec.AuthMethodType == "API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT" {
|
||||||
|
org, err := wr.APIApp.Organization(ctx, wr.refResolver)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
project, err := wr.APIApp.Project(ctx, wr.refResolver)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if wr.APIApp.Status.KeyId != "" {
|
||||||
|
_, err = ztdClient.GetAppKey(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.GetAppKeyRequest{
|
||||||
|
ProjectId: project.Status.ProjectId,
|
||||||
|
AppId: wr.APIApp.Status.AppId,
|
||||||
|
KeyId: wr.APIApp.Status.KeyId,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "AlreadyExists") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp, err := ztdClient.AddAppKey(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.AddAppKeyRequest{
|
||||||
|
ProjectId: project.Status.ProjectId,
|
||||||
|
AppId: wr.APIApp.Status.AppId,
|
||||||
|
Type: authn.KeyType_KEY_TYPE_JSON,
|
||||||
|
ExpirationDate: nil,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error adding Key to app: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
key := types.NamespacedName{
|
||||||
|
Name: wr.APIApp.Name + "-privatekey-secret",
|
||||||
|
Namespace: wr.APIApp.Namespace,
|
||||||
|
}
|
||||||
|
|
||||||
|
secretData := map[string][]byte{"key.json": resp.KeyDetails}
|
||||||
|
secret, err := wr.Builder.BuildSecret(builder.SecretOpts{Immutable: true, Zitadel: nil, Key: key, Data: secretData}, wr.APIApp)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error building Secret: %v", err)
|
||||||
|
}
|
||||||
|
if err := wr.Create(ctx, secret); err != nil {
|
||||||
|
return fmt.Errorf("error creating Client-secret Secret: %v", err)
|
||||||
|
}
|
||||||
|
patch := ctrlClient.MergeFrom(wr.APIApp.DeepCopy())
|
||||||
|
wr.APIApp.Status.KeyId = resp.Id
|
||||||
|
return wr.Client.Status().Patch(ctx, wr.APIApp, patch)
|
||||||
|
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wr *wrappedAPIAppReconciler) PatchStatus(ctx context.Context, patcher condition.Patcher) error {
|
||||||
|
patch := client.MergeFrom(wr.APIApp.DeepCopy())
|
||||||
|
patcher(&wr.APIApp.Status)
|
||||||
|
|
||||||
|
if err := wr.Client.Status().Patch(ctx, wr.APIApp, patch); err != nil {
|
||||||
|
return fmt.Errorf("error patching APIApp status: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupWithManager sets up the controller with the Manager.
|
||||||
|
func (r *APIAppReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||||
|
return ctrl.NewControllerManagedBy(mgr).
|
||||||
|
For(&zitadelv1alpha1.APIApp{}).
|
||||||
|
Owns(&corev1.Secret{}).
|
||||||
|
WithOptions(controller.Options{RateLimiter: workqueue.NewItemExponentialFailureRateLimiter(time.Millisecond*500, time.Minute*3)}).
|
||||||
|
Complete(r)
|
||||||
|
}
|
||||||
91
src/internal/controller/apiapp_controller_finalizer.go
Normal file
91
src/internal/controller/apiapp_controller_finalizer.go
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
zitadelv1alpha1 "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/api/v1alpha1"
|
||||||
|
"bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/controller/zitadel"
|
||||||
|
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel-go/v2/pkg/client/management"
|
||||||
|
"github.com/zitadel/zitadel-go/v2/pkg/client/middleware"
|
||||||
|
pb "github.com/zitadel/zitadel-go/v2/pkg/client/zitadel/management"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
ctrlClient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
APIAppFinalizerName = "apiapp.zitadel.topmanage.com/apiapp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type wrappedAPIAppFinalizer struct {
|
||||||
|
client.Client
|
||||||
|
APIApp *zitadelv1alpha1.APIApp
|
||||||
|
refresolver *zitadelv1alpha1.RefResolver
|
||||||
|
}
|
||||||
|
|
||||||
|
func newWrappedAPIAppFinalizer(client client.Client, APIApp *zitadelv1alpha1.APIApp, refresolver *zitadelv1alpha1.RefResolver) zitadel.WrappedFinalizer {
|
||||||
|
return &wrappedAPIAppFinalizer{
|
||||||
|
Client: client,
|
||||||
|
APIApp: APIApp,
|
||||||
|
refresolver: refresolver,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wf *wrappedAPIAppFinalizer) AddFinalizer(ctx context.Context) error {
|
||||||
|
if wf.ContainsFinalizer() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return wf.patch(ctx, wf.APIApp, func(APIApp *zitadelv1alpha1.APIApp) {
|
||||||
|
controllerutil.AddFinalizer(APIApp, APIAppFinalizerName)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wf *wrappedAPIAppFinalizer) RemoveFinalizer(ctx context.Context) error {
|
||||||
|
if !wf.ContainsFinalizer() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return wf.patch(ctx, wf.APIApp, func(APIApp *zitadelv1alpha1.APIApp) {
|
||||||
|
controllerutil.RemoveFinalizer(wf.APIApp, APIAppFinalizerName)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wr *wrappedAPIAppFinalizer) ContainsFinalizer() bool {
|
||||||
|
return controllerutil.ContainsFinalizer(wr.APIApp, APIAppFinalizerName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wf *wrappedAPIAppFinalizer) Reconcile(ctx context.Context, ztdClient *management.Client) error {
|
||||||
|
if wf.APIApp.Status.AppId == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
org, err := wf.APIApp.Organization(ctx, wf.refresolver)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
project, err := wf.APIApp.Project(ctx, wf.refresolver)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = ztdClient.RemoveApp(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.RemoveAppRequest{ProjectId: project.Status.ProjectId, AppId: wf.APIApp.Status.AppId})
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "doesn't exist") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wr *wrappedAPIAppFinalizer) patch(ctx context.Context, APIApp *zitadelv1alpha1.APIApp,
|
||||||
|
patchFn func(*zitadelv1alpha1.APIApp)) error {
|
||||||
|
patch := ctrlClient.MergeFrom(APIApp.DeepCopy())
|
||||||
|
patchFn(APIApp)
|
||||||
|
|
||||||
|
if err := wr.Client.Patch(ctx, APIApp, patch); err != nil {
|
||||||
|
return fmt.Errorf("error patching APIApp finalizer: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -84,39 +84,7 @@ func newWrappedMachineUserReconciler(client client.Client, refResolver *zitadelv
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (wr *wrappedMachineUserReconciler) Reconcile(ctx context.Context, ztdClient *management.Client) error {
|
func (wr *wrappedMachineUserReconciler) Reconcile(ctx context.Context, ztdClient *management.Client) error {
|
||||||
// if wr.MachineUser.Status.AppId != "" {
|
// TODO: update machine user
|
||||||
// appResp, err := ztdClient.GetAppByID(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.GetAppByIDRequest{
|
|
||||||
// ProjectId: project.Status.ProjectId,
|
|
||||||
// AppId: string(wr.MachineUser.Status.AppId),
|
|
||||||
// })
|
|
||||||
// if err != nil {
|
|
||||||
// return fmt.Errorf("Error getting MachineUser: %v", err)
|
|
||||||
// }
|
|
||||||
// if appResp.App != nil {
|
|
||||||
// _, err := ztdClient.UpdateMachineUserConfig(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.UpdateMachineUserConfigRequest{ProjectId: project.Status.ProjectId, AppId: wr.MachineUser.Status.AppId,
|
|
||||||
// RedirectUris: wr.MachineUser.Spec.RedirectUris,
|
|
||||||
// ResponseTypes: responseTypes,
|
|
||||||
// GrantTypes: grantTypes,
|
|
||||||
// AppType: app.MachineUserType(app.MachineUserType_value[wr.MachineUser.Spec.AppType]),
|
|
||||||
// AuthMethodType: app.OIDCAuthMethodType(app.OIDCAuthMethodType_value[wr.MachineUser.Spec.AuthMethodType]),
|
|
||||||
// PostLogoutRedirectUris: wr.MachineUser.Spec.PostLogoutRedirectUris,
|
|
||||||
// DevMode: wr.MachineUser.Spec.DevMode,
|
|
||||||
// AccessTokenType: app.OIDCTokenType(app.OIDCTokenType_value[wr.MachineUser.Spec.AccessTokenType]),
|
|
||||||
// AccessTokenRoleAssertion: wr.MachineUser.Spec.AccessTokenRoleAssertion,
|
|
||||||
// IdTokenRoleAssertion: wr.MachineUser.Spec.IdTokenRoleAssertion,
|
|
||||||
// IdTokenUserinfoAssertion: wr.MachineUser.Spec.IdTokenUserinfoAssertion,
|
|
||||||
// ClockSkew: durationpb.New(wr.MachineUser.Spec.ClockSkew.Duration),
|
|
||||||
// AdditionalOrigins: wr.MachineUser.Spec.AdditionalOrigins,
|
|
||||||
// SkipNativeAppSuccessPage: wr.MachineUser.Spec.SkipNativeAppSuccessPage,
|
|
||||||
// })
|
|
||||||
// if err != nil {
|
|
||||||
// if !strings.Contains(err.Error(), "No changes") {
|
|
||||||
// return fmt.Errorf("Error updating MachineUser: %v", err)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
zitadel, err := wr.refResolver.ZitadelCluster(ctx, &wr.MachineUser.Spec.ZitadelClusterRef, wr.MachineUser.Namespace)
|
zitadel, err := wr.refResolver.ZitadelCluster(ctx, &wr.MachineUser.Spec.ZitadelClusterRef, wr.MachineUser.Namespace)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
Reference in New Issue
Block a user