From 51c87a6bdf7869a5e315e41cea8d611fe19f0681 Mon Sep 17 00:00:00 2001 From: eoinfennessy Date: Sat, 30 Mar 2024 16:34:55 +0000 Subject: [PATCH 1/2] Create option types --- pkg/option/option.go | 192 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 pkg/option/option.go diff --git a/pkg/option/option.go b/pkg/option/option.go new file mode 100644 index 0000000..16c291f --- /dev/null +++ b/pkg/option/option.go @@ -0,0 +1,192 @@ +// +kubebuilder:object:generate=true + +/* +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 option + +// TODO: Move this package into a shared types repo + +import ( + "errors" + "fmt" + "reflect" + "strconv" + + "github.com/Jeffail/gabs/v2" + krakenv1alpha1 "github.com/kraken-iac/kraken/api/v1alpha1" +) + +type ValueFromConfigMap struct { + Name string `json:"name"` + Key string `json:"key"` +} + +func (vfcm ValueFromConfigMap) ToConfigMapDependency() krakenv1alpha1.ConfigMapDependency { + return krakenv1alpha1.ConfigMapDependency{ + Name: vfcm.Name, + Key: vfcm.Key, + } +} + +type ValueFromSecret struct { + Name string `json:"name"` + Key string `json:"key"` +} + +func (vfs ValueFromSecret) ToSecretDependency() { + panic("Not implemented") +} + +type ValueFromKrakenResource struct { + Kind string `json:"kind"` + Name string `json:"name"` + Path string `json:"path"` +} + +func (vfkr ValueFromKrakenResource) ToKrakenResourceDependency(kind reflect.Kind) krakenv1alpha1.KrakenResourceDependency { + return krakenv1alpha1.KrakenResourceDependency{ + Kind: vfkr.Kind, + Name: vfkr.Name, + Path: vfkr.Path, + ReflectKind: kind, + } +} + +type ValueFrom struct { + ConfigMap *ValueFromConfigMap `json:"configMap,omitempty"` + Secret *ValueFromSecret `json:"secret,omitempty"` + KrakenResource *ValueFromKrakenResource `json:"krakenResource,omitempty"` +} + +func (vf ValueFrom) AddToDependencyRequestSpec(dr *krakenv1alpha1.DependencyRequestSpec, kind reflect.Kind) { + if vf.KrakenResource != nil { + dr.KrakenResourceDependencies = append(dr.KrakenResourceDependencies, vf.KrakenResource.ToKrakenResourceDependency(kind)) + } + if vf.ConfigMap != nil { + dr.ConfigMapDependencies = append(dr.ConfigMapDependencies, vf.ConfigMap.ToConfigMapDependency()) + } + if vf.Secret != nil { + panic("Unimplemented") + } +} + +type String struct { + Value *string `json:"value,omitempty"` + ValueFrom *ValueFrom `json:"valueFrom,omitempty"` +} + +func (s String) ToApplicableValue(dv krakenv1alpha1.DependentValues) (*string, error) { + if s.Value != nil { + return s.Value, nil + } + if s.ValueFrom == nil { + return nil, nil + } + if s.ValueFrom.ConfigMap != nil { + return getValueFromConfigMap(s.ValueFrom.ConfigMap, dv.FromConfigMaps) + } + if s.ValueFrom.KrakenResource != nil { + return getValueFromKrakenResource[string](s.ValueFrom.KrakenResource, dv.FromKrakenResources) + } + return nil, errors.New("ValueFrom object is not nil but does not contain any non-nil pointer references") +} + +type Int struct { + Value *int `json:"value,omitempty"` + ValueFrom *ValueFrom `json:"valueFrom,omitempty"` +} + +func (i Int) ToApplicableValue(dv krakenv1alpha1.DependentValues) (*int, error) { + if i.Value != nil { + return i.Value, nil + } + if i.ValueFrom == nil { + return nil, nil + } + if i.ValueFrom.ConfigMap != nil { + valString, err := getValueFromConfigMap(i.ValueFrom.ConfigMap, dv.FromConfigMaps) + if err != nil { + return nil, err + } + val, err := strconv.Atoi(*valString) + if err != nil { + return nil, err + } + return &val, nil + } + if i.ValueFrom.KrakenResource != nil { + // Unmarshalled JSON numbers are of type float64 + valFloat, err := getValueFromKrakenResource[float64](i.ValueFrom.KrakenResource, dv.FromKrakenResources) + if err != nil { + return nil, err + } + val := int(*valFloat) + return &val, nil + } + return nil, errors.New("ValueFrom object is not nil but does not contain any non-nil pointer references") +} + +func getValueFromConfigMap(cmRef *ValueFromConfigMap, cmVals krakenv1alpha1.DependentValuesFromConfigMaps) (*string, error) { + cm, exists := cmVals[cmRef.Name] + if !exists { + return nil, fmt.Errorf("ConfigMap \"%s\" does not exist in DependentValues", cmRef.Name) + } + val, exists := cm[cmRef.Key] + if !exists { + return nil, fmt.Errorf("key \"%s\" does not exist in DependentValues ConfigMap \"%s\"", cmRef.Key, cmRef.Name) + } + return &val, nil +} + +func getValueFromKrakenResource[T any]( + krRef *ValueFromKrakenResource, + krVals krakenv1alpha1.DependentValuesFromKrakenResources, +) (*T, error) { + kind, exists := krVals[krRef.Kind] + if !exists { + return nil, fmt.Errorf("no entry for kind \"%s\" in DependentValues", krRef.Kind) + } + resource, exists := kind[krRef.Name] + if !exists { + return nil, fmt.Errorf("no entry for resource \"%s\" in DependentValues", krRef.Name) + } + jsonVal, exists := resource[krRef.Path] + if !exists { + return nil, fmt.Errorf("no entry for path \"%s\" in DependentValues", krRef.Path) + } + + jsonContainer, err := gabs.ParseJSON(jsonVal.Raw) + if err != nil { + return nil, fmt.Errorf("error parsing JSON: %s", err) + } + data := jsonContainer.Data() + + var val T + expectedType := reflect.TypeOf(val).Kind() + actualType := reflect.TypeOf(data).Kind() + if actualType != expectedType { + return nil, fmt.Errorf( + "provided value \"%s\" is of type \"%s\"; expected type \"%s\"", + data, + actualType, + expectedType, + ) + } + + val = data.(T) + return &val, nil +} From b9c63d30e25043dd6267f6fb974355b191262de9 Mon Sep 17 00:00:00 2001 From: eoinfennessy Date: Sat, 30 Mar 2024 16:36:52 +0000 Subject: [PATCH 2/2] Use DependencyRequest API to import referenced state --- api/v1alpha1/ec2instance_types.go | 33 +++- api/v1alpha1/zz_generated.deepcopy.go | 4 + ...ken-iac.eoinfennessy.com_ec2instances.yaml | 170 +++++++++++++++++- config/rbac/role.yaml | 12 ++ config/samples/aws_v1alpha1_ec2instance.yaml | 26 ++- .../dependencies/configmap-dependency.yaml | 7 + .../statedeclaration-dependency.yaml | 15 ++ go.mod | 3 +- go.sum | 8 + internal/controller/conversion.go | 72 ++++++++ internal/controller/ec2instance_controller.go | 168 +++++++++++++++-- pkg/ec2instance_client/ec2instance_client.go | 4 +- pkg/option/zz_generated.deepcopy.go | 148 +++++++++++++++ 13 files changed, 629 insertions(+), 41 deletions(-) create mode 100644 config/samples/dependencies/configmap-dependency.yaml create mode 100644 config/samples/dependencies/statedeclaration-dependency.yaml create mode 100644 internal/controller/conversion.go create mode 100644 pkg/option/zz_generated.deepcopy.go diff --git a/api/v1alpha1/ec2instance_types.go b/api/v1alpha1/ec2instance_types.go index 7e6cbc2..95836bc 100644 --- a/api/v1alpha1/ec2instance_types.go +++ b/api/v1alpha1/ec2instance_types.go @@ -17,6 +17,10 @@ limitations under the License. package v1alpha1 import ( + "reflect" + + "github.com/kraken-iac/aws-ec2-instance/pkg/option" + "github.com/kraken-iac/kraken/api/v1alpha1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -25,19 +29,32 @@ import ( // EC2InstanceSpec defines the desired state of EC2Instance type EC2InstanceSpec struct { - ImageId string `json:"imageId"` - InstanceType string `json:"instanceType"` - - //+kubebuilder:validation:Minimum=1 - MaxCount int `json:"maxCount"` - - //+kubebuilder:validation:Minimum=1 - MinCount int `json:"minCount"` + ImageID option.String `json:"imageID"` + InstanceType option.String `json:"instanceType"` + MaxCount option.Int `json:"maxCount"` + MinCount option.Int `json:"minCount"` // +optional Tags map[string]string `json:"tags,omitempty"` } +func (s EC2InstanceSpec) GenerateDependencyRequestSpec() v1alpha1.DependencyRequestSpec { + dr := v1alpha1.DependencyRequestSpec{} + if s.ImageID.ValueFrom != nil { + s.ImageID.ValueFrom.AddToDependencyRequestSpec(&dr, reflect.String) + } + if s.InstanceType.ValueFrom != nil { + s.InstanceType.ValueFrom.AddToDependencyRequestSpec(&dr, reflect.String) + } + if s.MaxCount.ValueFrom != nil { + s.MaxCount.ValueFrom.AddToDependencyRequestSpec(&dr, reflect.Int) + } + if s.MinCount.ValueFrom != nil { + s.MinCount.ValueFrom.AddToDependencyRequestSpec(&dr, reflect.Int) + } + return dr +} + // EC2InstanceStatus defines the observed state of EC2Instance type EC2InstanceStatus struct { Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index d3371c2..57787ff 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -87,6 +87,10 @@ func (in *EC2InstanceList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *EC2InstanceSpec) DeepCopyInto(out *EC2InstanceSpec) { *out = *in + in.ImageID.DeepCopyInto(&out.ImageID) + in.InstanceType.DeepCopyInto(&out.InstanceType) + in.MaxCount.DeepCopyInto(&out.MaxCount) + in.MinCount.DeepCopyInto(&out.MinCount) if in.Tags != nil { in, out := &in.Tags, &out.Tags *out = make(map[string]string, len(*in)) diff --git a/config/crd/bases/aws.kraken-iac.eoinfennessy.com_ec2instances.yaml b/config/crd/bases/aws.kraken-iac.eoinfennessy.com_ec2instances.yaml index 50fd897..0e76087 100644 --- a/config/crd/bases/aws.kraken-iac.eoinfennessy.com_ec2instances.yaml +++ b/config/crd/bases/aws.kraken-iac.eoinfennessy.com_ec2instances.yaml @@ -34,22 +34,176 @@ spec: spec: description: EC2InstanceSpec defines the desired state of EC2Instance properties: - imageId: - type: string + imageID: + properties: + value: + type: string + valueFrom: + properties: + configMap: + properties: + key: + type: string + name: + type: string + required: + - key + - name + type: object + krakenResource: + properties: + kind: + type: string + name: + type: string + path: + type: string + required: + - kind + - name + - path + type: object + secret: + properties: + key: + type: string + name: + type: string + required: + - key + - name + type: object + type: object + type: object instanceType: - type: string + properties: + value: + type: string + valueFrom: + properties: + configMap: + properties: + key: + type: string + name: + type: string + required: + - key + - name + type: object + krakenResource: + properties: + kind: + type: string + name: + type: string + path: + type: string + required: + - kind + - name + - path + type: object + secret: + properties: + key: + type: string + name: + type: string + required: + - key + - name + type: object + type: object + type: object maxCount: - minimum: 1 - type: integer + properties: + value: + type: integer + valueFrom: + properties: + configMap: + properties: + key: + type: string + name: + type: string + required: + - key + - name + type: object + krakenResource: + properties: + kind: + type: string + name: + type: string + path: + type: string + required: + - kind + - name + - path + type: object + secret: + properties: + key: + type: string + name: + type: string + required: + - key + - name + type: object + type: object + type: object minCount: - minimum: 1 - type: integer + properties: + value: + type: integer + valueFrom: + properties: + configMap: + properties: + key: + type: string + name: + type: string + required: + - key + - name + type: object + krakenResource: + properties: + kind: + type: string + name: + type: string + path: + type: string + required: + - kind + - name + - path + type: object + secret: + properties: + key: + type: string + name: + type: string + required: + - key + - name + type: object + type: object + type: object tags: additionalProperties: type: string type: object required: - - imageId + - imageID - instanceType - maxCount - minCount diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 4f4f539..5c76132 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -37,6 +37,18 @@ rules: verbs: - create - patch +- apiGroups: + - core.kraken-iac.eoinfennessy.com + resources: + - dependencyrequests + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - core.kraken-iac.eoinfennessy.com resources: diff --git a/config/samples/aws_v1alpha1_ec2instance.yaml b/config/samples/aws_v1alpha1_ec2instance.yaml index ef0f217..84c191c 100644 --- a/config/samples/aws_v1alpha1_ec2instance.yaml +++ b/config/samples/aws_v1alpha1_ec2instance.yaml @@ -9,7 +9,25 @@ metadata: app.kubernetes.io/created-by: aws-ec2-instance name: ec2instance-sample spec: - imageId: "ami-0277155c3f0ab2930" - instanceType: "t2.nano" - maxCount: 1 - minCount: 1 + imageID: + valueFrom: + configMap: + name: my-ec2-config + key: imageID + instanceType: + valueFrom: + krakenResource: + kind: ec2instance + name: my-instance + path: instances.0.instanceType + maxCount: + valueFrom: + configMap: + name: my-ec2-config + key: maxCount + minCount: + valueFrom: + krakenResource: + kind: ec2instance + name: my-instance + path: instances.0.minCount diff --git a/config/samples/dependencies/configmap-dependency.yaml b/config/samples/dependencies/configmap-dependency.yaml new file mode 100644 index 0000000..d212bda --- /dev/null +++ b/config/samples/dependencies/configmap-dependency.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: my-ec2-config +data: + imageID: "ami-0277155c3f0ab2930" + maxCount: "1" \ No newline at end of file diff --git a/config/samples/dependencies/statedeclaration-dependency.yaml b/config/samples/dependencies/statedeclaration-dependency.yaml new file mode 100644 index 0000000..d578492 --- /dev/null +++ b/config/samples/dependencies/statedeclaration-dependency.yaml @@ -0,0 +1,15 @@ +apiVersion: core.kraken-iac.eoinfennessy.com/v1alpha1 +kind: StateDeclaration +metadata: + labels: + app.kubernetes.io/name: statedeclaration + app.kubernetes.io/instance: statedeclaration-sample + app.kubernetes.io/part-of: kraken + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: kraken + name: ec2instance-my-instance +spec: + data: + instances: + - instanceType: t2.nano + minCount: 1 \ No newline at end of file diff --git a/go.mod b/go.mod index d15e1f6..53e5c03 100644 --- a/go.mod +++ b/go.mod @@ -27,6 +27,7 @@ require ( ) require ( + github.com/Jeffail/gabs/v2 v2.7.0 github.com/aws/aws-sdk-go-v2/config v1.26.6 github.com/aws/aws-sdk-go-v2/service/ec2 v1.146.0 github.com/beorn7/perks v1.0.1 // indirect @@ -52,7 +53,7 @@ require ( github.com/imdario/mergo v0.3.6 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/kraken-iac/kraken v0.1.0 + github.com/kraken-iac/kraken v0.3.1 github.com/mailru/easyjson v0.7.7 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect diff --git a/go.sum b/go.sum index 024a9ab..4bf92f0 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/Jeffail/gabs/v2 v2.7.0 h1:Y2edYaTcE8ZpRsR2AtmPu5xQdFDIthFG0jYhu5PY8kg= +github.com/Jeffail/gabs/v2 v2.7.0/go.mod h1:dp5ocw1FvBBQYssgHsG7I1WYsiLRtkUaB1FEtSwvNUw= github.com/aws/aws-sdk-go-v2 v1.24.1 h1:xAojnj+ktS95YZlDf0zxWBkbFtymPeDP+rvUQIH3uAU= github.com/aws/aws-sdk-go-v2 v1.24.1/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4= github.com/aws/aws-sdk-go-v2/config v1.26.6 h1:Z/7w9bUqlRI0FFQpetVuFYEsjzE3h7fpU6HuGmfPL/o= @@ -106,6 +108,12 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kraken-iac/kraken v0.1.0 h1:BtdOK5KfKqsEXJZBn9mQwHCRSFRiJPC82Dc7OVOohhg= github.com/kraken-iac/kraken v0.1.0/go.mod h1:DWYEW7ukNyx8y5y5mTKnIiKZOyc0xg8v/OhVqzekAk8= +github.com/kraken-iac/kraken v0.2.0 h1:5QVUUfni63wg5rgZPH7TEEQCiuQc+4Vp6v5PRZ5flzY= +github.com/kraken-iac/kraken v0.2.0/go.mod h1:DWYEW7ukNyx8y5y5mTKnIiKZOyc0xg8v/OhVqzekAk8= +github.com/kraken-iac/kraken v0.3.0 h1:ugVSR7NP8jtMXl9K43KbEkoGXZuzY4xwri/JHcQW3eA= +github.com/kraken-iac/kraken v0.3.0/go.mod h1:DWYEW7ukNyx8y5y5mTKnIiKZOyc0xg8v/OhVqzekAk8= +github.com/kraken-iac/kraken v0.3.1 h1:k4o5dSMdjL3Ry/73/DGlZ+64GgBaVP7+bBvsVmTONKo= +github.com/kraken-iac/kraken v0.3.1/go.mod h1:DWYEW7ukNyx8y5y5mTKnIiKZOyc0xg8v/OhVqzekAk8= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= diff --git a/internal/controller/conversion.go b/internal/controller/conversion.go new file mode 100644 index 0000000..2189e91 --- /dev/null +++ b/internal/controller/conversion.go @@ -0,0 +1,72 @@ +/* +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 ( + "fmt" + + "github.com/kraken-iac/aws-ec2-instance/api/v1alpha1" + krakenv1alpha1 "github.com/kraken-iac/kraken/api/v1alpha1" +) + +type ec2InstanceApplicableValues struct { + imageID string + instanceType string + maxCount int + minCount int +} + +func toApplicableValues( + ec2Spec v1alpha1.EC2InstanceSpec, + depValues krakenv1alpha1.DependentValues, +) (*ec2InstanceApplicableValues, error) { + av := ec2InstanceApplicableValues{} + + if imageID, err := ec2Spec.ImageID.ToApplicableValue(depValues); err != nil { + return nil, err + } else if imageID == nil { + return nil, fmt.Errorf("no applicable value provided for ImageID") + } else { + av.imageID = *imageID + } + + if instanceType, err := ec2Spec.InstanceType.ToApplicableValue(depValues); err != nil { + return nil, err + } else if instanceType == nil { + return nil, fmt.Errorf("no applicable value provided for InstanceType") + } else { + av.instanceType = *instanceType + } + + if maxCount, err := ec2Spec.MaxCount.ToApplicableValue(depValues); err != nil { + return nil, err + } else if maxCount == nil { + return nil, fmt.Errorf("no applicable value provided for MaxCount") + } else { + av.maxCount = *maxCount + } + + if minCount, err := ec2Spec.MinCount.ToApplicableValue(depValues); err != nil { + return nil, err + } else if minCount == nil { + return nil, fmt.Errorf("no applicable value provided for MinCount") + } else { + av.minCount = *minCount + } + + return &av, nil +} diff --git a/internal/controller/ec2instance_controller.go b/internal/controller/ec2instance_controller.go index 7ebc1bb..64e6891 100644 --- a/internal/controller/ec2instance_controller.go +++ b/internal/controller/ec2instance_controller.go @@ -20,6 +20,7 @@ import ( "context" "encoding/json" "fmt" + "reflect" "time" v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" @@ -36,7 +37,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" - awsv1alpha1 "github.com/kraken-iac/aws-ec2-instance/api/v1alpha1" + ec2instancev1alpha1 "github.com/kraken-iac/aws-ec2-instance/api/v1alpha1" ec2instanceclient "github.com/kraken-iac/aws-ec2-instance/pkg/ec2instance_client" krakenv1alpha1 "github.com/kraken-iac/kraken/api/v1alpha1" ) @@ -47,6 +48,8 @@ const ( nameTagKey string = "kraken-name" namespaceTagKey string = "kraken-namespace" + externalResourcePrefix string = "ec2instance" + conditionTypeReady string = "Ready" ) @@ -69,6 +72,7 @@ type EC2InstanceReconciler struct { //+kubebuilder:rbac:groups=aws.kraken-iac.eoinfennessy.com,resources=ec2instances/status,verbs=get;update;patch //+kubebuilder:rbac:groups=aws.kraken-iac.eoinfennessy.com,resources=ec2instances/finalizers,verbs=update //+kubebuilder:rbac:groups=core.kraken-iac.eoinfennessy.com,resources=statedeclarations,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=core.kraken-iac.eoinfennessy.com,resources=dependencyrequests,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=core,resources=events,verbs=create;patch // Reconcile is part of the main kubernetes reconciliation loop which aims to @@ -85,7 +89,7 @@ func (r *EC2InstanceReconciler) Reconcile(ctx context.Context, req ctrl.Request) log.Info("Reconcile triggered") // Fetch ec2Instance resource - ec2Instance := &awsv1alpha1.EC2Instance{} + ec2Instance := &ec2instancev1alpha1.EC2Instance{} if err := r.Client.Get(ctx, req.NamespacedName, ec2Instance); err != nil { if apierrors.IsNotFound(err) { log.Info("ec2Instance resource not found: Ignoring because it must have been deleted") @@ -153,6 +157,129 @@ func (r *EC2InstanceReconciler) Reconcile(ctx context.Context, req ctrl.Request) return ctrl.Result{}, nil } + // Construct DependencyRequest spec + newDependencyRequestSpec := ec2Instance.Spec.GenerateDependencyRequestSpec() + + // Fetch existing DependencyRequest if one exists + oldDependencyRequest := &krakenv1alpha1.DependencyRequest{} + var oldDependencyRequestExists bool + if err := r.Client.Get( + ctx, + client.ObjectKey{ + Name: fmt.Sprintf("%s-%s", externalResourcePrefix, req.Name), + Namespace: req.Namespace, + }, + oldDependencyRequest, + ); err != nil { + if apierrors.IsNotFound(err) { + oldDependencyRequestExists = false + } else { + log.Error(err, "Failed to fetch DependencyRequest resource: Requeuing") + return ctrl.Result{}, err + } + } else { + oldDependencyRequestExists = true + } + + // Delete old DependencyRequest if new DependencyRequest has no dependencies + if !newDependencyRequestSpec.HasDependencies() && oldDependencyRequestExists { + if err := r.Delete(ctx, oldDependencyRequest); err != nil { + log.Error(err, "Failed to delete DependencyRequest") + return ctrl.Result{}, client.IgnoreNotFound(err) + } + log.Info("Deleted DependencyRequest") + return ctrl.Result{}, nil + } + + if newDependencyRequestSpec.HasDependencies() { + // Create DependencyRequest + if !oldDependencyRequestExists { + dependencyRequest := &krakenv1alpha1.DependencyRequest{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-%s", externalResourcePrefix, req.Name), + Namespace: req.Namespace, + }, + Spec: newDependencyRequestSpec, + } + + if err := controllerutil.SetControllerReference( + ec2Instance, + dependencyRequest, + r.Scheme); err != nil { + log.Error(err, "Failed to set owner reference on StateDeclaration") + return reconcile.Result{}, err + } + + log.Info("Creating DependencyRequest", "name", dependencyRequest.ObjectMeta.Name) + if err := r.Client.Create(ctx, dependencyRequest); err != nil { + if apierrors.IsAlreadyExists(err) { + log.Info("DependencyRequest already exists") + return ctrl.Result{Requeue: true}, nil + } + log.Error(err, "Error creating DependencyRequest", "name", dependencyRequest.ObjectMeta.Name) + return ctrl.Result{}, err + } + // TODO: Update status conditions to include DependencyRequest info + return ctrl.Result{}, nil + } + + // Update DependencyRequest if old and new specs are different + if !reflect.DeepEqual(oldDependencyRequest.Spec, newDependencyRequestSpec) { + updatedDependencyRequest := oldDependencyRequest.DeepCopy() + updatedDependencyRequest.Spec = newDependencyRequestSpec + + log.Info("Updating DependencyRequest", "name", updatedDependencyRequest.ObjectMeta.Name) + if err := r.Client.Update(ctx, updatedDependencyRequest); err != nil { + log.Error(err, "Could not update DependencyRequest", "name", updatedDependencyRequest.ObjectMeta.Name) + return ctrl.Result{}, err + } + // TODO: Update status conditions to include DependencyRequest info + return ctrl.Result{}, nil + } + + // Return without requeue if DependencyRequest is not ready + if !meta.IsStatusConditionTrue( + oldDependencyRequest.Status.Conditions, + krakenv1alpha1.ConditionTypeReady, + ) { + log.Info("DependencyRequest is not yet ready") + dependencyRequestCondition := meta.FindStatusCondition( + oldDependencyRequest.Status.Conditions, + krakenv1alpha1.ConditionTypeReady, + ) + if dependencyRequestCondition == nil { + log.Info("DependencyRequest's status conditions are not yet set") + return ctrl.Result{Requeue: true}, nil + } + meta.SetStatusCondition( + &ec2Instance.Status.Conditions, + metav1.Condition{ + Type: conditionTypeReady, + Status: metav1.ConditionFalse, + Reason: "MissingDependency", + Message: dependencyRequestCondition.Message, + }, + ) + return ctrl.Result{}, r.Status().Update(ctx, ec2Instance) + } + } + + // Construct applicableValues object containing the actual values that will be applied + av, err := toApplicableValues(ec2Instance.Spec, oldDependencyRequest.Status.DependentValues) + if err != nil { + log.Error(err, "Could not construct applicable values") + meta.SetStatusCondition( + &ec2Instance.Status.Conditions, + metav1.Condition{ + Type: conditionTypeReady, + Status: metav1.ConditionFalse, + Reason: "DependencyError", + Message: fmt.Sprintf("Could not construct applicable values: %s", err), + }, + ) + return ctrl.Result{}, r.Status().Update(ctx, ec2Instance) + } + // Get running and pending instances matching name and namespace tags log.Info("Retrieving EC2 instances", "name", req.Name, "namespace", req.Namespace) instances, err := r.EC2InstanceClient.GetInstances(ctx, ec2instanceclient.FilterOptions{ @@ -182,9 +309,9 @@ func (r *EC2InstanceReconciler) Reconcile(ctx context.Context, req ctrl.Request) // TODO: compare all instances to spec and either update (if possible) or terminate those that do not match (update list) // Scale down - if len(instances) > ec2Instance.Spec.MaxCount { + if len(instances) > av.maxCount { log.Info("Scaling down EC2 instances") - terminationCount := len(instances) - ec2Instance.Spec.MaxCount + terminationCount := len(instances) - av.maxCount if _, err := r.EC2InstanceClient.TerminateInstances(ctx, instances[:terminationCount]); err != nil { log.Error(err, "Failed to terminate EC2 instances") meta.SetStatusCondition( @@ -201,13 +328,13 @@ func (r *EC2InstanceReconciler) Reconcile(ctx context.Context, req ctrl.Request) } // Scale up - if len(instances) < ec2Instance.Spec.MaxCount { + if len(instances) < av.maxCount { log.Info("Scaling up EC2 instances") maxCount, minCount := adjustMaxMinInstanceCount( len(instances), - ec2Instance.Spec.MaxCount, - ec2Instance.Spec.MinCount, + av.maxCount, + av.minCount, ) tags := makeInstanceTags(req, ec2Instance.Spec.Tags) @@ -215,8 +342,8 @@ func (r *EC2InstanceReconciler) Reconcile(ctx context.Context, req ctrl.Request) o, err := r.EC2InstanceClient.RunInstances(ctx, &ec2instanceclient.RunInstancesInput{ MaxCount: maxCount, MinCount: minCount, - ImageId: ec2Instance.Spec.ImageId, - InstanceType: ec2Instance.Spec.InstanceType, + ImageID: av.imageID, + InstanceType: av.instanceType, Tags: tags, }) if err != nil { @@ -235,6 +362,7 @@ func (r *EC2InstanceReconciler) Reconcile(ctx context.Context, req ctrl.Request) log.Info("Created instances", "instanceCount", len(o.Instances)) // Wait for pending instances to reach running state + log.Info("Waiting for pending instances to reach running state") if err := r.WaitUntilRunning( ctx, ec2instanceclient.FilterOptions{ @@ -247,6 +375,7 @@ func (r *EC2InstanceReconciler) Reconcile(ctx context.Context, req ctrl.Request) types.InstanceStateNameRunning, }, }, + // TODO: Make this time configurable time.Minute*2, ); err != nil { log.Error(err, "Encountered error waiting for running state") @@ -298,7 +427,7 @@ func (r *EC2InstanceReconciler) Reconcile(ctx context.Context, req ctrl.Request) // Create or update StateDeclaration stateDeclaration := &krakenv1alpha1.StateDeclaration{ ObjectMeta: metav1.ObjectMeta{ - Name: "ec2-" + req.Name, + Name: fmt.Sprintf("%s-%s", externalResourcePrefix, req.Name), Namespace: req.Namespace, }, } @@ -354,7 +483,7 @@ func (r *EC2InstanceReconciler) Reconcile(ctx context.Context, req ctrl.Request) } func (r *EC2InstanceReconciler) doFinalizerOperations( - ctx context.Context, req ctrl.Request, ec2Instance *awsv1alpha1.EC2Instance, + ctx context.Context, req ctrl.Request, ec2Instance *ec2instancev1alpha1.EC2Instance, ) error { log := log.FromContext(ctx) @@ -370,10 +499,12 @@ func (r *EC2InstanceReconciler) doFinalizerOperations( return err } - log.Info("Terminating EC2 instances") - if _, err := r.EC2InstanceClient.TerminateInstances(ctx, instances); err != nil { - log.Error(err, "Failed to terminate EC2 instances") - return err + if len(instances) > 0 { + log.Info("Terminating EC2 instances") + if _, err := r.EC2InstanceClient.TerminateInstances(ctx, instances); err != nil { + log.Error(err, "Failed to terminate EC2 instances") + return err + } } r.Recorder.Event(ec2Instance, "Warning", "Deleting", @@ -387,8 +518,9 @@ func (r *EC2InstanceReconciler) doFinalizerOperations( // SetupWithManager sets up the controller with the Manager. func (r *EC2InstanceReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). - For(&awsv1alpha1.EC2Instance{}). + For(&ec2instancev1alpha1.EC2Instance{}). Owns(&krakenv1alpha1.StateDeclaration{}). + Owns(&krakenv1alpha1.DependencyRequest{}). Complete(r) } @@ -412,11 +544,11 @@ func makeInstanceTags(req reconcile.Request, specTags map[string]string) map[str return tags } -func isMarkedForDeletion(ec2Instance *awsv1alpha1.EC2Instance) bool { +func isMarkedForDeletion(ec2Instance *ec2instancev1alpha1.EC2Instance) bool { return ec2Instance.DeletionTimestamp != nil } -func constructStateDeclarationData(ec2Instance awsv1alpha1.EC2Instance, instances []types.Instance) (*v1.JSON, error) { +func constructStateDeclarationData(ec2Instance ec2instancev1alpha1.EC2Instance, instances []types.Instance) (*v1.JSON, error) { dataMap := make(map[string]interface{}) dataMap["instances"] = instances dataMap["spec"] = ec2Instance.Spec diff --git a/pkg/ec2instance_client/ec2instance_client.go b/pkg/ec2instance_client/ec2instance_client.go index 9df0dc9..4a19e1d 100644 --- a/pkg/ec2instance_client/ec2instance_client.go +++ b/pkg/ec2instance_client/ec2instance_client.go @@ -29,7 +29,7 @@ func New(ctx context.Context, region string) (*ec2InstanceClient, error) { type RunInstancesInput struct { MaxCount int MinCount int - ImageId string + ImageID string InstanceType string Tags map[string]string } @@ -46,7 +46,7 @@ func (c ec2InstanceClient) RunInstances(ctx context.Context, params *RunInstance output, err := c.ec2Client.RunInstances(ctx, &ec2.RunInstancesInput{ MaxCount: aws.Int32(int32(params.MaxCount)), MinCount: aws.Int32(int32(params.MinCount)), - ImageId: aws.String(params.ImageId), + ImageId: aws.String(params.ImageID), InstanceType: types.InstanceType(params.InstanceType), TagSpecifications: tagSpecs, }) diff --git a/pkg/option/zz_generated.deepcopy.go b/pkg/option/zz_generated.deepcopy.go new file mode 100644 index 0000000..f6309f4 --- /dev/null +++ b/pkg/option/zz_generated.deepcopy.go @@ -0,0 +1,148 @@ +//go:build !ignore_autogenerated + +/* +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. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package option + +import () + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Int) DeepCopyInto(out *Int) { + *out = *in + if in.Value != nil { + in, out := &in.Value, &out.Value + *out = new(int) + **out = **in + } + if in.ValueFrom != nil { + in, out := &in.ValueFrom, &out.ValueFrom + *out = new(ValueFrom) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Int. +func (in *Int) DeepCopy() *Int { + if in == nil { + return nil + } + out := new(Int) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *String) DeepCopyInto(out *String) { + *out = *in + if in.Value != nil { + in, out := &in.Value, &out.Value + *out = new(string) + **out = **in + } + if in.ValueFrom != nil { + in, out := &in.ValueFrom, &out.ValueFrom + *out = new(ValueFrom) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new String. +func (in *String) DeepCopy() *String { + if in == nil { + return nil + } + out := new(String) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ValueFrom) DeepCopyInto(out *ValueFrom) { + *out = *in + if in.ConfigMap != nil { + in, out := &in.ConfigMap, &out.ConfigMap + *out = new(ValueFromConfigMap) + **out = **in + } + if in.Secret != nil { + in, out := &in.Secret, &out.Secret + *out = new(ValueFromSecret) + **out = **in + } + if in.KrakenResource != nil { + in, out := &in.KrakenResource, &out.KrakenResource + *out = new(ValueFromKrakenResource) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ValueFrom. +func (in *ValueFrom) DeepCopy() *ValueFrom { + if in == nil { + return nil + } + out := new(ValueFrom) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ValueFromConfigMap) DeepCopyInto(out *ValueFromConfigMap) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ValueFromConfigMap. +func (in *ValueFromConfigMap) DeepCopy() *ValueFromConfigMap { + if in == nil { + return nil + } + out := new(ValueFromConfigMap) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ValueFromKrakenResource) DeepCopyInto(out *ValueFromKrakenResource) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ValueFromKrakenResource. +func (in *ValueFromKrakenResource) DeepCopy() *ValueFromKrakenResource { + if in == nil { + return nil + } + out := new(ValueFromKrakenResource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ValueFromSecret) DeepCopyInto(out *ValueFromSecret) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ValueFromSecret. +func (in *ValueFromSecret) DeepCopy() *ValueFromSecret { + if in == nil { + return nil + } + out := new(ValueFromSecret) + in.DeepCopyInto(out) + return out +}