diff --git a/api/v1alpha1/irsasetup_types.go b/api/v1alpha1/irsasetup_types.go index dab2ea4..ca2cfb4 100644 --- a/api/v1alpha1/irsasetup_types.go +++ b/api/v1alpha1/irsasetup_types.go @@ -17,6 +17,8 @@ limitations under the License. package v1alpha1 import ( + "github.com/fluxcd/pkg/apis/meta" + apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -71,18 +73,59 @@ type SecretRef struct { // IRSASetupStatus defines the observed state of IRSASetup type IRSASetupStatus struct { - SelfHostedSetup CommonStatus `json:"selfHostedSetup,omitempty"` - CommonSetup CommonStatus `json:"commonSetup,omitempty"` + SelfHostedSetup []metav1.Condition `json:"selfHostedSetup,omitempty"` } -// CommonStatus is a set of status attributes -type CommonStatus struct { - Healthy bool `json:"healthy"` - Errors []string `json:"errors,omitempty"` +// GetStatusConditions returns a pointer to the Status.Conditions slice +func (in *IRSASetup) GetSelfhostedStatusConditions() *[]metav1.Condition { + return &in.Status.SelfHostedSetup } +func IRSASetupSelfHostedReady(irsa IRSASetup, reason, message string) IRSASetup { + newCondition := metav1.Condition{ + Type: meta.ReadyCondition, + Status: metav1.ConditionTrue, + Reason: reason, + Message: message, + } + apimeta.SetStatusCondition(irsa.GetSelfhostedStatusConditions(), newCondition) + return irsa +} + +func IRSASetupSelfHostedNotReady(irsa IRSASetup, reason, message string) IRSASetup { + newCondition := metav1.Condition{ + Type: meta.ReadyCondition, + Status: metav1.ConditionFalse, + Reason: reason, + Message: message, + } + apimeta.SetStatusCondition(irsa.GetSelfhostedStatusConditions(), newCondition) + return irsa +} + +// IRSASetupSelfHostedReadyStatus +func IRSASetupSelfHostedReadyStatus(irsa IRSASetup) *metav1.Condition { + if c := apimeta.FindStatusCondition(irsa.Status.SelfHostedSetup, meta.ReadyCondition); c != nil { + // return c, c.Status == metav1.ConditionTrue + return c + } + return nil +} + +func IsSelfHostedReadyConditionTrue(irsa IRSASetup) bool { + return apimeta.IsStatusConditionTrue(irsa.Status.SelfHostedSetup, meta.ReadyCondition) +} + +type SelfHostedReason string + +const ( + SelfHostedReasonFailedOidc SelfHostedReason = "SelfHostedSetupFailedOidcCreation" + SelfHostedReasonFailedKeys SelfHostedReason = "SelfHostedSetupFailedKeysCreation" +) + //+kubebuilder:object:root=true //+kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="SelfHostedReady",type="string",JSONPath=".status.selfHostedSetup.conditions[?(@.type==\"Ready\")].status",description="" // IRSASetup represents a configuration for setting up IAM Roles for Service Accounts (IRSA) in a Kubernetes cluster. type IRSASetup struct { diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 1aa3344..184cfcb 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -21,6 +21,7 @@ limitations under the License. package v1alpha1 import ( + "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -40,26 +41,6 @@ func (in *Auth) DeepCopy() *Auth { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *CommonStatus) DeepCopyInto(out *CommonStatus) { - *out = *in - if in.Errors != nil { - in, out := &in.Errors, &out.Errors - *out = make([]string, len(*in)) - copy(*out, *in) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommonStatus. -func (in *CommonStatus) DeepCopy() *CommonStatus { - if in == nil { - return nil - } - out := new(CommonStatus) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Discovery) DeepCopyInto(out *Discovery) { *out = *in @@ -155,8 +136,13 @@ func (in *IRSASetupSpec) DeepCopy() *IRSASetupSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *IRSASetupStatus) DeepCopyInto(out *IRSASetupStatus) { *out = *in - in.SelfHostedSetup.DeepCopyInto(&out.SelfHostedSetup) - in.CommonSetup.DeepCopyInto(&out.CommonSetup) + if in.SelfHostedSetup != nil { + in, out := &in.SelfHostedSetup, &out.SelfHostedSetup + *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 IRSASetupStatus. diff --git a/config/crd/bases/irsa.kkb0318.github.io_irsasetups.yaml b/config/crd/bases/irsa.kkb0318.github.io_irsasetups.yaml index c3fbfb2..63714e9 100644 --- a/config/crd/bases/irsa.kkb0318.github.io_irsasetups.yaml +++ b/config/crd/bases/irsa.kkb0318.github.io_irsasetups.yaml @@ -14,7 +14,11 @@ spec: singular: irsasetup scope: Namespaced versions: - - name: v1alpha1 + - additionalPrinterColumns: + - jsonPath: .status.selfHostedSetup.conditions[?(@.type=="Ready")].status + name: SelfHostedReady + type: string + name: v1alpha1 schema: openAPIV3Schema: description: IRSASetup represents a configuration for setting up IAM Roles @@ -92,30 +96,75 @@ spec: status: description: IRSASetupStatus defines the observed state of IRSASetup properties: - commonSetup: - description: CommonStatus is a set of status attributes - properties: - errors: - items: - type: string - type: array - healthy: - type: boolean - required: - - healthy - type: object selfHostedSetup: - description: CommonStatus is a set of status attributes - properties: - errors: - items: + items: + description: "Condition contains details for one aspect of the current + state of this API Resource.\n---\nThis struct is intended for + direct use as an array at the field path .status.conditions. For + example,\n\n\n\ttype FooStatus struct{\n\t // Represents the + observations of a foo's current state.\n\t // Known .status.conditions.type + are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // + +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t + \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t + \ // other fields\n\t}" + 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 - type: array - healthy: - type: boolean - required: - - healthy - type: object + 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 type: object type: object served: true diff --git a/go.mod b/go.mod index ff4e70e..3bd3055 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ module github.com/kkb0318/irsa-manager -go 1.21 +go 1.22 -toolchain go1.21.8 +toolchain go1.22.2 require ( github.com/aws/aws-sdk-go-v2 v1.26.1 @@ -10,6 +10,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/iam v1.32.0 github.com/aws/aws-sdk-go-v2/service/s3 v1.53.1 github.com/aws/smithy-go v1.20.2 + github.com/fluxcd/pkg/apis/meta v1.4.0 github.com/go-jose/go-jose/v4 v4.0.1 github.com/onsi/ginkgo/v2 v2.17.1 github.com/onsi/gomega v1.30.0 diff --git a/go.sum b/go.sum index bef3eb4..7527925 100644 --- a/go.sum +++ b/go.sum @@ -49,6 +49,8 @@ github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/fluxcd/pkg/apis/meta v1.4.0 h1:nNdgB6FFHP3cubxZCViaCFDUVlAbpq9+hvKEIveOGMg= +github.com/fluxcd/pkg/apis/meta v1.4.0/go.mod h1:81sZ01ShTuLc1C3M1dFJNkINareBysvmrO1b8zJFFKs= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U= diff --git a/internal/controller/irsasetup_controller.go b/internal/controller/irsasetup_controller.go index e4a4660..72ffe1e 100644 --- a/internal/controller/irsasetup_controller.go +++ b/internal/controller/irsasetup_controller.go @@ -18,6 +18,7 @@ package controller import ( "context" + "fmt" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" @@ -120,7 +121,16 @@ func (r *IRSASetupReconciler) reconcileDelete(ctx context.Context, obj *irsav1al return nil } +// reconcileSelfhosted ensures that the self-hosted resources are set up correctly. +// This function performs the following operations based on the state of the object: +// - If the self-hosted setup has previously succeeded, the function returns immediately without making changes. +// - If the self-hosted setup was previously attempted but failed, or if it's being run for the first time, it will attempt to create all necessary resources. This includes the creation of key pairs, JWKs, OIDC IDP configurations, and Kubernetes secrets. +// - The function enforces a 'force update' strategy in case of failures related to kubernetes Secrets creation or OIDC setup. This means it starts from scratch to ensure all components are correctly configured. func reconcileSelfhosted(ctx context.Context, obj *irsav1alpha1.IRSASetup, awsClient awsclient.AwsClient, kubeClient *kubernetes.KubernetesClient) error { + if irsav1alpha1.IsSelfHostedReadyConditionTrue(*obj) { + // Selfhosted Setup have already succeeded + return nil + } keyPair, err := selfhosted.CreateKeyPair() if err != nil { return err @@ -139,14 +149,37 @@ func reconcileSelfhosted(ctx context.Context, obj *irsav1alpha1.IRSASetup, awsCl } kubeHandler := handler.NewKubernetesHandler(kubeClient) kubeHandler.Append(secret) + + var e error + var reason irsav1alpha1.SelfHostedReason + defer func() { + if e != nil { + *obj = irsav1alpha1.IRSASetupSelfHostedNotReady(*obj, string(reason), e.Error()) + } + }() + + var forceUpdate bool + condition := irsav1alpha1.IRSASetupSelfHostedReadyStatus(*obj) + switch irsav1alpha1.SelfHostedReason(condition.Reason) { + case irsav1alpha1.SelfHostedReasonFailedKeys, irsav1alpha1.SelfHostedReasonFailedOidc: + forceUpdate = true + default: + forceUpdate = false + } + fmt.Println(forceUpdate) // TODO: force Update logic err = selfhosted.Execute(ctx, factory) if err != nil { + e = err + reason = irsav1alpha1.SelfHostedReasonFailedOidc return err } err = kubeHandler.CreateAll(ctx) if err != nil { + e = err + reason = irsav1alpha1.SelfHostedReasonFailedKeys return err } + *obj = irsav1alpha1.IRSASetupSelfHostedReady(*obj, "SelfHostedSetupReady", e.Error()) return nil }